From 7599c5d489f486e00982eb2be8f471535130152e Mon Sep 17 00:00:00 2001 From: Taylor Thomas Date: Tue, 24 Sep 2019 12:42:56 -0600 Subject: [PATCH] feat(*): Ports `--set-file` flag to v3 I made a few modifications from the original code to fit in with the new code layout and to clarify a few things. This is a port of #3758 Signed-off-by: Taylor Thomas --- cmd/helm/install.go | 8 ++++- cmd/helm/upgrade.go | 4 ++- pkg/cli/values/options.go | 16 +++++++-- pkg/strvals/parser.go | 71 +++++++++++++++++++++++++++++++------- pkg/strvals/parser_test.go | 33 ++++++++++++++++++ 5 files changed, 116 insertions(+), 16 deletions(-) diff --git a/cmd/helm/install.go b/cmd/helm/install.go index 509ad2564..898231a3a 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -42,7 +42,9 @@ 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'. +a string value use '--set-string'. In case a value is large and therefore +you want not to use neither '--values' nor '--set', use '--set-file' to read the +single large value from file. $ helm install -f myvalues.yaml myredis ./redis @@ -54,6 +56,9 @@ or $ helm install --set-string long_int=1234567890 myredis ./redis +or + $ helm install --set-file my_script=dothings.sh 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: @@ -140,6 +145,7 @@ 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.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.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)") } func addChartPathOptionsFlags(f *pflag.FlagSet, c *action.ChartPathOptions) { diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go index 647e73c83..f04641dd5 100644 --- a/cmd/helm/upgrade.go +++ b/cmd/helm/upgrade.go @@ -42,7 +42,9 @@ 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'. +values, use '--set-string'. In case a value is large and therefore +you want not to use neither '--values' nor '--set', use '--set-file' to read the +single large value from file. 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 diff --git a/pkg/cli/values/options.go b/pkg/cli/values/options.go index e9f7a3ca3..9eade297e 100644 --- a/pkg/cli/values/options.go +++ b/pkg/cli/values/options.go @@ -33,10 +33,11 @@ type Options struct { ValueFiles []string StringValues []string Values []string + FileValues []string } -// MergeValues merges values from files specified via -f/--values and -// directly via --set or --set-string, marshaling them to YAML +// MergeValues merges values from files specified via -f/--values and directly +// via --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{}{} @@ -70,6 +71,17 @@ func (opts *Options) MergeValues(p getter.Providers) (map[string]interface{}, er } } + // User specified a value via --set-file + for _, value := range opts.FileValues { + reader := func(rs []rune) (interface{}, error) { + bytes, err := readFile(string(rs), p) + return string(bytes), err + } + if err := strvals.ParseIntoFile(value, base, reader); err != nil { + return nil, errors.Wrap(err, "failed parsing --set-file data") + } + } + return base, nil } diff --git a/pkg/strvals/parser.go b/pkg/strvals/parser.go index a2f02fdd1..eaadbfb07 100644 --- a/pkg/strvals/parser.go +++ b/pkg/strvals/parser.go @@ -70,6 +70,20 @@ func ParseInto(s string, dest map[string]interface{}) error { return t.parse() } +// ParseFile parses a set line, but its final value is loaded from the file at the path specified by the original value. +// +// A set line is of the form name1=path1,name2=path2 +// +// When the files at path1 and path2 contained "val1" and "val2" respectively, the set line is consumed as +// name1=val1,name2=val2 +func ParseFile(s string, reader RunesValueReader) (map[string]interface{}, error) { + vals := map[string]interface{}{} + scanner := bytes.NewBufferString(s) + t := newFileParser(scanner, vals, reader) + err := t.parse() + return vals, err +} + // ParseIntoString parses a strvals line nad merges the result into dest. // // This method always returns a string as the value. @@ -79,16 +93,36 @@ func ParseIntoString(s string, dest map[string]interface{}) error { return t.parse() } +// ParseIntoFile parses a filevals line and merges the result into dest. +// +// This method always returns a string as the value. +func ParseIntoFile(s string, dest map[string]interface{}, reader RunesValueReader) error { + scanner := bytes.NewBufferString(s) + t := newFileParser(scanner, dest, reader) + return t.parse() +} + +// RunesValueReader is a function that takes the given value (a slice of runes) +// and returns the parsed value +type RunesValueReader func([]rune) (interface{}, error) + // parser is a simple parser that takes a strvals line and parses it into a // map representation. type parser struct { - sc *bytes.Buffer - data map[string]interface{} - st bool + sc *bytes.Buffer + data map[string]interface{} + reader RunesValueReader } func newParser(sc *bytes.Buffer, data map[string]interface{}, stringBool bool) *parser { - return &parser{sc: sc, data: data, st: stringBool} + stringConverter := func(rs []rune) (interface{}, error) { + return typedVal(rs, stringBool), nil + } + return &parser{sc: sc, data: data, reader: stringConverter} +} + +func newFileParser(sc *bytes.Buffer, data map[string]interface{}, reader RunesValueReader) *parser { + return &parser{sc: sc, data: data, reader: reader} } func (t *parser) parse() error { @@ -152,8 +186,12 @@ func (t *parser) key(data map[string]interface{}) error { set(data, string(k), "") return e case ErrNotList: - v, e := t.val() - set(data, string(k), typedVal(v, t.st)) + rs, e := t.val() + if e != nil && e != io.EOF { + return e + } + v, e := t.reader(rs) + set(data, string(k), v) return e default: return e @@ -225,8 +263,12 @@ func (t *parser) listItem(list []interface{}, i int) ([]interface{}, error) { case io.EOF: return setIndex(list, i, ""), err case ErrNotList: - v, e := t.val() - return setIndex(list, i, typedVal(v, t.st)), e + rs, e := t.val() + if e != nil && e != io.EOF { + return list, e + } + v, e := t.reader(rs) + return setIndex(list, i, v), e default: return list, e } @@ -274,7 +316,7 @@ func (t *parser) valList() ([]interface{}, error) { list := []interface{}{} stop := runeSet([]rune{',', '}'}) for { - switch v, last, err := runesUntil(t.sc, stop); { + switch rs, last, err := runesUntil(t.sc, stop); { case err != nil: if err == io.EOF { err = errors.New("list must terminate with '}'") @@ -285,10 +327,15 @@ func (t *parser) valList() ([]interface{}, error) { if r, _, e := t.sc.ReadRune(); e == nil && r != ',' { t.sc.UnreadRune() } - list = append(list, typedVal(v, t.st)) - return list, nil + v, e := t.reader(rs) + list = append(list, v) + return list, e case last == ',': - list = append(list, typedVal(v, t.st)) + v, e := t.reader(rs) + if e != nil { + return list, e + } + list = append(list, v) } } } diff --git a/pkg/strvals/parser_test.go b/pkg/strvals/parser_test.go index 0bb1562d5..ff8d58587 100644 --- a/pkg/strvals/parser_test.go +++ b/pkg/strvals/parser_test.go @@ -358,6 +358,39 @@ func TestParseInto(t *testing.T) { } } +func TestParseIntoFile(t *testing.T) { + got := map[string]interface{}{} + input := "name1=path1" + expect := map[string]interface{}{ + "name1": "value1", + } + rs2v := func(rs []rune) (interface{}, error) { + v := string(rs) + if v != "path1" { + t.Errorf("%s: RunesValueReader: Expected value path1, got %s", input, v) + return "", nil + } + return "value1", nil + } + + if err := ParseIntoFile(input, got, rs2v); err != nil { + t.Fatal(err) + } + + y1, err := yaml.Marshal(expect) + if err != nil { + t.Fatal(err) + } + y2, err := yaml.Marshal(got) + if err != nil { + t.Fatalf("Error serializing parsed value: %s", err) + } + + if string(y1) != string(y2) { + t.Errorf("%s: Expected:\n%s\nGot:\n%s", input, y1, y2) + } +} + func TestToYAML(t *testing.T) { // The TestParse does the hard part. We just verify that YAML formatting is // happening.