pull/11626/merge
Bhargav Ravuri 7 months ago committed by GitHub
commit 01db73ce1d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -44,6 +44,7 @@ const (
func addValueOptionsFlags(f *pflag.FlagSet, v *values.Options) { func addValueOptionsFlags(f *pflag.FlagSet, v *values.Options) {
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.StringSliceVarP(&v.ValuesDirectories, "values-directory", "d", []string{}, "specify values directory to recursively read for value's YAML files. Note: The YAML files in the directory are read in the lexical order. (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)")
f.StringArrayVar(&v.FileValues, "set-file", []string{}, "set values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2)") f.StringArrayVar(&v.FileValues, "set-file", []string{}, "set values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2)")

@ -18,8 +18,10 @@ package values
import ( import (
"io" "io"
"io/fs"
"net/url" "net/url"
"os" "os"
"path/filepath"
"strings" "strings"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -31,21 +33,48 @@ 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 ValueFiles []string // -f/--values
StringValues []string // --set-string ValuesDirectories []string // -d/--values-directory
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 specified via any of the following flags, and marshals them to YAML:
// via --set-json, --set, --set-string, or --set-file, marshaling them to YAML // 1. -d/--values-directory - from values file(s) in the directory(s)
// 2. -f/--values - from values file(s) or URL
// 3. --set-json - from input JSON
// 4. --set - from input key-value pairs
// 5. --set-string - from input key-value pairs, with string values, always
// 6. --set-file - from files
// 7. --set-literal - from input string literal
//
// The precedence order of inputs are 1 to 7, where 1 gets evaluated first and 7 last. i.e., If key1="val1" in inputs
// from --values-directory, and key1="val2" in --values, the second overwrites the first and the final value of key1
// is "val2". Similarly values from --set-json are replaced from that of --values, and so on.
func (opts *Options) MergeValues(p getter.Providers) (map[string]interface{}, error) { func (opts *Options) MergeValues(p getter.Providers) (map[string]interface{}, error) {
base := map[string]interface{}{} base := map[string]interface{}{}
// User specified a values files via -f/--values var valuesFiles []string
for _, filePath := range opts.ValueFiles {
// User specified directory(s) via -d/--values-directory
for _, dir := range opts.ValuesDirectories {
// Recursive list of YAML files in input values directory
files, err := recursiveListOfFilesInDir(dir, `.yaml`)
if err != nil {
// Error already wrapped
return nil, err
}
valuesFiles = append(valuesFiles, files...)
}
// User specified values files via -f/--values
valuesFiles = append(valuesFiles, opts.ValueFiles...)
for _, filePath := range valuesFiles {
currentMap := map[string]interface{}{} currentMap := map[string]interface{}{}
bytes, err := readFile(filePath, p) bytes, err := readFile(filePath, p)
@ -145,3 +174,31 @@ func readFile(filePath string, p getter.Providers) ([]byte, error) {
} }
return data.Bytes(), err return data.Bytes(), err
} }
// recursiveListOfFilesInDir lists the directory recursively, i.e., files in all nested directories.
// The list can be filtered by file extension. If no extension is specified, it returns all files.
//
// Result format: [<dir>/<file>, ..., <dir>/<sub-dir>/<file> ...]
func recursiveListOfFilesInDir(directory, extension string) ([]string, error) {
var files []string
// Traverse through the directory, recursively
err := filepath.WalkDir(directory, func(path string, file fs.DirEntry, err error) error {
// Check if accessing the file failed
if err != nil {
return errors.Wrapf(err, "failed to read info of file %q", path)
}
// When the file has the required extension, or when extension is not specified
if !file.IsDir() && (extension == "" || filepath.Ext(path) == extension) {
files = append(files, path)
}
return nil
})
if err != nil {
return nil, errors.Wrapf(err, "failed to recursively list files in directory %q", directory)
}
return files, nil
}

@ -23,7 +23,7 @@ import (
"helm.sh/helm/v4/pkg/getter" "helm.sh/helm/v4/pkg/getter"
) )
func TestMergeValues(t *testing.T) { func Test_mergeMaps(t *testing.T) {
nestedMap := map[string]interface{}{ nestedMap := map[string]interface{}{
"foo": "bar", "foo": "bar",
"baz": map[string]string{ "baz": map[string]string{
@ -86,3 +86,147 @@ func TestReadFile(t *testing.T) {
t.Errorf("Expected error when has special strings") t.Errorf("Expected error when has special strings")
} }
} }
func TestOptions_MergeValues(t *testing.T) {
const (
crewNameKey = `crew`
shipNameKey = `ship`
powerUserskey = `power-users`
nonPowerUsersKey = `non-power-users`
strawHatsCrew = `Straw Hat Pirates`
strawHatsShip1 = `Going Merry`
strawHatsShip2 = `Thousand Sunny`
)
var (
powerUsersVal = []interface{}{
"Luffy",
"Chopper",
"Robin",
"Brook",
}
nonPowerUsersVal = []interface{}{
"Zoro",
"Nami",
"Ussop",
"Sanji",
"Franky",
"Jinbei",
}
)
type args struct {
p getter.Providers
}
tests := []struct {
name string
opts Options
args args
want map[string]interface{}
wantErr bool
}{
{
name: "--values-directory with single level",
opts: Options{
ValueFiles: []string{},
ValuesDirectories: []string{
"testdata/chart-with-values-dir/values.d",
},
StringValues: []string{},
Values: []string{},
FileValues: []string{},
JSONValues: []string{},
},
args: args{
p: []getter.Provider{},
},
want: map[string]interface{}{
powerUserskey: powerUsersVal,
nonPowerUsersKey: nonPowerUsersVal,
},
wantErr: false,
},
{
name: "--values-directory with nested directories",
opts: Options{
ValueFiles: []string{},
ValuesDirectories: []string{
"testdata/multi-level-values-dir/values.d",
},
StringValues: []string{},
Values: []string{},
FileValues: []string{},
JSONValues: []string{},
},
args: args{
p: []getter.Provider{},
},
want: map[string]interface{}{
crewNameKey: strawHatsCrew,
shipNameKey: strawHatsShip1,
powerUserskey: powerUsersVal,
nonPowerUsersKey: nonPowerUsersVal,
},
wantErr: false,
},
{
name: "--values-directory value overwritten by --values",
opts: Options{
ValueFiles: []string{
"testdata/multi-level-values-dir/ship.yaml",
},
ValuesDirectories: []string{
"testdata/multi-level-values-dir/values.d",
},
StringValues: []string{},
Values: []string{},
FileValues: []string{},
JSONValues: []string{},
},
args: args{
p: []getter.Provider{},
},
want: map[string]interface{}{
crewNameKey: strawHatsCrew,
shipNameKey: strawHatsShip2, // This is the value overwritten by values file "ship.yaml"
powerUserskey: powerUsersVal,
nonPowerUsersKey: nonPowerUsersVal,
},
wantErr: false,
},
{
name: "--values-directory with missing directory",
opts: Options{
ValueFiles: []string{},
ValuesDirectories: []string{
"testdata/chart-with-values-dir/non-existing/",
},
StringValues: []string{},
Values: []string{},
FileValues: []string{},
JSONValues: []string{},
},
args: args{
p: []getter.Provider{},
},
want: map[string]interface{}(nil),
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.opts.MergeValues(tt.args.p)
if (err != nil) != tt.wantErr {
t.Errorf("Options.MergeValues() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Expected result from MergeValues() = %v, got %v", tt.want, got)
}
})
}
}

@ -0,0 +1,7 @@
non-power-users:
- Zoro
- Nami
- Ussop
- Sanji
- Franky
- Jinbei

@ -0,0 +1,5 @@
power-users:
- Luffy
- Chopper
- Robin
- Brook

@ -0,0 +1,7 @@
non-power-users:
- Zoro
- Nami
- Ussop
- Sanji
- Franky
- Jinbei

@ -0,0 +1,5 @@
power-users:
- Luffy
- Chopper
- Robin
- Brook
Loading…
Cancel
Save