feat(values): Add flag -d/--values-directory

Introduced new CLI flag `-d`/`--values-directory`
for loading values from YAML files from a
specified directory. Following are the key
details:

- This will be used by the following commands:
  1. `install`
  2. `lint`
  3. `package`
  4. `template`
  5. `upgrade`
  Updated the help text of these commands with the
  same.

- Made this flag the lowest in precedence i.e.,
  values from values directory can be overridden
  by inputs from any other input flags
  (`-f`/`--values`, `--set-json`, `--set`,
  `--set-string`, `--set-file`, `--set-literal`).
  Note: The default values from the chart's
  `values.yaml` file will have the lowest
  precedence than all the input flags including
  `-d`/`--values-directory` by design. In order
  the values from `values.yaml` to override the
  values from `-d`/`--values-directory`, users can
  explicitly provide the chart's `values.yaml`
  file path using the `-f`/`--values` flag.

- This flag reads all `.yaml` files from the
  specified directory and its subdirectories
  recursively, in lexicographical order. For eg.
  for the following directory structure:

  foo/
  ├── bar/
  │    └── bar.yaml
  ├── baz/
  │    ├── baz.yaml
  │    └── qux.yaml
  ├── baz.txt
  └── foo.yaml

  The files will be read in the order:
  `bar/bar.yaml`, `baz/baz.yaml`, `baz/qux.yaml`,
  `foo.yaml`.

- If the specified directory does not exist, an
  error is returned.

- Non-YAML files in the directory are ignored.
  i.e., if the specified directory exists but
  contains no YAML files, an empty map is returned
  without errors.

- If multiple YAML files contain overlapping keys,
  the values are overridden in lexicographical
  order. For eg. in the above example, if both
  `baz/baz.yaml` and `foo.yaml` contain a key
  `replicaCount`, the value from `foo.yaml` will
  take precedence as it is read last.

Fixes helm#10416

Signed-off-by: Bhargav Ravuri <bhargav.ravuri@infracloud.io>
pull/11626/head
Bhargav Ravuri 1 year ago committed by Bhargav Ravuri
parent bdc459d73c
commit d5e31764c3
No known key found for this signature in database
GPG Key ID: 26D368286059A548

