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, ¤tMap); 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 }