From 119996b50d2d9cb0b0d0b7c8975e888a1d1d5c83 Mon Sep 17 00:00:00 2001 From: Oscar Mauricio Forero Carrillo Date: Sat, 2 Nov 2019 12:10:09 +0800 Subject: [PATCH] feat: (6715) Add --env-values argument to helm command Signed-off-by: Oscar Mauricio Forero Carrillo --- cmd/helm/flags.go | 1 + cmd/helm/install.go | 2 +- cmd/helm/install_test.go | 6 ++++++ cmd/helm/upgrade.go | 2 +- pkg/chart/chart.go | 21 +++++++++++++++++++ pkg/chart/loader/archive.go | 19 +++++++++++++---- pkg/chart/loader/directory.go | 14 ++++++++++--- pkg/chart/loader/load.go | 36 +++++++++++++++++++++++++++------ pkg/chart/loader/load_test.go | 14 ++++++------- pkg/cli/values/options.go | 9 +++++---- pkg/lint/lint.go | 2 +- pkg/lint/rules/template.go | 4 ++-- pkg/lint/rules/template_test.go | 6 +++--- 13 files changed, 104 insertions(+), 32 deletions(-) diff --git a/cmd/helm/flags.go b/cmd/helm/flags.go index 467abbd6e..3180657fa 100644 --- a/cmd/helm/flags.go +++ b/cmd/helm/flags.go @@ -35,6 +35,7 @@ func addValueOptionsFlags(f *pflag.FlagSet, v *values.Options) { 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)") + f.StringVarP(&v.EnvValuesFile, "env-values", "e", "", "specify the name of a YAML file inside (the path should be relative to the root of the chart) the chart to override the default values, it won't fail if it does not exist") } func addChartPathOptionsFlags(f *pflag.FlagSet, c *action.ChartPathOptions) { diff --git a/cmd/helm/install.go b/cmd/helm/install.go index 40935db17..d804f6135 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -171,7 +171,7 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options } // Check chart dependencies to make sure all are present in /charts - chartRequested, err := loader.Load(cp) + chartRequested, err := loader.LoadWithEnvValues(cp, valueOpts.EnvValuesFile) if err != nil { return nil, err } diff --git a/cmd/helm/install_test.go b/cmd/helm/install_test.go index 9ab25417b..2af7ca675 100644 --- a/cmd/helm/install_test.go +++ b/cmd/helm/install_test.go @@ -59,6 +59,12 @@ func TestInstall(t *testing.T) { cmd: "install virgil testdata/testcharts/alpine -f testdata/testcharts/alpine/extra_values.yaml -f testdata/testcharts/alpine/more_values.yaml", golden: "output/install-with-multiple-values-files.txt", }, + // Install, values environment values yaml + { + name: "install with environment values", + cmd: "install virgil testdata/testcharts/alpine -e more_values.yaml", + golden: "output/install-with-multiple-values-files.txt", + }, // Install, no charts { name: "install with no chart specified", diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go index eadc3b63d..65c4bb7f3 100644 --- a/cmd/helm/upgrade.go +++ b/cmd/helm/upgrade.go @@ -117,7 +117,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { } // Check chart dependencies to make sure all are present in /charts - ch, err := loader.Load(chartPath) + ch, err := loader.LoadWithEnvValues(chartPath, valueOpts.EnvValuesFile) if err != nil { return err } diff --git a/pkg/chart/chart.go b/pkg/chart/chart.go index f36cf8236..42cd28ba8 100644 --- a/pkg/chart/chart.go +++ b/pkg/chart/chart.go @@ -127,3 +127,24 @@ func (ch *Chart) CRDs() []*File { } return files } + +// MergeValues takes two maps from string to anything and returns a new map with the combined key:value pairs +// the values in the second map argument will override those in the first +func MergeValues(a, b map[string]interface{}) map[string]interface{} { + out := make(map[string]interface{}, len(a)) + for k, v := range a { + out[k] = v + } + for k, v := range b { + if v, ok := v.(map[string]interface{}); ok { + if bv, ok := out[k]; ok { + if bv, ok := bv.(map[string]interface{}); ok { + out[k] = MergeValues(bv, v) + continue + } + } + } + out[k] = v + } + return out +} diff --git a/pkg/chart/loader/archive.go b/pkg/chart/loader/archive.go index 3c50fe379..8382eac71 100644 --- a/pkg/chart/loader/archive.go +++ b/pkg/chart/loader/archive.go @@ -39,12 +39,17 @@ var drivePathPattern = regexp.MustCompile(`^[a-zA-Z]:/`) type FileLoader string // Load loads a chart -func (l FileLoader) Load() (*chart.Chart, error) { - return LoadFile(string(l)) +func (l FileLoader) Load(envYaml string) (*chart.Chart, error) { + return LoadFileWithEnvValues(string(l), envYaml) } // LoadFile loads from an archive file. func LoadFile(name string) (*chart.Chart, error) { + return LoadFileWithEnvValues(name, "") +} + +// LoadFileWithEnvValues loads from an archive file. +func LoadFileWithEnvValues(name string, envYaml string) (*chart.Chart, error) { if fi, err := os.Stat(name); err != nil { return nil, err } else if fi.IsDir() { @@ -62,7 +67,7 @@ func LoadFile(name string) (*chart.Chart, error) { return nil, err } - c, err := LoadArchive(raw) + c, err := LoadArchiveWithEnvValues(raw, envYaml) if err != nil { if err == gzip.ErrHeader { return nil, fmt.Errorf("file '%s' does not appear to be a valid chart file (details: %s)", name, err) @@ -179,10 +184,16 @@ func LoadArchiveFiles(in io.Reader) ([]*BufferedFile, error) { // LoadArchive loads from a reader containing a compressed tar archive. func LoadArchive(in io.Reader) (*chart.Chart, error) { + return LoadArchiveWithEnvValues(in, "") +} + +// LoadArchiveWithEnvValues loads from a reader containing a compressed tar archive, +// overriding the default values with the values from the environment file in the chart. +func LoadArchiveWithEnvValues(in io.Reader, envYaml string) (*chart.Chart, error) { files, err := LoadArchiveFiles(in) if err != nil { return nil, err } - return LoadFiles(files) + return LoadFilesWithEnvValues(files, envYaml) } diff --git a/pkg/chart/loader/directory.go b/pkg/chart/loader/directory.go index a12c5158e..3d5463cbb 100644 --- a/pkg/chart/loader/directory.go +++ b/pkg/chart/loader/directory.go @@ -34,14 +34,22 @@ import ( type DirLoader string // Load loads the chart -func (l DirLoader) Load() (*chart.Chart, error) { - return LoadDir(string(l)) +func (l DirLoader) Load(envYaml string) (*chart.Chart, error) { + return LoadDirWithEnvValues(string(l), envYaml) } // LoadDir loads from a directory. // // This loads charts only from directories. func LoadDir(dir string) (*chart.Chart, error) { + return LoadDirWithEnvValues(dir, "") +} + +// LoadDirWithEnvValues loads from a directory, +// overriding the default values with the values from the environment file in the chart. +// +// This loads charts only from directories. +func LoadDirWithEnvValues(dir string, envYaml string) (*chart.Chart, error) { topdir, err := filepath.Abs(dir) if err != nil { return nil, err @@ -111,5 +119,5 @@ func LoadDir(dir string) (*chart.Chart, error) { return c, err } - return LoadFiles(files) + return LoadFilesWithEnvValues(files, envYaml) } diff --git a/pkg/chart/loader/load.go b/pkg/chart/loader/load.go index f04c0e9b3..7d9e60df8 100644 --- a/pkg/chart/loader/load.go +++ b/pkg/chart/loader/load.go @@ -31,7 +31,7 @@ import ( // ChartLoader loads a chart. type ChartLoader interface { - Load() (*chart.Chart, error) + Load(envYaml string) (*chart.Chart, error) } // Loader returns a new ChartLoader appropriate for the given chart name @@ -55,11 +55,22 @@ func Loader(name string) (ChartLoader, error) { // If a .helmignore file is present, the directory loader will skip loading any files // matching it. But .helmignore is not evaluated when reading out of an archive. func Load(name string) (*chart.Chart, error) { + return LoadWithEnvValues(name, "") +} + +// LoadWithEnvValues takes a string name, tries to resolve it to a file or directory, and then loads it. +// +// This is the preferred way to load a chart. It will discover the chart encoding +// and hand off to the appropriate chart reader. +// +// If a .helmignore file is present, the directory loader will skip loading any files +// matching it. But .helmignore is not evaluated when reading out of an archive. +func LoadWithEnvValues(name string, envYaml string) (*chart.Chart, error) { l, err := Loader(name) if err != nil { return nil, err } - return l.Load() + return l.Load(envYaml) } // BufferedFile represents an archive file buffered for later processing. @@ -70,8 +81,15 @@ type BufferedFile struct { // LoadFiles loads from in-memory files. func LoadFiles(files []*BufferedFile) (*chart.Chart, error) { + return LoadFilesWithEnvValues(files, "") +} + +// LoadFilesWithEnvValues loads from in-memory files. +func LoadFilesWithEnvValues(files []*BufferedFile, envYaml string) (*chart.Chart, error) { c := new(chart.Chart) subcharts := make(map[string][]*BufferedFile) + values := make(map[string]interface{}) + env := make(map[string]interface{}) for _, f := range files { switch { @@ -94,10 +112,13 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) { return c, errors.Wrap(err, "cannot load Chart.lock") } case f.Name == "values.yaml": - c.Values = make(map[string]interface{}) - if err := yaml.Unmarshal(f.Data, &c.Values); err != nil { + if err := yaml.Unmarshal(f.Data, &values); err != nil { return c, errors.Wrap(err, "cannot load values.yaml") } + case f.Name == envYaml: + if err := yaml.Unmarshal(f.Data, &env); err != nil { + return c, errors.Wrapf(err, "cannot load %s", envYaml) + } case f.Name == "values.schema.json": c.Schema = f.Data @@ -136,6 +157,9 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) { } } + if len(values) != 0 || len(env) != 0 { + c.Values = chart.MergeValues(values, env) + } if err := c.Validate(); err != nil { return c, err } @@ -152,7 +176,7 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) { return c, errors.Errorf("error unpacking tar in %s: expected %s, got %s", c.Name(), n, file.Name) } // Untar the chart and add to c.Dependencies - sc, err = LoadArchive(bytes.NewBuffer(file.Data)) + sc, err = LoadArchiveWithEnvValues(bytes.NewBuffer(file.Data), envYaml) default: // We have to trim the prefix off of every file, and ignore any file // that is in charts/, but isn't actually a chart. @@ -165,7 +189,7 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) { f.Name = parts[1] buff = append(buff, f) } - sc, err = LoadFiles(buff) + sc, err = LoadFilesWithEnvValues(buff, envYaml) } if err != nil { diff --git a/pkg/chart/loader/load_test.go b/pkg/chart/loader/load_test.go index 96b4dc608..044c406c1 100644 --- a/pkg/chart/loader/load_test.go +++ b/pkg/chart/loader/load_test.go @@ -36,7 +36,7 @@ func TestLoadDir(t *testing.T) { if err != nil { t.Fatalf("Failed to load testdata: %s", err) } - c, err := l.Load() + c, err := l.Load("") if err != nil { t.Fatalf("Failed to load testdata: %s", err) } @@ -55,7 +55,7 @@ func TestLoadDirWithDevNull(t *testing.T) { if err != nil { t.Fatalf("Failed to load testdata: %s", err) } - if _, err := l.Load(); err == nil { + if _, err := l.Load(""); err == nil { t.Errorf("packages with an irregular file (/dev/null) should not load") } } @@ -75,7 +75,7 @@ func TestLoadDirWithSymlink(t *testing.T) { t.Fatalf("Failed to load testdata: %s", err) } - c, err := l.Load() + c, err := l.Load("") if err != nil { t.Fatalf("Failed to load testdata: %s", err) } @@ -90,7 +90,7 @@ func TestLoadV1(t *testing.T) { if err != nil { t.Fatalf("Failed to load testdata: %s", err) } - c, err := l.Load() + c, err := l.Load("") if err != nil { t.Fatalf("Failed to load testdata: %s", err) } @@ -103,7 +103,7 @@ func TestLoadFileV1(t *testing.T) { if err != nil { t.Fatalf("Failed to load testdata: %s", err) } - c, err := l.Load() + c, err := l.Load("") if err != nil { t.Fatalf("Failed to load testdata: %s", err) } @@ -116,7 +116,7 @@ func TestLoadFile(t *testing.T) { if err != nil { t.Fatalf("Failed to load testdata: %s", err) } - c, err := l.Load() + c, err := l.Load("") if err != nil { t.Fatalf("Failed to load testdata: %s", err) } @@ -212,7 +212,7 @@ func TestLoadV2WithReqs(t *testing.T) { if err != nil { t.Fatalf("Failed to load testdata: %s", err) } - c, err := l.Load() + c, err := l.Load("") if err != nil { t.Fatalf("Failed to load testdata: %s", err) } diff --git a/pkg/cli/values/options.go b/pkg/cli/values/options.go index e6ad71767..42c037812 100644 --- a/pkg/cli/values/options.go +++ b/pkg/cli/values/options.go @@ -30,10 +30,11 @@ import ( ) type Options struct { - ValueFiles []string - StringValues []string - Values []string - FileValues []string + ValueFiles []string + StringValues []string + Values []string + FileValues []string + EnvValuesFile string } // MergeValues merges values from files specified via -f/--values and directly diff --git a/pkg/lint/lint.go b/pkg/lint/lint.go index d47951671..04a44ba38 100644 --- a/pkg/lint/lint.go +++ b/pkg/lint/lint.go @@ -31,6 +31,6 @@ func All(basedir string, values map[string]interface{}, namespace string, strict linter := support.Linter{ChartDir: chartDir} rules.Chartfile(&linter) rules.Values(&linter) - rules.Templates(&linter, values, namespace, strict) + rules.Templates(&linter, values, namespace, strict, "") return linter } diff --git a/pkg/lint/rules/template.go b/pkg/lint/rules/template.go index 5c6cd7336..7bab12b0c 100644 --- a/pkg/lint/rules/template.go +++ b/pkg/lint/rules/template.go @@ -36,7 +36,7 @@ var ( ) // Templates lints the templates in the Linter. -func Templates(linter *support.Linter, values map[string]interface{}, namespace string, strict bool) { +func Templates(linter *support.Linter, values map[string]interface{}, namespace string, strict bool, envYaml string) { path := "templates/" templatesPath := filepath.Join(linter.ChartDir, path) @@ -48,7 +48,7 @@ func Templates(linter *support.Linter, values map[string]interface{}, namespace } // Load chart and parse templates, based on tiller/release_server - chart, err := loader.Load(linter.ChartDir) + chart, err := loader.LoadWithEnvValues(linter.ChartDir, envYaml) chartLoaded := linter.RunLinterRule(support.ErrorSev, path, err) diff --git a/pkg/lint/rules/template_test.go b/pkg/lint/rules/template_test.go index ddb46aba0..e405e38a2 100644 --- a/pkg/lint/rules/template_test.go +++ b/pkg/lint/rules/template_test.go @@ -51,7 +51,7 @@ const strict = false func TestTemplateParsing(t *testing.T) { linter := support.Linter{ChartDir: templateTestBasedir} - Templates(&linter, values, namespace, strict) + Templates(&linter, values, namespace, strict, "") res := linter.Messages if len(res) != 1 { @@ -74,7 +74,7 @@ func TestTemplateIntegrationHappyPath(t *testing.T) { defer os.Rename(ignoredTemplatePath, wrongTemplatePath) linter := support.Linter{ChartDir: templateTestBasedir} - Templates(&linter, values, namespace, strict) + Templates(&linter, values, namespace, strict, "") res := linter.Messages if len(res) != 0 { @@ -84,7 +84,7 @@ func TestTemplateIntegrationHappyPath(t *testing.T) { func TestV3Fail(t *testing.T) { linter := support.Linter{ChartDir: "./testdata/v3-fail"} - Templates(&linter, values, namespace, strict) + Templates(&linter, values, namespace, strict, "") res := linter.Messages if len(res) != 3 {