pkg/template/cmdutil/values.go (149 lines of code) (raw):
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package cmdutil
import (
"bytes"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/elastic/harp/pkg/sdk/cmdutil"
"github.com/elastic/harp/pkg/sdk/flags/strvals"
"github.com/elastic/harp/pkg/sdk/log"
"github.com/elastic/harp/pkg/template/values"
)
// Inspired from Helm v3
// https://github.com/helm/helm/blob/master/pkg/cli/values/options.go
// ValueOptions represents value loader options.
type ValueOptions struct {
ValueFiles []string
StringValues []string
Values []string
FileValues []string
}
// MergeValues merges values from files specified via -f/--values and directly
// via --set, --set-string, or --set-file, marshaling them to YAML
func (opts *ValueOptions) MergeValues() (map[string]interface{}, error) {
base := map[string]interface{}{}
// save the current directory and chdir back to it when done
currentDirectory, err := os.Getwd()
if err != nil {
return nil, fmt.Errorf("unable to save current directory path: %w", err)
}
// User specified a values files via --values
for _, filePath := range opts.ValueFiles {
currentMap := map[string]interface{}{}
// Process each file path
if err := processFilePath(currentDirectory, filePath, ¤tMap); err != nil {
return nil, err
}
// Merge with the previous map
base = mergeMaps(base, currentMap)
}
// User specified a value via --set
for _, value := range opts.Values {
if err := strvals.ParseInto(value, base); err != nil {
return nil, fmt.Errorf("failed parsing --set data: %w", err)
}
}
// User specified a value via --set-string
for _, value := range opts.StringValues {
if err := strvals.ParseIntoString(value, base); err != nil {
return nil, fmt.Errorf("failed parsing --set-string data: %w", err)
}
}
// User specified a value via --set-file
for _, value := range opts.FileValues {
reader := func(rs []rune) (interface{}, error) {
b, err := os.ReadFile(string(rs))
return string(b), err
}
if err := strvals.ParseIntoFile(value, base, reader); err != nil {
return nil, fmt.Errorf("failed parsing --set-file data: %w", err)
}
}
return base, nil
}
// -----------------------------------------------------------------------------
func processFilePath(currentDirectory, filePath string, result interface{}) error {
defer func() {
log.CheckErr("unable to reset to current working directory", os.Chdir(currentDirectory))
}()
// Check for type overrides
parts := strings.Split(filePath, ":")
filePath = parts[0]
valuePrefix := ""
inputType := ""
if len(parts) > 1 {
var err error
// Expand if using homedir alias
filePath, err = cmdutil.Expand(filePath)
if err != nil {
return fmt.Errorf("unable to expand homedir: %w", err)
}
// <type>:<path>
inputType = parts[1]
// Check prefix usage
// <path>:<type>:<prefix>
if len(parts) > 2 {
valuePrefix = parts[2]
}
}
// Retrieve file type from extension
fileType := getFileType(filePath, inputType)
// Retrieve appropriate parser
p, err := values.GetParser(fileType)
if err != nil {
return fmt.Errorf("error occurred during parser instance retrieval for type '%s': %w", fileType, err)
}
// Drain file content
_, err = os.Stat(filePath)
if err != nil {
return fmt.Errorf(
"unable to os.Stat file name %s before attempting to build reader from current directory %s: error: %w",
filePath,
currentDirectory,
err,
)
}
reader, err := cmdutil.Reader(filePath)
if err != nil {
return fmt.Errorf("unable to build a reader from '%s' for current directory %s: %w",
filePath,
currentDirectory,
err)
}
// Drain reader
var contentBytes []byte
contentBytes, err = io.ReadAll(reader)
if err != nil {
return fmt.Errorf("unable to drain all reader content from '%s': %w", filePath, err)
}
// Check prefix
if valuePrefix != "" {
// Parse with detected parser
var fileContent interface{}
if err := p.Unmarshal(contentBytes, &fileContent); err != nil {
return fmt.Errorf("unable to unmarshal content from '%s' as '%s': %w", filePath, fileType, err)
}
// Re-encode JSON prepending the prefix
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(map[string]interface{}{
valuePrefix: fileContent,
}); err != nil {
return fmt.Errorf("unable to re-encode as JSON with prefix '%s', content from '%s' as '%s': %w", valuePrefix, filePath, fileType, err)
}
// Send as result
if err := json.NewDecoder(&buf).Decode(result); err != nil {
return fmt.Errorf("unable to decode json content from '%s' parsed as '%s': %w", filePath, fileType, err)
}
} else if err := p.Unmarshal(contentBytes, result); err != nil {
return fmt.Errorf("unable to unmarshal content from '%s' as '%s', you should use an explicit prefix: %w", filePath, fileType, err)
}
// No error
return nil
}
func mergeMaps(a, b map[string]interface{}) map[string]interface{} {
out := make(map[string]interface{}, len(a))
for k, v := range a {
out[k] = v
}
for k, v := range b {
if v, ok := v.(map[string]interface{}); ok {
if bv, ok := out[k]; ok {
if bv, ok := bv.(map[string]interface{}); ok {
out[k] = mergeMaps(bv, v)
continue
}
}
}
out[k] = v
}
return out
}
func getFileType(fileName, input string) string {
// Format override
if input != "" {
return input
}
// Stdin filename assumed as YAML
if fileName == "-" {
return "yaml"
}
// No extension return filename
if filepath.Ext(fileName) == "" {
return filepath.Base(fileName)
}
// Extract extension
fileExtension := filepath.Ext(fileName)
// Return extension whithout the '.'
return fileExtension[1:]
}