github.com/zntrio/harp/v2@v2.0.9/pkg/template/cmdutil/values.go (about)

     1  // Licensed to Elasticsearch B.V. under one or more contributor
     2  // license agreements. See the NOTICE file distributed with
     3  // this work for additional information regarding copyright
     4  // ownership. Elasticsearch B.V. licenses this file to you under
     5  // the Apache License, Version 2.0 (the "License"); you may
     6  // not use this file except in compliance with the License.
     7  // You may obtain a copy of the License at
     8  //
     9  //     http://www.apache.org/licenses/LICENSE-2.0
    10  //
    11  // Unless required by applicable law or agreed to in writing,
    12  // software distributed under the License is distributed on an
    13  // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    14  // KIND, either express or implied.  See the License for the
    15  // specific language governing permissions and limitations
    16  // under the License.
    17  
    18  package cmdutil
    19  
    20  import (
    21  	"bytes"
    22  	"encoding/json"
    23  	"fmt"
    24  	"io"
    25  	"os"
    26  	"path"
    27  	"path/filepath"
    28  	"strings"
    29  
    30  	"github.com/zntrio/harp/v2/pkg/sdk/cmdutil"
    31  	"github.com/zntrio/harp/v2/pkg/sdk/flags/strvals"
    32  	"github.com/zntrio/harp/v2/pkg/sdk/log"
    33  	"github.com/zntrio/harp/v2/pkg/template/values"
    34  )
    35  
    36  // Inspired from Helm v3
    37  // https://github.com/helm/helm/blob/master/pkg/cli/values/options.go
    38  
    39  // ValueOptions represents value loader options.
    40  type ValueOptions struct {
    41  	ValueFiles   []string
    42  	StringValues []string
    43  	Values       []string
    44  	FileValues   []string
    45  }
    46  
    47  // MergeValues merges values from files specified via -f/--values and directly
    48  // via --set, --set-string, or --set-file, marshaling them to YAML.
    49  func (opts *ValueOptions) MergeValues() (map[string]interface{}, error) {
    50  	base := map[string]interface{}{}
    51  
    52  	// save the current directory and chdir back to it when done
    53  	currentDirectory, err := os.Getwd()
    54  	if err != nil {
    55  		return nil, fmt.Errorf("unable to save current directory path: %w", err)
    56  	}
    57  
    58  	// User specified a values files via --values
    59  	for _, filePath := range opts.ValueFiles {
    60  		currentMap := map[string]interface{}{}
    61  
    62  		// Process each file path
    63  		if err := processFilePath(currentDirectory, filePath, &currentMap); err != nil {
    64  			return nil, err
    65  		}
    66  
    67  		// Merge with the previous map
    68  		base = mergeMaps(base, currentMap)
    69  	}
    70  
    71  	// User specified a value via --set
    72  	for _, value := range opts.Values {
    73  		if err := strvals.ParseInto(value, base); err != nil {
    74  			return nil, fmt.Errorf("failed parsing --set data: %w", err)
    75  		}
    76  	}
    77  
    78  	// User specified a value via --set-string
    79  	for _, value := range opts.StringValues {
    80  		if err := strvals.ParseIntoString(value, base); err != nil {
    81  			return nil, fmt.Errorf("failed parsing --set-string data: %w", err)
    82  		}
    83  	}
    84  
    85  	// User specified a value via --set-file
    86  	for _, value := range opts.FileValues {
    87  		reader := func(rs []rune) (interface{}, error) {
    88  			bytes, err := os.ReadFile(string(rs))
    89  			return string(bytes), err
    90  		}
    91  		if err := strvals.ParseIntoFile(value, base, reader); err != nil {
    92  			return nil, fmt.Errorf("failed parsing --set-file data: %w", err)
    93  		}
    94  	}
    95  
    96  	return base, nil
    97  }
    98  
    99  // -----------------------------------------------------------------------------
   100  
   101  func processFilePath(currentDirectory, filePath string, result interface{}) error {
   102  	defer func() {
   103  		log.CheckErr("unable to reset to current working directory", os.Chdir(currentDirectory))
   104  	}()
   105  
   106  	// Check for type overrides
   107  	parts := strings.Split(filePath, ":")
   108  
   109  	filePath = parts[0]
   110  	valuePrefix := ""
   111  	inputType := ""
   112  
   113  	if len(parts) > 1 {
   114  		var err error
   115  
   116  		// Expand if using homedir alias
   117  		filePath, err = cmdutil.Expand(filePath)
   118  		if err != nil {
   119  			return fmt.Errorf("unable to expand homedir: %w", err)
   120  		}
   121  
   122  		// <type>:<path>
   123  		inputType = parts[1]
   124  
   125  		// Check prefix usage
   126  		// <path>:<type>:<prefix>
   127  		if len(parts) > 2 {
   128  			valuePrefix = parts[2]
   129  		}
   130  	}
   131  
   132  	// Retrieve file type from extension
   133  	fileType := getFileType(filePath, inputType)
   134  
   135  	// Retrieve appropriate parser
   136  	p, err := values.GetParser(fileType)
   137  	if err != nil {
   138  		return fmt.Errorf("error occurred during parser instance retrieval for type %q: %w", fileType, err)
   139  	}
   140  
   141  	// Change current directory if filePath is not Stdin
   142  	if filePath != "-" {
   143  		// Rebase current working dir to target file to process file inclusions
   144  		// Split directory and filename
   145  		var confDir string
   146  		confDir, filePath = path.Split(filePath)
   147  		// If confDir is not blank (current path)
   148  		if confDir != "" {
   149  			if errChDir := os.Chdir(confDir); errChDir != nil {
   150  				return fmt.Errorf("unable to change working directory for %q: %w", confDir, errChDir)
   151  			}
   152  		}
   153  	}
   154  
   155  	// Drain file content
   156  	reader, err := cmdutil.Reader(filePath)
   157  	if err != nil {
   158  		return fmt.Errorf("unable to build a reader from %q: %w", filePath, err)
   159  	}
   160  
   161  	// Drain reader
   162  	var contentBytes []byte
   163  	contentBytes, err = io.ReadAll(reader)
   164  	if err != nil {
   165  		return fmt.Errorf("unable to drain all reader content from %q: %w", filePath, err)
   166  	}
   167  
   168  	// Check prefix
   169  	if valuePrefix != "" {
   170  		// Parse with detected parser
   171  		var fileContent interface{}
   172  		if err := p.Unmarshal(contentBytes, &fileContent); err != nil {
   173  			return fmt.Errorf("unable to unmarshal content from %q as %q: %w", filePath, fileType, err)
   174  		}
   175  
   176  		// Re-encode JSON prepending the prefix
   177  		var buf bytes.Buffer
   178  		if err := json.NewEncoder(&buf).Encode(map[string]interface{}{
   179  			valuePrefix: fileContent,
   180  		}); err != nil {
   181  			return fmt.Errorf("unable to re-encode as JSON with prefix %q, content from %q as %q: %w", valuePrefix, filePath, fileType, err)
   182  		}
   183  
   184  		// Send as result
   185  		if err := json.NewDecoder(&buf).Decode(result); err != nil {
   186  			return fmt.Errorf("unable to decode json content from %q parsed as %q: %w", filePath, fileType, err)
   187  		}
   188  	} else if err := p.Unmarshal(contentBytes, result); err != nil {
   189  		return fmt.Errorf("unable to unmarshal content from %q as %q, you should use an explicit prefix: %w", filePath, fileType, err)
   190  	}
   191  
   192  	// No error
   193  	return nil
   194  }
   195  
   196  func mergeMaps(a, b map[string]interface{}) map[string]interface{} {
   197  	out := make(map[string]interface{}, len(a))
   198  	for k, v := range a {
   199  		out[k] = v
   200  	}
   201  	for k, v := range b {
   202  		if v, ok := v.(map[string]interface{}); ok {
   203  			if bv, ok := out[k]; ok {
   204  				if bv, ok := bv.(map[string]interface{}); ok {
   205  					out[k] = mergeMaps(bv, v)
   206  					continue
   207  				}
   208  			}
   209  		}
   210  		out[k] = v
   211  	}
   212  	return out
   213  }
   214  
   215  func getFileType(fileName, input string) string {
   216  	// Format override
   217  	if input != "" {
   218  		return input
   219  	}
   220  
   221  	// Stdin filename assumed as YAML
   222  	if fileName == "-" {
   223  		return "yaml"
   224  	}
   225  
   226  	// No extension return filename
   227  	if filepath.Ext(fileName) == "" {
   228  		return filepath.Base(fileName)
   229  	}
   230  
   231  	// Extract extension
   232  	fileExtension := filepath.Ext(fileName)
   233  
   234  	// Return extension whithout the '.'
   235  	return fileExtension[1:]
   236  }