@ -21,8 +21,10 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"io/fs"
"net/url" "net/url"
"os" "os"
"path/filepath"
"strings" "strings"
"helm.sh/helm/v4/pkg/chart/v2/loader" "helm.sh/helm/v4/pkg/chart/v2/loader"
@ -32,21 +34,74 @@ import (
// Options captures the different ways to specify values // Options captures the different ways to specify values
type Options struct { type Options struct {
ValueFiles []string // -f/--values ValuesDirectories []string // -d / --values-directory
StringValues []string // --set-string ValueFiles []string // -f / --values
Values []string // --set StringValues []string // --set-string
FileValues []string // --set-file Values []string // --set
JSONValues []string // --set-json FileValues []string // --set-file
LiteralValues []string // --set-literal JSONValues []string // --set-json
LiteralValues []string // --set-literal
} }
// MergeValues merges values from files specified via -f/--values and directly // MergeValues merges values from multiple sources according to Helm's precedence rules.
// via --set-json, --set, --set-string, or --set-file, marshaling them to YAML //
func (opts *Options) MergeValues(p getter.Providers) (map[string]interface{}, error) { // The following list is ordered from lowest to highest precedence; items lower in the list override those above.
base := map[string]interface{}{} // 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 chart’s 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{}
// User specified a values files via -f/--values var valuesFiles []string
for _, filePath := range opts.ValueFiles {
// 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) raw, err := readFile(filePath, p)
if err != nil { if err != nil {
return nil, err return nil, err
@ -59,12 +114,12 @@ func (opts *Options) MergeValues(p getter.Providers) (map[string]interface{}, er
base = loader.MergeMaps(base, currentMap) base = loader.MergeMaps(base, currentMap)
} }
// User specified a value via --set-json // 3. User specified a value via --set-json.
for _, value := range opts.JSONValues { for _, value := range opts.JSONValues {
trimmedValue := strings.TrimSpace(value) trimmedValue := strings.TrimSpace(value)
if len(trimmedValue) > 0 && trimmedValue[0] == '{' { if len(trimmedValue) > 0 && trimmedValue[0] == '{' {
// If value is JSON object format, parse it as map // If value is JSON object format, parse it as map
var jsonMap map[string]interface{} var jsonMap map[string]any
if err := json.Unmarshal([]byte(trimmedValue), &jsonMap); err != nil { if err := json.Unmarshal([]byte(trimmedValue), &jsonMap); err != nil {
return nil, fmt.Errorf("failed parsing --set-json data JSON: %s", value) return nil, fmt.Errorf("failed parsing --set-json data JSON: %s", value)
} }
@ -77,23 +132,23 @@ func (opts *Options) MergeValues(p getter.Providers) (map[string]interface{}, er
} }
} }
// User specified a value via --set // 4. User specified a value via --set.
for _, value := range opts.Values { for _, value := range opts.Values {
if err := strvals.ParseInto(value, base); err != nil { if err := strvals.ParseInto(value, base); err != nil {
return nil, fmt.Errorf("failed parsing --set data: %w", err) return nil, fmt.Errorf("failed parsing --set data: %w", err)
} }
} }
// User specified a value via --set-string // 5. User specified a value via --set-string.
for _, value := range opts.StringValues { for _, value := range opts.StringValues {
if err := strvals.ParseIntoString(value, base); err != nil { if err := strvals.ParseIntoString(value, base); err != nil {
return nil, fmt.Errorf("failed parsing --set-string data: %w", err) return nil, fmt.Errorf("failed parsing --set-string data: %w", err)
} }
} }
// User specified a value via --set-file // 6. User specified a value via --set-file.
for _, value := range opts.FileValues { for _, value := range opts.FileValues {
reader := func(rs []rune) (interface{}, error) { reader := func(rs []rune) (any, error) {
bytes, err := readFile(string(rs), p) bytes, err := readFile(string(rs), p)
if err != nil { if err != nil {
return nil, err return nil, err
@ -105,7 +160,7 @@ func (opts *Options) MergeValues(p getter.Providers) (map[string]interface{}, er
} }
} }
// User specified a value via --set-literal // 7. User specified a value via --set-literal.
for _, value := range opts.LiteralValues { for _, value := range opts.LiteralValues {
if err := strvals.ParseLiteralInto(value, base); err != nil { if err := strvals.ParseLiteralInto(value, base); err != nil {
return nil, fmt.Errorf("failed parsing --set-literal data: %w", err) return nil, fmt.Errorf("failed parsing --set-literal data: %w", err)
@ -136,3 +191,74 @@ func readFile(filePath string, p getter.Providers) ([]byte, error) {
} }
return data.Bytes(), nil 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"
}

@ -298,7 +298,7 @@ func TestMergeValuesCLI(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
opts Options opts Options
expected map[string]interface{} expected map[string]any
wantErr bool wantErr bool
}{ }{
{ {
@ -306,8 +306,8 @@ func TestMergeValuesCLI(t *testing.T) {
opts: Options{ opts: Options{
JSONValues: []string{`{"foo": {"bar": "baz"}}`}, JSONValues: []string{`{"foo": {"bar": "baz"}}`},
}, },
expected: map[string]interface{}{ expected: map[string]any{
"foo": map[string]interface{}{ "foo": map[string]any{
"bar": "baz", "bar": "baz",
}, },
}, },
@ -317,9 +317,9 @@ func TestMergeValuesCLI(t *testing.T) {
opts: Options{ opts: Options{
JSONValues: []string{"foo.bar=[1,2,3]"}, JSONValues: []string{"foo.bar=[1,2,3]"},
}, },
expected: map[string]interface{}{ expected: map[string]any{
"foo": map[string]interface{}{ "foo": map[string]any{
"bar": []interface{}{1.0, 2.0, 3.0}, "bar": []any{1.0, 2.0, 3.0},
}, },
}, },
}, },
@ -328,7 +328,7 @@ func TestMergeValuesCLI(t *testing.T) {
opts: Options{ opts: Options{
Values: []string{"foo=bar"}, Values: []string{"foo=bar"},
}, },
expected: map[string]interface{}{ expected: map[string]any{
"foo": "bar", "foo": "bar",
}, },
}, },
@ -337,7 +337,7 @@ func TestMergeValuesCLI(t *testing.T) {
opts: Options{ opts: Options{
StringValues: []string{"foo=123"}, StringValues: []string{"foo=123"},
}, },
expected: map[string]interface{}{ expected: map[string]any{
"foo": "123", "foo": "123",
}, },
}, },
@ -346,7 +346,7 @@ func TestMergeValuesCLI(t *testing.T) {
opts: Options{ opts: Options{
LiteralValues: []string{"foo=true"}, LiteralValues: []string{"foo=true"},
}, },
expected: map[string]interface{}{ expected: map[string]any{
"foo": "true", "foo": "true",
}, },
}, },
@ -358,7 +358,7 @@ func TestMergeValuesCLI(t *testing.T) {
JSONValues: []string{`{"c": "foo1"}`}, JSONValues: []string{`{"c": "foo1"}`},
LiteralValues: []string{"d=bar1"}, LiteralValues: []string{"d=bar1"},
}, },
expected: map[string]interface{}{ expected: map[string]any{
"a": "foo", "a": "foo",
"b": "bar", "b": "bar",
"c": "foo1", "c": "foo1",
@ -372,6 +372,447 @@ func TestMergeValuesCLI(t *testing.T) {
}, },
wantErr: true, wantErr: true,
}, },
{
// When the --values-directory (-d) flag is used, the given directory is scanned recursively for YAML files.
// If the directory doesn't exist, the function should:
// - Return a nil map and an error.
name: "missing values directory",
opts: Options{
// Set using flag -d / --values-directory (read values from YAML files in the specified directory).
ValuesDirectories: []string{
"testdata/one-piece-chart/values.d/no-such-directory",
},
},
// Directory doesn't exist, so expect a nil map.
expected: nil,
// Directory doesn't exist, so expect an error.
wantErr: true,
},
{
// When the --values-directory (-d) flag is used, the given directory is scanned recursively for YAML files.
// If the directory exists and contains no files, the function should:
// - Return an empty map.
// - No errors should occur.
name: "values directory without files",
opts: Options{
// Set using flag -d / --values-directory (read values from YAML files in the specified directory).
ValuesDirectories: []string{
"testdata/one-piece-chart/values.d/values-directory-without-files",
},
},
// No YAML files, so expect an empty map.
expected: map[string]any{},
// No error expected.
wantErr: false,
},
{
// When the --values-directory (-d) flag is used, the given directory is scanned recursively for YAML files.
// If the directory exists and contains only non-YAML files, the function should:
// - Return an empty map.
// - No errors should occur.
name: "values directory with only non-YAML files",
opts: Options{
// Set using flag -d / --values-directory (read values from YAML files in the specified directory).
ValuesDirectories: []string{
"testdata/one-piece-chart/values.d/values-directory-with-only-non-yaml-files",
},
},
expected: map[string]any{},
// No error expected.
wantErr: false,
},
{
// When the --values-directory (-d) flag is used, the given directory is scanned recursively for YAML files.
// If the directory exists and contains single YAML file, the function should:
// - Read and parse the YAML file.
// - Return the result map, which contains the key-value pairs from the file.
// - No errors should occur.
name: "values directory with single YAML file",
opts: Options{
// Set using flag -d / --values-directory (read values from YAML files in the specified directory).
ValuesDirectories: []string{
"testdata/one-piece-chart/values.d/values-directory-with-single-yaml-file",
},
},
expected: map[string]any{
// Field "alliances" is read from "alliances-law.yaml" file.
"alliances": []any{
map[string]any{
"name": "Heart Pirates",
"captain": "Trafalgar D. Water Law",
},
},
},
// No error expected.
wantErr: false,
},
{
// When the --values-directory (-d) flag is used, the given directory is scanned recursively for YAML files.
// If the directory exists and contains one YAML file and one non-YAML file, the function should:
// - Read and parse the YAML file.
// - Return the result map, which contains the key-value pairs from the YAML file.
// - No errors should occur.
name: "values directory with one YAML file and one non-YAML file",
opts: Options{
// Set using flag -d / --values-directory (read values from YAML files in the specified directory).
ValuesDirectories: []string{
"testdata/one-piece-chart/values.d/values-directory-with-one-yaml-file-and-one-non-yaml-file",
},
},
expected: map[string]any{
// Field "ship" is read from "ship-thousand-sunny.yaml" file.
"ship": map[string]any{
"name": "Thousand Sunny",
"status": "Active",
"speed": "Fast",
},
},
// No error expected.
wantErr: false,
},
{
// When the --values-directory (-d) flag is used, the given directory is scanned recursively for YAML files.
// If the directory exists and contains multiple YAML files with no overlapping keys, the function should:
// - Read and parse all YAML files individually.
// - Merge the resulting key-value maps into a single map.
// - Return the merged map, where key-value pairs from all files are present, and none are overwritten
// (since there is no overlap).
// - No errors should occur.
name: "values directory with multiple YAML files, without overlapping keys",
opts: Options{
// Set using flag -d / --values-directory (read values from YAML files in the specified directory).
ValuesDirectories: []string{
"testdata/one-piece-chart/values.d/" +
"values-directory-with-multiple-yaml-files-without-overlapping-keys",
},
},
expected: map[string]any{
// Field "alliances" is read from "alliances-kid.yaml".
"alliances": []any{
map[string]any{
"name": "Kid Pirates",
"captain": "Eustass Kid",
},
},
// Field "ship" is read from "ship-going-merry.yaml".
"ship": map[string]any{
"name": "Going Merry",
"status": "Destroyed",
"speed": "Slow",
},
},
// No error expected.
wantErr: false,
},
{
// When the --values-directory (-d) flag is used, the given directory is scanned recursively for YAML files.
// If the directory exists and contains multiple YAML files with few overlapping keys, the function should:
// - Read and parse all YAML files individually.
// - Merge the resulting key-value maps into a single map.
// - Return the merged map, where key-value pairs from all files are present, and for overlapping keys, the
// value from the last file (lexicographically) processed takes precedence (overwrites previous values).
// - No errors should occur.
name: "values directory with multiple YAML files, with few overlapping keys",
opts: Options{
// Set using flag -d / --values-directory (read values from YAML files in the specified directory).
ValuesDirectories: []string{
"testdata/one-piece-chart/values.d/" +
"values-directory-with-multiple-yaml-files-with-few-overlapping-keys",
},
},
expected: map[string]any{
"crew": []any{
map[string]any{
"name": "Monkey D. Luffy",
"role": "Captain",
// Field "bounty" is set to "30,000,000" in "1-crew-luffy-east-blue.yaml", and is overridden
// as "100,000,000" in "2-crew-luffy-alabasta.yaml".
"bounty": "100,000,000",
"age": float64(19),
"devil_fruit": "Gomu Gomu no Mi",
},
},
},
// No error expected.
wantErr: false,
},
{
// When the --values-directory (-d) flag is used, the given directory is scanned recursively for YAML files.
// If the directory exists and contains multiple YAML files with all overlapping keys, the function should:
// - Read and parse all YAML files individually.
// - Merge the resulting key-value maps into a single map.
// - Return the merged map, where key-value pairs from the last file (lexicographically) overwriting all
// from previous files.
// - No errors should occur.
name: "values directory with multiple YAML files, with all overlapping keys",
opts: Options{
// Set using flag -d / --values-directory (read values from YAML files in the specified directory).
ValuesDirectories: []string{
"testdata/one-piece-chart/values.d/" +
"values-directory-with-multiple-yaml-files-with-all-overlapping-keys",
},
},
expected: map[string]any{
"ship": map[string]any{
// Field "name" is set to "Going Merry" in "ship-going-merry.yaml", and is overridden as
// "Thousand Sunny" in "ship-thousand-sunny.yaml".
"name": "Thousand Sunny",
// Field "status" is set to "Destroyed" in "ship-going-merry.yaml", and is overridden as
// "Active" in "ship-thousand-sunny.yaml".
"status": "Active",
// Field "speed" is set to "Slow" in "ship-going-merry.yaml", and is overridden as "Fast" in
// "ship-thousand-sunny.yaml".
"speed": "Fast",
},
},
// No error expected.
wantErr: false,
},
{
// When the --values-directory (-d) flag is used, the given directory is scanned recursively for YAML files.
// If the directory exists and nested directories and YAML files with no overlapping keys, the function
// should:
// - Read and parse all YAML files, from all levels, individually.
// - Merge the resulting key-value maps into a single map.
// - Return the merged map, where key-value pairs from all files are present, and none are overwritten
// (since there is no overlap).
// - No errors should occur.
name: "values directory with nested directories and YAML files, without overlapping keys",
opts: Options{
// Set using flag -d / --values-directory (read values from YAML files in the specified directory).
ValuesDirectories: []string{
"testdata/one-piece-chart/values.d/" +
"values-directory-with-nested-directories-and-YAML-files-without-overlapping-keys",
},
},
expected: map[string]any{
// Field "crew" is read from "1-crew-luffy-east-blue.yaml".
"crew": []any{
map[string]any{
"name": "Monkey D. Luffy",
"role": "Captain",
"bounty": "30,000,000",
"age": float64(19),
"devil_fruit": "Gomu Gomu no Mi",
},
},
// Field "alliances" is read from "subdir/alliances-grandfleet.yaml".
"alliances": []any{
map[string]any{
"name": "Straw Hat Grand Fleet",
"captain": "Various",
},
},
},
// No error expected.
wantErr: false,
},
{
// When the --values-directory (-d) flag is used, the given directory is scanned recursively for YAML files.
// If the directory exists and nested directories and YAML files with few overlapping keys, the function
// should:
// - Read and parse all YAML files, from all levels, individually.
// - Merge the resulting key-value maps into a single map.
// - Return the merged map, where key-value pairs from all files are present, and for overlapping keys, the
// value from the last file (lexicographically) processed takes precedence (overwrites previous values).
// - No errors should occur.
name: "values directory with nested directories and YAML files, with few overlapping keys",
opts: Options{
// Set using flag -d / --values-directory (read values from YAML files in the specified directory).
ValuesDirectories: []string{
"testdata/one-piece-chart/values.d/" +
"values-directory-with-nested-directories-and-YAML-files-with-few-overlapping-keys",
},
},
expected: map[string]any{
"crew": []any{
map[string]any{
"name": "Monkey D. Luffy",
"role": "Captain",
// Field "bounty" is set to "30,000,000" in "1-crew-luffy-east-blue.yaml", and is overridden
// as "100,000,000" in "subdir/2-crew-luffy-alabasta.yaml".
"bounty": "100,000,000",
"age": float64(19),
"devil_fruit": "Gomu Gomu no Mi",
},
},
},
// No error expected.
wantErr: false,
},
{
// When the --values-directory (-d) flag is used, the given directory is scanned recursively for YAML files.
// If the directory exists and nested directories and YAML files with all overlapping keys, the function
// should:
// - Read and parse all YAML files, from all levels, individually.
// - Merge the resulting key-value maps into a single map.
// - Return the merged map, where key-value pairs from the last file (lexicographically) overwriting all
// from previous files.
// - No errors should occur.
name: "values directory with nested directories and YAML files, with all overlapping keys",
opts: Options{
// Set using flag -d / --values-directory (read values from YAML files in the specified directory).
ValuesDirectories: []string{
"testdata/one-piece-chart/values.d/" +
"values-directory-with-nested-directories-and-YAML-files-with-all-overlapping-keys",
},
},
expected: map[string]any{
"ship": map[string]any{
// Field "name" is set to "Going Merry" in "ship-going-merry.yaml", and is overridden as
// "Thousand Sunny" in "subdir/ship-thousand-sunny.yaml".
"name": "Thousand Sunny",
// Field "status" is set to "Destroyed" in "ship-going-merry.yaml", and is overridden as
// "Active" in "subdir/ship-thousand-sunny.yaml".
"status": "Active",
// Field "speed" is set to "Slow" in "ship-going-merry.yaml", and is overridden as "Fast" in
// "subdir/ship-thousand-sunny.yaml".
"speed": "Fast",
},
},
// No error expected.
wantErr: false,
},
{
// When the --values-directory (-d) flag is used, the given directory is scanned recursively for YAML files
// with different extensions (case-insensitive). Ignore files with non-YAML extensions.
// If the directory exists and contains YAML files with different extensions (case-insensitive), the
// function should:
// - Read and parse all YAML files with different extensions (case-insensitive) individually. Ignore files
// with non-YAML extensions.
// - Merge the resulting key-value maps into a single map.
// - Return the merged map, where key-value pairs from all files are present.
// - No errors should occur.
name: "values directory with multiple YAML files, with different extensions case-insensitive, " +
"ignore other files",
opts: Options{
// Set using flag -d / --values-directory (read values from YAML files in the specified directory).
ValuesDirectories: []string{
"testdata/one-piece-chart/values.d/" +
"values-directory-with-multiple-yaml-files-with-different-extensions-case-insensitive",
},
},
expected: map[string]any{
// Field "crew" is read from "1-crew-luffy-east-blue.YML".
"crew": []any{
map[string]any{
"name": "Monkey D. Luffy",
"role": "Captain",
"bounty": "30,000,000",
"age": float64(19),
"devil_fruit": "Gomu Gomu no Mi",
},
},
// Field "alliances" is read from "alliances-kid.yml".
"alliances": []any{
map[string]any{
"name": "Kid Pirates",
"captain": "Eustass Kid",
},
},
// Field "ship" is read from "ship-going-merry.YAML".
"ship": map[string]any{
"name": "Going Merry",
"status": "Destroyed",
"speed": "Slow",
},
},
// No error expected.
wantErr: false,
},
{
// When the --values-directory (-d) flag is used to scan the directory recursively for YAML files, and the
// remaining input flags -f/--values, --set-string, --set, --set-file, --set-json, --set-literal are used to
// override values from the directory.
// If the directory exists and nested directories and YAML files with all overlapping keys, the function
// should:
// - Read and parse all YAML files, from all levels, individually.
// - Merge the resulting key-value maps into a single map.
// - Return the merged map, where key-value pairs from the last file (lexicographically) overwriting all
// from previous files.
// - No errors should occur.
name: "values directory with YAML files, and values overridden via all other flags " +
"(-f/--values, --set-string, --set, --set-file, --set-json, --set-literal)",
opts: Options{
// Set using flag -d / --values-directory (read values from YAML files in the specified directory).
ValuesDirectories: []string{
"testdata/one-piece-chart/values.d/" +
"values-directory-with-YAML-files-and-values-overridden-via-all-other-flags",
},
// Set using flag -f / --values (read values from the specified YAML file).
ValueFiles: []string{
"testdata/one-piece-chart/values.d/shared-values/alliances-kid.yaml",
},
// Set using flag --set-string (inline string key=value pairs, value must be a string).
StringValues: []string{
"crew[0].role=Legendary Captain",
},
// Set using flag --set (inline key=value pairs, value type is inferred).
Values: []string{
// String value "ship.name" will be overridden via --set.
"ship.name=Going Merry",
// Integer value "crew[0].age" will be overridden via --set.
"crew[0].age=20",
},
// Set using flag --set-file (read value from specified file).
FileValues: []string{
"crew[0].devil_fruit=testdata/one-piece-chart/values.d/shared-values/devil-fruit.txt",
},
// Set using flag --set-json (parse JSON string for value).
JSONValues: []string{
`ship.status={"condition":"Destroyed","where":"Enies Lobby"}`,
},
// Set using flag --set-literal (force literal string value, even if it is parsable to any other type).
LiteralValues: []string{
// String value "ship.speed" will be overridden via --set-literal.
"ship.speed=Slow",
// Integer value "crew[0].bounty" will be overridden via --set-literal.
// Note: --set-literal treats numbers as strings.
"crew[0].bounty=1,500,000,000",
},
},
expected: map[string]any{
"alliances": []any{ // Added via -f / --values.
map[string]any{
"captain": "Eustass Kid",
"name": "Kid Pirates",
},
},
"crew": []any{
map[string]any{
"name": "Monkey D. Luffy",
// Field "role" is overridden by --set-string.
"role": "Legendary Captain",
// Field "bounty" is overridden by --set-literal.
"bounty": "1,500,000,000",
// Field "devil_fruit" is overridden by --set-file.
"devil_fruit": "Hito Hito no Mi",
// Field "age" is overridden by --set.
// Note: --set treats numbers as int64.
"age": int64(20),
},
},
"ship": map[string]any{
// Field "name" is overridden by --set.
"name": "Going Merry",
// Field "status" is overridden by --set-json.
"status": map[string]any{
"condition": "Destroyed",
"where": "Enies Lobby",
},
// Field "speed" is overridden by --set-literal.
"speed": "Slow",
},
},
// No error expected.
wantErr: false,
},
} }
for _, tt := range tests { for _, tt := range tests {

@ -0,0 +1,6 @@
apiVersion: v2
name: onepiece-novals
description: Helm chart for testing --values-directory precedence with no defaults
type: application
version: 0.1.0
appVersion: "1.0"

@ -0,0 +1,3 @@
Values merged into this release:
kubectl get cm {{ include "onepiece-novals.fullname" . }}-values -o yaml

@ -0,0 +1,3 @@
{{- define "onepiece-novals.fullname" -}}
{{- printf "%s" .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- end -}}

@ -0,0 +1,7 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "onepiece-novals.fullname" . }}-values
data:
merged-values.yaml: |
{{ toYaml .Values | indent 4 }}

@ -0,0 +1,36 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "one-piece-chart.fullname" . }}-app
spec:
replicas: 1
selector:
matchLabels:
app: {{ include "one-piece-chart.fullname" . }}
template:
metadata:
labels:
app: {{ include "one-piece-chart.fullname" . }}
spec:
containers:
- name: onepiece-container
image: alpine:3.18
command: ["/bin/sh", "-c"]
args:
- |
echo "Mounted bounty files at /bounties:"
for f in /bounties/*.yaml; do
[ -e "$f" ] || continue
echo "---"
echo "Member: $(basename "$f" .yaml)"
cat "$f"
done
sleep 3600
volumeMounts:
- name: crew-bounties
mountPath: /bounties
readOnly: true
volumes:
- name: crew-bounties
configMap:
name: {{ include "one-piece-chart.fullname" . }}-crew

@ -0,0 +1,6 @@
crew:
- name: "Monkey D. Luffy"
role: "Captain"
bounty: "30,000,000"
age: 19
devil_fruit: "Gomu Gomu no Mi"

@ -0,0 +1,6 @@
crew:
- name: "Monkey D. Luffy"
role: "Captain"
bounty: "100,000,000"
age: 19
devil_fruit: "Gomu Gomu no Mi"

@ -0,0 +1,3 @@
alliances:
- name: "Straw Hat Grand Fleet"
captain: "Various"

@ -0,0 +1,3 @@
alliances:
- name: "Kid Pirates"
captain: "Eustass Kid"

@ -0,0 +1,3 @@
alliances:
- name: "Heart Pirates"
captain: "Trafalgar D. Water Law"

@ -0,0 +1,4 @@
# This will be ignored anyway.
ship:
name: "Polar Tang"
status: "Active"

@ -0,0 +1,4 @@
ship:
name: "Going Merry"
status: "Destroyed"
speed: "Slow"

@ -0,0 +1,4 @@
ship:
name: "Thousand Sunny"
status: "Active"
speed: "Fast"

@ -0,0 +1 @@
# Intentionally empty – no defaults.

@ -47,6 +47,10 @@ const (
) )
func addValueOptionsFlags(f *pflag.FlagSet, v *values.Options) { func addValueOptionsFlags(f *pflag.FlagSet, v *values.Options) {
f.StringSliceVarP(&v.ValuesDirectories, "values-directory", "d", []string{},
"specify values directory to scan recursively for \".yaml\" files. Files are loaded in lexical order. When "+
"keys overlap, values from the later file override those from earlier ones. Note: This flag can be "+
"specified multiple times.")
f.StringSliceVarP(&v.ValueFiles, "values", "f", []string{}, "specify values in a YAML file or a URL (can specify multiple)") f.StringSliceVarP(&v.ValueFiles, "values", "f", []string{}, "specify values in a YAML file or a URL (can specify multiple)")
f.StringArrayVar(&v.Values, "set", []string{}, "set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") f.StringArrayVar(&v.Values, "set", []string{}, "set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)")
f.StringArrayVar(&v.StringValues, "set-string", []string{}, "set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") f.StringArrayVar(&v.StringValues, "set-string", []string{}, "set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)")

@ -47,12 +47,14 @@ This command installs a chart archive.
The install argument must be a chart reference, a path to a packaged chart, The install argument must be a chart reference, a path to a packaged chart,
a path to an unpacked chart directory or a URL. a path to an unpacked chart directory or a URL.
To override values in a chart, use either the '--values' flag and pass in a file To override values in a chart, use the '-f'/'--values' flag to provide a file or
or use the '--set' flag and pass configuration from the command line, to force the '--set' flag to pass configuration directly from the command line. To force
a string value use '--set-string'. You can use '--set-file' to set individual a string value, use '--set-string'. Use '--set-file' when a value is too long
values from a file when the value itself is too long for the command line for the command line or dynamically generated. You can also use '--set-json' to
or is dynamically generated. You can also use '--set-json' to set json values provide JSON values (scalars, objects, or arrays) from the command line, either
(scalars/objects/arrays) from the command line. Additionally, you can use '--set-json' and passing json object as a string. as a direct argument or by passing a JSON object as a string. Alternatively, use
the '-d'/'--values-directory' flag to specify a directory containing YAML files
when you have many values or shared configurations split across files.
$ helm install -f myvalues.yaml myredis ./redis $ helm install -f myvalues.yaml myredis ./redis
@ -76,6 +78,9 @@ or
$ helm install --set-json '{"master":{"sidecars":[{"name":"sidecar","image":"myImage","imagePullPolicy":"Always","ports":[{"name":"portname","containerPort":1234}]}]}}' myredis ./redis $ helm install --set-json '{"master":{"sidecars":[{"name":"sidecar","image":"myImage","imagePullPolicy":"Always","ports":[{"name":"portname","containerPort":1234}]}]}}' myredis ./redis
or
$ helm install -d values.d/ myredis ./redis
You can specify the '--values'/'-f' flag multiple times. The priority will be given to the You can specify the '--values'/'-f' flag multiple times. The priority will be given to the
last (right-most) file specified. For example, if both myvalues.yaml and override.yaml last (right-most) file specified. For example, if both myvalues.yaml and override.yaml
contained a key called 'Test', the value set in override.yaml would take precedence: contained a key called 'Test', the value set in override.yaml would take precedence:
@ -96,6 +101,33 @@ And in the following example, 'foo' is set to '{"key1":"value1","key2":"bar"}':
$ helm install --set-json='foo={"key1":"value1","key2":"value2"}' --set-json='foo.key2="bar"' myredis ./redis $ helm install --set-json='foo={"key1":"value1","key2":"value2"}' --set-json='foo.key2="bar"' myredis ./redis
Key details about flag '-d'/'--values-directory':
- **Purpose:**
- Specify a directory containing values YAML files.
- **Behavior:**
- All YAML files in the directory and its nested subdirectories are loaded
recursively. Non-YAML files are skipped.
- Files within a single directory are processed in **lexicographical order**,
with later files overriding earlier ones when keys overlap.
- **Precedence:**
- This flag has the **lowest precedence** among all value flags
('-d'/'--values-directory', '-f'/'--values', '--set', '--set-string',
'--set-file', '--set-json', '--set-literal'). Values from these other flags
override values from files in the specified directory.
- Exception: The chart's default 'values.yaml' has a **lower precedence** than
the '-d'/'--values-directory' flag, i.e., the values from files in the
directory can override it. To let default values override directory files,
include 'values.yaml' explicitly via '-f'/'--values'.
- **Multiple Directories:**
- The flag can be specified **multiple times**.
- When multiple directories are provided, files in directories specified later
override values from earlier directories.
- **Lexicographical ordering** applies within each directory (and its nested
subdirectories) and not between directories specified with multiple
'-d'/'--values-directory' flags.
$ helm install -d default-values/ -d prod-overrides/ myredis ./redis
To check the generated manifests of a release without installing the chart, To check the generated manifests of a release without installing the chart,
the --debug and --dry-run flags can be combined. the --debug and --dry-run flags can be combined.

@ -50,12 +50,14 @@ argument can be either: a chart reference('example/mariadb'), a path to a chart
a packaged chart, or a fully qualified URL. For chart references, the latest a packaged chart, or a fully qualified URL. For chart references, the latest
version will be specified unless the '--version' flag is set. version will be specified unless the '--version' flag is set.
To override values in a chart, use either the '--values' flag and pass in a file To override values in a chart, use the '-f'/'--values' flag to provide a file or
or use the '--set' flag and pass configuration from the command line, to force string the '--set' flag to pass configuration directly from the command line. To force
values, use '--set-string'. You can use '--set-file' to set individual a string value, use '--set-string'. Use '--set-file' when a value is too long
values from a file when the value itself is too long for the command line for the command line or dynamically generated. You can also use '--set-json' to
or is dynamically generated. You can also use '--set-json' to set json values provide JSON values (scalars, objects, or arrays) from the command line, either
(scalars/objects/arrays) from the command line. Additionally, you can use '--set-json' and passing json object as a string. as a direct argument or by passing a JSON object as a string. Alternatively, use
the '-d'/'--values-directory' flag to specify a directory containing YAML files
when you have many values or shared configurations split across files.
You can specify the '--values'/'-f' flag multiple times. The priority will be given to the You can specify the '--values'/'-f' flag multiple times. The priority will be given to the
last (right-most) file specified. For example, if both myvalues.yaml and override.yaml last (right-most) file specified. For example, if both myvalues.yaml and override.yaml
@ -76,6 +78,33 @@ or '--set' flags. Priority is given to new values.
$ helm upgrade --reuse-values --set foo=bar --set foo=newbar redis ./redis $ helm upgrade --reuse-values --set foo=bar --set foo=newbar redis ./redis
Key details about flag '-d'/'--values-directory':
- **Purpose:**
- Specify a directory containing values YAML files.
- **Behavior:**
- All YAML files in the directory and its nested subdirectories are loaded
recursively. Non-YAML files are skipped.
- Files within a single directory are processed in **lexicographical order**,
with later files overriding earlier ones when keys overlap.
- **Precedence:**
- This flag has the **lowest precedence** among all value flags
('-d'/'--values-directory', '-f'/'--values', '--set', '--set-string',
'--set-file', '--set-json', '--set-literal'). Values from these other flags
override values from files in the specified directory.
- Exception: The chart's default 'values.yaml' has a **lower precedence** than
the '-d'/'--values-directory' flag, i.e., the values from files in the
directory can override it. To let default values override directory files,
include 'values.yaml' explicitly via '-f'/'--values'.
- **Multiple Directories:**
- The flag can be specified **multiple times**.
- When multiple directories are provided, files in directories specified later
override values from earlier directories.
- **Lexicographical ordering** applies within each directory (and its nested
subdirectories) and not between directories specified with multiple
'-d'/'--values-directory' flags.
$ helm upgrade -d default-values/ -d prod-overrides/ myredis ./redis
The --dry-run flag will output all generated chart manifests, including Secrets The --dry-run flag will output all generated chart manifests, including Secrets
which can contain sensitive values. To hide Kubernetes Secrets use the which can contain sensitive values. To hide Kubernetes Secrets use the
--hide-secret flag. Please carefully consider how and when these flags are used. --hide-secret flag. Please carefully consider how and when these flags are used.

Loading…
Cancel
Save