You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
helm/pkg/cli/values/options.go

265 lines
8.4 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/*
Copyright The Helm Authors.
Licensed 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 values
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/fs"
"net/url"
"os"
"path/filepath"
"strings"
"helm.sh/helm/v4/pkg/chart/v2/loader"
"helm.sh/helm/v4/pkg/getter"
"helm.sh/helm/v4/pkg/strvals"
)
// Options captures the different ways to specify values
type Options struct {
ValuesDirectories []string // -d / --values-directory
ValueFiles []string // -f / --values
StringValues []string // --set-string
Values []string // --set
FileValues []string // --set-file
JSONValues []string // --set-json
LiteralValues []string // --set-literal
}
// MergeValues merges values from multiple sources according to Helm's precedence rules.
//
// The following list is ordered from lowest to highest precedence; items lower in the list override those above.
// i.e., values from sources later in the list take precedence over earlier ones:
//
// 1. -d / --values-directory : Values files from one or more directories.
// 2. -f / --values : Values files or URLs.
// 3. --set-json : Values provided as raw JSON.
// 4. --set : Inline key=value pairs.
// 5. --set-string : Inline key=value pairs (values always treated as strings).
// 6. --set-file : Values read from file contents.
// 7. --set-literal : Values provided as raw string literals.
//
// For example, if `captain=Luffy` is set via --values-directory (1) and `captain=Usopp` is set via --values (2), the
// final merged value for `captain` will be "Usopp".
//
// ---
//
// In case any supported flag is specified multiple times, the latter occurrence has higher precedence, i.e. overrides
// the former.
//
// For example, for `--set captain=Luffy --set captain=Usopp`, the final merged value for `captain` will be "Usopp".
//
// This applies to all values flags (-d/--values-directory, -f/--values, --set-json, --set, --set-string, --set-file,
// and --set-literal).
//
// ---
//
// Additional context: The precedence of default values.
//
// - By default, Helm reads values from the charts values.yaml file (if present).
// - These default values have the lowest precedence (level 0), meaning any values specified via the above-mentioned
// flags will override them.
// - If the same values.yaml file is explicitly provided using -f/--values, its values can override those loaded via
// -d/--values-directory. However, in that case, they are no longer considered default values.
//
// Note: Default values are not handled by this function, but understanding their precedence is important for the
// overall behavior.
func (opts *Options) MergeValues(p getter.Providers) (map[string]any, error) {
base := map[string]any{}
var valuesFiles []string
// 1. User specified directory(s) via -d/--values-directory.
for _, dir := range opts.ValuesDirectories {
// Recursively find all .yaml files in the directory.
files, err := listYAMLFilesRecursively(dir)
if err != nil {
// Error is already wrapped.
return nil, err
}
valuesFiles = append(valuesFiles, files...)
}
// 2. User specified values files via -f/--values.
valuesFiles = append(valuesFiles, opts.ValueFiles...)
for _, filePath := range valuesFiles {
raw, err := readFile(filePath, p)
if err != nil {
return nil, err
}
currentMap, err := loader.LoadValues(bytes.NewReader(raw))
if err != nil {
return nil, fmt.Errorf("failed to parse %s: %w", filePath, err)
}
// Merge with the previous map
base = loader.MergeMaps(base, currentMap)
}
// 3. User specified a value via --set-json.
for _, value := range opts.JSONValues {
trimmedValue := strings.TrimSpace(value)
if len(trimmedValue) > 0 && trimmedValue[0] == '{' {
// If value is JSON object format, parse it as map
var jsonMap map[string]any
if err := json.Unmarshal([]byte(trimmedValue), &jsonMap); err != nil {
return nil, fmt.Errorf("failed parsing --set-json data JSON: %s", value)
}
base = loader.MergeMaps(base, jsonMap)
} else {
// Otherwise, parse it as key=value format
if err := strvals.ParseJSON(value, base); err != nil {
return nil, fmt.Errorf("failed parsing --set-json data %s", value)
}
}
}
// 4. 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)
}
}
// 5. 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)
}
}
// 6. User specified a value via --set-file.
for _, value := range opts.FileValues {
reader := func(rs []rune) (any, error) {
bytes, err := readFile(string(rs), p)
if err != nil {
return nil, err
}
return string(bytes), err
}
if err := strvals.ParseIntoFile(value, base, reader); err != nil {
return nil, fmt.Errorf("failed parsing --set-file data: %w", err)
}
}
// 7. User specified a value via --set-literal.
for _, value := range opts.LiteralValues {
if err := strvals.ParseLiteralInto(value, base); err != nil {
return nil, fmt.Errorf("failed parsing --set-literal data: %w", err)
}
}
return base, nil
}
// readFile load a file from stdin, the local directory, or a remote file with a url.
func readFile(filePath string, p getter.Providers) ([]byte, error) {
if strings.TrimSpace(filePath) == "-" {
return io.ReadAll(os.Stdin)
}
u, err := url.Parse(filePath)
if err != nil {
return nil, err
}
// FIXME: maybe someone handle other protocols like ftp.
g, err := p.ByScheme(u.Scheme)
if err != nil {
return os.ReadFile(filePath)
}
data, err := g.Get(filePath, getter.WithURL(filePath))
if err != nil {
return nil, err
}
return data.Bytes(), nil
}
// listYAMLFilesRecursively walks a directory tree and returns a lexicographically sorted list of all YAML files.
//
// Example: (dir="foo")
//
// foo/
// ├── bar/
// │ └── bar.yaml
// ├── baz/
// │ ├── baz.yaml
// │ └── qux.yaml
// ├── baz.txt
// └── foo.yaml
//
// Result: ["foo/bar/bar.yaml", "foo/baz/baz.yaml", "foo/baz/qux.yaml", "foo/foo.yaml"]
func listYAMLFilesRecursively(dir string) ([]string, error) {
var files []string
// Check if the directory exists and is a directory.
info, err := os.Stat(dir)
if err != nil {
return nil, fmt.Errorf("failed to access values directory %q: %w", dir, err)
}
if !info.IsDir() {
return nil, fmt.Errorf("path %q is not a directory", dir)
}
// Walk the directory tree in lexical order. For the above example, this will visit:
// 1. foo/bar
// 2. foo/bar/bar.yaml
// 3. foo/baz
// 4. foo/baz/baz.yaml
// 5. foo/baz/qux.yaml
// 6. foo/baz.txt
// 7. foo/foo.yaml
//
// The inner function filters the “.yaml” files as follows:
// 1. foo/bar/bar.yaml
// 2. foo/baz/baz.yaml
// 3. foo/baz/qux.yaml
// 4. foo/foo.yaml
//
// Note: Since filepath.WalkDir walks in lexical order, the returned list of files is also sorted lexicographically.
err = filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return fmt.Errorf("failed to walk directory %q: %w", path, err)
}
// Collect YAML files (.yaml or .yml, case-insensitive), skipping directories.
if !d.IsDir() && isYamlFileExtension(d.Name()) {
files = append(files, path)
}
return nil
})
if err != nil {
return nil, fmt.Errorf("failed to list files in directory %q: %w", dir, err)
}
return files, nil
}
// isYamlFileExtension checks if the given file name has a YAML file extension. It returns true for files ending with
// .yaml or .yml (case-insensitive).
func isYamlFileExtension(fileName string) bool {
// Extract file extension and convert to lower case for case-insensitive comparison.
ext := strings.ToLower(filepath.Ext(fileName))
// Check for .yaml or .yml extensions.
return ext == ".yaml" || ext == ".yml"
}