From 8563f1589fdb08e03a97867e36d1be2044bb45ed Mon Sep 17 00:00:00 2001 From: Oscar Forero Date: Mon, 21 Oct 2019 17:40:17 +0800 Subject: [PATCH] feat: (6715) Add --environment argument to helm template Signed-off-by: Oscar Forero Signed-off-by: Oscar Mauricio Forero Carrillo --- .gitignore | 1 + cmd/helm/inspect_test.go | 2 +- cmd/helm/install.go | 29 +---------------- cmd/helm/install_test.go | 19 +++++------ cmd/helm/lint.go | 2 +- cmd/helm/template.go | 6 +++- pkg/chartutil/load.go | 64 +++++++++++++++++++++++++++++++++----- pkg/chartutil/load_test.go | 12 +++++-- pkg/chartutil/values.go | 27 ++++++++++++++++ 9 files changed, 113 insertions(+), 49 deletions(-) diff --git a/.gitignore b/.gitignore index d1ec13265..c2239042e 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ vendor/ .classpath .project .settings/** +cmd/helm/debug.test diff --git a/cmd/helm/inspect_test.go b/cmd/helm/inspect_test.go index c4ce005b0..cee092c7f 100644 --- a/cmd/helm/inspect_test.go +++ b/cmd/helm/inspect_test.go @@ -56,7 +56,7 @@ func TestInspect(t *testing.T) { expect := []string{ strings.Replace(strings.TrimSpace(string(cdata)), "\r", "", -1), - strings.Replace(strings.TrimSpace(string(data)), "\r", "", -1), + strings.Replace(strings.Split(string(data), "\n")[1], "\r", "", -1), strings.Replace(strings.TrimSpace(string(readmeData)), "\r", "", -1), } diff --git a/cmd/helm/install.go b/cmd/helm/install.go index 117c7ba5b..1cbbeaa9f 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -360,33 +360,6 @@ func (i *installCmd) run() error { return write(i.out, &statusWriter{status}, outputFormat(i.output)) } -// Merges source and destination map, preferring values from the source map -func mergeValues(dest map[string]interface{}, src map[string]interface{}) map[string]interface{} { - for k, v := range src { - // If the key doesn't exist already, then just set the key to that value - if _, exists := dest[k]; !exists { - dest[k] = v - continue - } - nextMap, ok := v.(map[string]interface{}) - // If it isn't another map, overwrite the value - if !ok { - dest[k] = v - continue - } - // Edge case: If the key exists in the destination, but isn't a map - destMap, isMap := dest[k].(map[string]interface{}) - // If the source map has a map for this key, prefer it - if !isMap { - dest[k] = v - continue - } - // If we got to this point, it is a map in both, so merge them - dest[k] = mergeValues(destMap, nextMap) - } - return dest -} - // vals merges values from files specified via -f/--values and // directly via --set or --set-string or --set-file, marshaling them to YAML func vals(valueFiles valueFiles, values []string, stringValues []string, fileValues []string, CertFile, KeyFile, CAFile string) ([]byte, error) { @@ -412,7 +385,7 @@ func vals(valueFiles valueFiles, values []string, stringValues []string, fileVal return []byte{}, fmt.Errorf("failed to parse %s: %s", filePath, err) } // Merge with the previous map - base = mergeValues(base, currentMap) + base = chartutil.MergeValues(base, currentMap) } // User specified a value via --set diff --git a/cmd/helm/install_test.go b/cmd/helm/install_test.go index e00c33a81..85801f34f 100644 --- a/cmd/helm/install_test.go +++ b/cmd/helm/install_test.go @@ -24,6 +24,7 @@ import ( "testing" "github.com/spf13/cobra" + "k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/helm" ) @@ -290,47 +291,47 @@ func TestNameTemplate(t *testing.T) { } func TestMergeValues(t *testing.T) { - nestedMap := map[string]interface{}{ + nestedMap := chartutil.Values{ "foo": "bar", "baz": map[string]string{ "cool": "stuff", }, } - anotherNestedMap := map[string]interface{}{ + anotherNestedMap := chartutil.Values{ "foo": "bar", "baz": map[string]string{ "cool": "things", "awesome": "stuff", }, } - flatMap := map[string]interface{}{ + flatMap := chartutil.Values{ "foo": "bar", "baz": "stuff", } - anotherFlatMap := map[string]interface{}{ + anotherFlatMap := chartutil.Values{ "testing": "fun", } - testMap := mergeValues(flatMap, nestedMap) + testMap := chartutil.MergeValues(flatMap, nestedMap) equal := reflect.DeepEqual(testMap, nestedMap) if !equal { t.Errorf("Expected a nested map to overwrite a flat value. Expected: %v, got %v", nestedMap, testMap) } - testMap = mergeValues(nestedMap, flatMap) + testMap = chartutil.MergeValues(nestedMap, flatMap) equal = reflect.DeepEqual(testMap, flatMap) if !equal { t.Errorf("Expected a flat value to overwrite a map. Expected: %v, got %v", flatMap, testMap) } - testMap = mergeValues(nestedMap, anotherNestedMap) + testMap = chartutil.MergeValues(nestedMap, anotherNestedMap) equal = reflect.DeepEqual(testMap, anotherNestedMap) if !equal { t.Errorf("Expected a nested map to overwrite another nested map. Expected: %v, got %v", anotherNestedMap, testMap) } - testMap = mergeValues(anotherFlatMap, anotherNestedMap) - expectedMap := map[string]interface{}{ + testMap = chartutil.MergeValues(anotherFlatMap, anotherNestedMap) + expectedMap := chartutil.Values{ "testing": "fun", "foo": "bar", "baz": map[string]string{ diff --git a/cmd/helm/lint.go b/cmd/helm/lint.go index 342cc22ea..164ebf467 100644 --- a/cmd/helm/lint.go +++ b/cmd/helm/lint.go @@ -198,7 +198,7 @@ func (l *lintCmd) vals() ([]byte, error) { return []byte{}, fmt.Errorf("failed to parse %s: %s", filePath, err) } // Merge with the previous map - base = mergeValues(base, currentMap) + base = chartutil.MergeValues(base, currentMap) } // User specified a value via --set diff --git a/cmd/helm/template.go b/cmd/helm/template.go index 24fe93b64..787f91814 100644 --- a/cmd/helm/template.go +++ b/cmd/helm/template.go @@ -69,6 +69,7 @@ type templateCmd struct { values []string stringValues []string fileValues []string + envValuesFile string nameTemplate string showNotes bool releaseName string @@ -103,6 +104,7 @@ func newTemplateCmd(out io.Writer) *cobra.Command { f.StringArrayVar(&t.values, "set", []string{}, "Set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") f.StringArrayVar(&t.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(&t.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.StringVar(&t.envValuesFile, "environment", "", "Use an environment values file inside the chart and the subcharts") f.StringVar(&t.nameTemplate, "name-template", "", "Specify template used to name the release") f.StringVar(&t.kubeVersion, "kube-version", defaultKubeVersion, "Kubernetes version used as Capabilities.KubeVersion.Major/Minor") f.StringArrayVarP(&t.apiVersions, "api-versions", "a", []string{}, "Kubernetes api versions used for Capabilities.APIVersions") @@ -112,6 +114,7 @@ func newTemplateCmd(out io.Writer) *cobra.Command { } func (t *templateCmd) run(cmd *cobra.Command, args []string) error { + println("Running template cmd") if len(args) < 1 { return errors.New("chart is required") } @@ -135,6 +138,7 @@ func (t *templateCmd) run(cmd *cobra.Command, args []string) error { if t.namespace == "" { t.namespace = defaultNamespace() } + // get combined values and create config rawVals, err := vals(t.valueFiles, t.values, t.stringValues, t.fileValues, "", "", "") if err != nil { @@ -155,7 +159,7 @@ func (t *templateCmd) run(cmd *cobra.Command, args []string) error { } // Check chart requirements to make sure all dependencies are present in /charts - c, err := chartutil.Load(t.chartPath) + c, err := chartutil.LoadWithEnvValuesFile(t.chartPath, t.envValuesFile) if err != nil { return prettyError(err) } diff --git a/pkg/chartutil/load.go b/pkg/chartutil/load.go index 0cbc956d1..2e348656e 100644 --- a/pkg/chartutil/load.go +++ b/pkg/chartutil/load.go @@ -31,6 +31,9 @@ import ( "regexp" "strings" + "runtime/debug" + + "github.com/ghodss/yaml" "github.com/golang/protobuf/ptypes/any" "k8s.io/helm/pkg/ignore" @@ -46,6 +49,17 @@ import ( // 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 LoadWithEnvValuesFile(name, "") +} + +// LoadWithEnvValuesFile takes a string name and a file 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 LoadWithEnvValuesFile(name string, envValuesFile string) (*chart.Chart, error) { name = filepath.FromSlash(name) fi, err := os.Stat(name) if err != nil { @@ -55,9 +69,9 @@ func Load(name string) (*chart.Chart, error) { if validChart, err := IsChartDir(name); !validChart { return nil, err } - return LoadDir(name) + return LoadDirWithEnvValuesFiles(name, envValuesFile) } - return LoadFile(name) + return LoadFileWithEnvValuesFile(name, envValuesFile) } // BufferedFile represents an archive file buffered for later processing. @@ -153,17 +167,30 @@ 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 LoadArchiveWithEnvValuesFile(in, "") +} + +// LoadArchiveWithEnvValuesFile loads from a reader containing a compressed tar archive. +func LoadArchiveWithEnvValuesFile(in io.Reader, envValuesFile string) (*chart.Chart, error) { files, err := loadArchiveFiles(in) if err != nil { return nil, err } - return LoadFiles(files) + return LoadFilesWithEnvValues(files, envValuesFile) } // LoadFiles loads from in-memory files. func LoadFiles(files []*BufferedFile) (*chart.Chart, error) { + debug.PrintStack() + return LoadFilesWithEnvValues(files, "None") +} + +// LoadFilesWithEnvValues loads from in-memory files and loads an Environment File +func LoadFilesWithEnvValues(files []*BufferedFile, envValuesFile string) (*chart.Chart, error) { c := &chart.Chart{} subcharts := map[string][]*BufferedFile{} + values := Values{} + environment := Values{} for _, f := range files { if f.Name == "Chart.yaml" { @@ -179,7 +206,9 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) { } else if f.Name == "values.toml" { return c, errors.New("values.toml is illegal as of 2.0.0-alpha.2") } else if f.Name == "values.yaml" { - c.Values = &chart.Config{Raw: string(f.Data)} + yaml.Unmarshal(f.Data, &values) + } else if f.Name == envValuesFile { + yaml.Unmarshal(f.Data, &environment) } else if strings.HasPrefix(f.Name, "templates/") { c.Templates = append(c.Templates, &chart.Template{Name: f.Name, Data: f.Data}) } else if strings.HasPrefix(f.Name, "charts/") { @@ -199,6 +228,15 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) { c.Files = append(c.Files, &any.Any{TypeUrl: f.Name, Value: f.Data}) } } + MergeValues(values, environment) + valuesYml, err := values.YAML() + if err == nil { + if len(values) != 0 { + c.Values = &chart.Config{Raw: strings.TrimSpace(valuesYml)} + } + } else { + return c, fmt.Errorf("Unable to marshall values back to yaml") + } // Ensure that we got a Chart.yaml file if c.Metadata == nil { @@ -233,7 +271,7 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) { f.Name = parts[1] buff = append(buff, f) } - sc, err = LoadFiles(buff) + sc, err = LoadFilesWithEnvValues(buff, envValuesFile) } if err != nil { @@ -248,6 +286,11 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) { // LoadFile loads from an archive file. func LoadFile(name string) (*chart.Chart, error) { + return LoadFileWithEnvValuesFile(name, "") +} + +// LoadFileWithEnvValuesFile loads from an archive file. +func LoadFileWithEnvValuesFile(name string, envValuesFile string) (*chart.Chart, error) { if fi, err := os.Stat(name); err != nil { return nil, err } else if fi.IsDir() { @@ -265,7 +308,7 @@ func LoadFile(name string) (*chart.Chart, error) { return nil, err } - c, err := LoadArchive(raw) + c, err := LoadArchiveWithEnvValuesFile(raw, envValuesFile) 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) @@ -305,6 +348,13 @@ func ensureArchive(name string, raw *os.File) error { // // This loads charts only from directories. func LoadDir(dir string) (*chart.Chart, error) { + return LoadDirWithEnvValuesFiles(dir, "") +} + +// LoadDirWithEnvValuesFiles loads from a directory. +// +// This loads charts only from directories. +func LoadDirWithEnvValuesFiles(dir string, envValueFiles string) (*chart.Chart, error) { topdir, err := filepath.Abs(dir) if err != nil { return nil, err @@ -367,5 +417,5 @@ func LoadDir(dir string) (*chart.Chart, error) { return c, err } - return LoadFiles(files) + return LoadFilesWithEnvValues(files, envValueFiles) } diff --git a/pkg/chartutil/load_test.go b/pkg/chartutil/load_test.go index 8ef45e01f..034a4ed88 100644 --- a/pkg/chartutil/load_test.go +++ b/pkg/chartutil/load_test.go @@ -23,10 +23,12 @@ import ( "os" "path" "path/filepath" + "reflect" "strings" "testing" "time" + "github.com/ghodss/yaml" "k8s.io/helm/pkg/proto/hapi/chart" ) @@ -199,8 +201,14 @@ icon: https://example.com/64x64.png t.Errorf("Expected chart name to be 'frobnitz', got %s", c.Metadata.Name) } - if c.Values.Raw != defaultValues { - t.Error("Expected chart values to be populated with default values") + values := Values{} + yaml.Unmarshal([]byte(c.Values.Raw), &values) + expectedValues := Values{} + yaml.Unmarshal([]byte(c.Values.Raw), &expectedValues) + equal := reflect.DeepEqual(values, expectedValues) + if !equal { + t.Errorf("Expected chart values to be populated with default values. Expected: %v, got %v", values, expectedValues) + // t.Error("Expected chart values to be populated with default values") } if len(c.Templates) != 2 { diff --git a/pkg/chartutil/values.go b/pkg/chartutil/values.go index f82b95950..bab08daf6 100644 --- a/pkg/chartutil/values.go +++ b/pkg/chartutil/values.go @@ -42,6 +42,33 @@ const GlobalKey = "global" // Values represents a collection of chart values. type Values map[string]interface{} +// MergeValues merges source and destination map, preferring values from the source map +func MergeValues(dest Values, src Values) Values { + for k, v := range src { + // If the key doesn't exist already, then just set the key to that value + if _, exists := dest[k]; !exists { + dest[k] = v + continue + } + nextMap, ok := v.(Values) + // If it isn't another map, overwrite the value + if !ok { + dest[k] = v + continue + } + // Edge case: If the key exists in the destination, but isn't a map + destMap, isMap := dest[k].(Values) + // If the source map has a map for this key, prefer it + if !isMap { + dest[k] = v + continue + } + // If we got to this point, it is a map in both, so merge them + dest[k] = MergeValues(destMap, nextMap) + } + return dest +} + // YAML encodes the Values into a YAML string. func (v Values) YAML() (string, error) { b, err := yaml.Marshal(v)