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 3 months 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"
"fmt"
"io"
"io/fs"
"net/url"
"os"
"path/filepath"
"strings"
"helm.sh/helm/v4/pkg/chart/v2/loader"
@ -32,21 +34,74 @@ import (
// Options captures the different ways to specify values
type Options struct {
ValueFiles []string // -f/--values
StringValues []string // --set-string
Values []string // --set
FileValues []string // --set-file
JSONValues []string // --set-json
LiteralValues []string // --set-literal
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 files specified via -f/--values and directly
// via --set-json, --set, --set-string, or --set-file, marshaling them to YAML
func (opts *Options) MergeValues(p getter.Providers) (map[string]interface{}, error) {
base := map[string]interface{}{}
// 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{}
// User specified a values files via -f/--values
for _, filePath := range opts.ValueFiles {
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
@ -59,12 +114,12 @@ func (opts *Options) MergeValues(p getter.Providers) (map[string]interface{}, er
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 {
trimmedValue := strings.TrimSpace(value)
if len(trimmedValue) > 0 && trimmedValue[0] == '{' {
// 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 {
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 {
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
// 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)
}
}
// User specified a value via --set-file
// 6. User specified a value via --set-file.
for _, value := range opts.FileValues {
reader := func(rs []rune) (interface{}, error) {
reader := func(rs []rune) (any, error) {
bytes, err := readFile(string(rs), p)
if err != nil {
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 {
if err := strvals.ParseLiteralInto(value, base); err != nil {
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
}
// 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 {
name string
opts Options
expected map[string]interface{}
expected map[string]any
wantErr bool
}{
{
@ -306,8 +306,8 @@ func TestMergeValuesCLI(t *testing.T) {
opts: Options{
JSONValues: []string{`{"foo": {"bar": "baz"}}`},
},
expected: map[string]interface{}{
"foo": map[string]interface{}{
expected: map[string]any{
"foo": map[string]any{
"bar": "baz",
},
},
@ -317,9 +317,9 @@ func TestMergeValuesCLI(t *testing.T) {
opts: Options{
JSONValues: []string{"foo.bar=[1,2,3]"},
},
expected: map[string]interface{}{
"foo": map[string]interface{}{
"bar": []interface{}{1.0, 2.0, 3.0},
expected: map[string]any{
"foo": map[string]any{
"bar": []any{1.0, 2.0, 3.0},
},
},
},
@ -328,7 +328,7 @@ func TestMergeValuesCLI(t *testing.T) {
opts: Options{
Values: []string{"foo=bar"},
},
expected: map[string]interface{}{
expected: map[string]any{
"foo": "bar",
},
},
@ -337,7 +337,7 @@ func TestMergeValuesCLI(t *testing.T) {
opts: Options{
StringValues: []string{"foo=123"},
},
expected: map[string]interface{}{
expected: map[string]any{
"foo": "123",
},
},
@ -346,7 +346,7 @@ func TestMergeValuesCLI(t *testing.T) {
opts: Options{
LiteralValues: []string{"foo=true"},
},
expected: map[string]interface{}{
expected: map[string]any{
"foo": "true",
},
},
@ -358,7 +358,7 @@ func TestMergeValuesCLI(t *testing.T) {
JSONValues: []string{`{"c": "foo1"}`},
LiteralValues: []string{"d=bar1"},
},
expected: map[string]interface{}{
expected: map[string]any{
"a": "foo",
"b": "bar",
"c": "foo1",
@ -372,6 +372,447 @@ func TestMergeValuesCLI(t *testing.T) {
},
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 {

@ -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) {
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.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)")

@ -47,12 +47,14 @@ This command installs a chart archive.
The install argument must be a chart reference, a path to a packaged chart,
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
or use the '--set' flag and pass configuration from the command line, to force
a string value use '--set-string'. You can use '--set-file' to set individual
values from a file when the value itself is too long for the command line
or is dynamically generated. You can also use '--set-json' to set json values
(scalars/objects/arrays) from the command line. Additionally, you can use '--set-json' and passing json object as a string.
To override values in a chart, use the '-f'/'--values' flag to provide a file or
the '--set' flag to pass configuration directly from the command line. To force
a string value, use '--set-string'. Use '--set-file' when a value is too long
for the command line or dynamically generated. You can also use '--set-json' to
provide JSON values (scalars, objects, or arrays) from the command line, either
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
@ -76,6 +78,9 @@ or
$ 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
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:
@ -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
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,
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
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
or use the '--set' flag and pass configuration from the command line, to force string
values, use '--set-string'. You can use '--set-file' to set individual
values from a file when the value itself is too long for the command line
or is dynamically generated. You can also use '--set-json' to set json values
(scalars/objects/arrays) from the command line. Additionally, you can use '--set-json' and passing json object as a string.
To override values in a chart, use the '-f'/'--values' flag to provide a file or
the '--set' flag to pass configuration directly from the command line. To force
a string value, use '--set-string'. Use '--set-file' when a value is too long
for the command line or dynamically generated. You can also use '--set-json' to
provide JSON values (scalars, objects, or arrays) from the command line, either
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
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
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
which can contain sensitive values. To hide Kubernetes Secrets use the
--hide-secret flag. Please carefully consider how and when these flags are used.

Loading…
Cancel
Save