From f0b18ccba54f15e42ac55f2c3741dadaba560310 Mon Sep 17 00:00:00 2001 From: John Bonham Date: Fri, 1 Sep 2017 14:39:16 -0700 Subject: [PATCH 1/2] feat(helm): Merges helm-template plugin into cli. Sets up the template command using cobra, and adds tests to ensure the command is working properly. Closes #1887 --- cmd/helm/helm.go | 1 + cmd/helm/template.go | 199 ++++++++++++++++++ cmd/helm/template_test.go | 130 ++++++++++++ .../testcharts/templatetest/Chart.yaml | 3 + .../testcharts/templatetest/other_values.yaml | 1 + .../templatetest/templates/NOTES.txt | 1 + .../templatetest/templates/deployment.yaml | 4 + .../templatetest/templates/service.yaml | 4 + .../testcharts/templatetest/values.yaml | 1 + 9 files changed, 344 insertions(+) create mode 100644 cmd/helm/template.go create mode 100644 cmd/helm/template_test.go create mode 100644 cmd/helm/testdata/testcharts/templatetest/Chart.yaml create mode 100644 cmd/helm/testdata/testcharts/templatetest/other_values.yaml create mode 100644 cmd/helm/testdata/testcharts/templatetest/templates/NOTES.txt create mode 100644 cmd/helm/testdata/testcharts/templatetest/templates/deployment.yaml create mode 100644 cmd/helm/testdata/testcharts/templatetest/templates/service.yaml create mode 100644 cmd/helm/testdata/testcharts/templatetest/values.yaml diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index bc8885b4b..979b208c5 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -106,6 +106,7 @@ func newRootCmd(args []string) *cobra.Command { newSearchCmd(out), newServeCmd(out), newVerifyCmd(out), + newTemplateCmd(out), // release commands addFlagsTLS(newDeleteCmd(nil, out)), diff --git a/cmd/helm/template.go b/cmd/helm/template.go new file mode 100644 index 000000000..5b751a6a1 --- /dev/null +++ b/cmd/helm/template.go @@ -0,0 +1,199 @@ +/* +Copyright 2017 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "errors" + "fmt" + "io" + "io/ioutil" + "path/filepath" + "sort" + "strings" + + "github.com/ghodss/yaml" + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/chartutil" + "k8s.io/helm/pkg/engine" + "k8s.io/helm/pkg/proto/hapi/chart" + "k8s.io/helm/pkg/strvals" + "k8s.io/helm/pkg/timeconv" +) + +const templateDesc = ` +Render chart templates locally and display the output. + +This does not require Tiller. However, any values that would normally be +looked up or retrieved in-cluster will be faked locally. Additionally, none +of the server-side testing of chart validity (e.g. whether an API is supported) +is done. + +To render just one template in a chart, use '-x': + $ helm template mychart -x mychart/templates/deployment.yaml +` + +type templateCmd struct { + setVals []string + valsFiles valueFiles + flagVerbose bool + showNotes bool + releaseName string + namespace string + renderFiles []string + + out io.Writer +} + +func newTemplateCmd(out io.Writer) *cobra.Command { + tem := &templateCmd{ + out: out, + } + + cmd := &cobra.Command{ + Use: "template [flags] CHART", + Short: "locally render templates", + Long: templateDesc, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return errors.New("chart is required") + } + return tem.run(args) + }, + } + + f := cmd.Flags() + f.StringArrayVar(&tem.setVals, "set", []string{}, "set values on the command line. See 'helm install -h'") + f.VarP(&tem.valsFiles, "values", "f", "specify one or more YAML files of values") + f.BoolVarP(&tem.flagVerbose, "verbose", "v", false, "show the computed YAML values as well.") + f.BoolVar(&tem.showNotes, "notes", false, "show the computed NOTES.txt file as well.") + f.StringVarP(&tem.releaseName, "release", "r", "RELEASE-NAME", "release name") + f.StringVarP(&tem.namespace, "namespace", "n", "NAMESPACE", "namespace") + f.StringArrayVarP(&tem.renderFiles, "execute", "x", []string{}, "only execute the given templates.") + + return cmd +} + +func (tc *templateCmd) run(args []string) error { + c, err := chartutil.Load(args[0]) + if err != nil { + return err + } + + vv, err := tc.vals() + if err != nil { + return err + } + + config := &chart.Config{Raw: string(vv), Values: map[string]*chart.Value{}} + + if tc.flagVerbose { + fmt.Fprintf(tc.out, "---\n# merged values") + fmt.Fprintf(tc.out, "%s\n", string(vv)) + + } + + options := chartutil.ReleaseOptions{ + Name: tc.releaseName, + Time: timeconv.Now(), + Namespace: tc.namespace, + //Revision: 1, + //IsInstall: true, + } + + // Set up engine. + renderer := engine.New() + + vals, err := chartutil.ToRenderValues(c, config, options) + if err != nil { + return err + } + + out, err := renderer.Render(c, vals) + if err != nil { + return err + } + + in := func(needle string, haystack []string) bool { + for _, h := range haystack { + if h == needle { + return true + } + } + return false + } + + sortedKeys := make([]string, 0, len(out)) + for key := range out { + sortedKeys = append(sortedKeys, key) + } + sort.Strings(sortedKeys) + + // If renderFiles is set, we ONLY print those. + if len(tc.renderFiles) > 0 { + for _, name := range sortedKeys { + data := out[name] + if in(name, tc.renderFiles) { + fmt.Fprintf(tc.out, "---\n# Source: %s\n", name) + fmt.Fprintf(tc.out, "%s\n", data) + } + } + return nil + } + + for _, name := range sortedKeys { + data := out[name] + b := filepath.Base(name) + if !tc.showNotes && b == "NOTES.txt" { + continue + } + if strings.HasPrefix(b, "_") { + continue + } + fmt.Fprintf(tc.out, "---\n# Source: %s\n", name) + fmt.Fprintf(tc.out, "%s\n", data) + } + return nil +} + +func (tc *templateCmd) vals() ([]byte, error) { + base := map[string]interface{}{} + + // User specified a values files via -f/--values + for _, filePath := range tc.valsFiles { + currentMap := map[string]interface{}{} + bytes, err := ioutil.ReadFile(filePath) + if err != nil { + return []byte{}, err + } + + if err := yaml.Unmarshal(bytes, ¤tMap); err != nil { + return []byte{}, fmt.Errorf("failed to parse %s: %s", filePath, err) + } + // Merge with the previous map + base = mergeValues(base, currentMap) + } + + // User specified a value via --set + for _, value := range tc.setVals { + if err := strvals.ParseInto(value, base); err != nil { + return []byte{}, fmt.Errorf("failed parsing --set data: %s", err) + } + } + + return yaml.Marshal(base) +} diff --git a/cmd/helm/template_test.go b/cmd/helm/template_test.go new file mode 100644 index 000000000..a06b8c546 --- /dev/null +++ b/cmd/helm/template_test.go @@ -0,0 +1,130 @@ +/* +Copyright 2017 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "bytes" + "regexp" + "testing" +) + +type templateCase struct { + name string + args []string + flags []string + // expected and notExpected are strings to be matched. This supports regular expressions. + expected []string + notExpected []string + err bool +} + +func TestTemplate(t *testing.T) { + testCases := []templateCase{ + { + name: "template basic", + args: []string{"testdata/testcharts/templatetest"}, + expected: []string{ + "name: defaultname", + "Source: templatetest/templates/deployment.yaml", + "Source: templatetest/templates/service.yaml", + }, + notExpected: []string{ + "1. These are the notes", + "merged values", + }, + }, + { + name: "template missing chart", + args: []string{""}, + err: true, + }, + { + name: "template with valid value file", + args: []string{"testdata/testcharts/templatetest"}, + flags: []string{"--values", "testdata/testcharts/templatetest/other_values.yaml"}, + expected: []string{"name: othername"}, + }, + { + name: "template with invalid value file", + args: []string{"testdata/testcharts/templatetest"}, + flags: []string{"--values", ""}, + err: true, + }, + { + name: "template set existing key", + args: []string{"testdata/testcharts/templatetest"}, + flags: []string{"--set", "name=customname"}, + expected: []string{"name: customname"}, + }, + { + name: "template set non-existing key", + args: []string{"testdata/testcharts/templatetest"}, + flags: []string{"--set", "invalid=customvalue"}, + notExpected: []string{"customvalue"}, + }, + { + name: "template include notes", + args: []string{"testdata/testcharts/templatetest"}, + flags: []string{"--notes"}, + expected: []string{"1. These are the notes"}, + }, + { + name: "template verbose output", + args: []string{"testdata/testcharts/templatetest"}, + flags: []string{"--verbose"}, + expected: []string{"merged values"}, + }, + { + name: "template render specific existing file", + args: []string{"testdata/testcharts/templatetest"}, + flags: []string{"--execute", "templatetest/templates/deployment.yaml"}, + expected: []string{"Source: templatetest/templates/deployment.yaml"}, + notExpected: []string{"Source: templatetest/templates/service.yaml"}, + }, + { + name: "template render specific non-existing file", + args: []string{"testdata/testcharts/templatetest"}, + flags: []string{"--execute", "templatetest/templates/ingress.yaml"}, + notExpected: []string{"Source: templatetest/templates/ingress.yaml"}, + }, + } + + var buf bytes.Buffer + for _, tc := range testCases { + cmd := newTemplateCmd(&buf) + cmd.ParseFlags(tc.flags) + err := cmd.RunE(cmd, tc.args) + if (err != nil) != tc.err { + t.Errorf("%q. expected error, got '%v'", tc.name, err) + } + + by := buf.Bytes() + for _, exp := range tc.expected { + re := regexp.MustCompile(exp) + if !re.Match(by) { + t.Errorf("%q. expected\n%q\ngot\n%q", tc.name, exp, buf.String()) + } + } + for _, exp := range tc.notExpected { + re := regexp.MustCompile(exp) + if re.Match(by) { + t.Errorf("%q. not expected\n%q\nin\n%q", tc.name, exp, buf.String()) + } + } + buf.Reset() + } +} diff --git a/cmd/helm/testdata/testcharts/templatetest/Chart.yaml b/cmd/helm/testdata/testcharts/templatetest/Chart.yaml new file mode 100644 index 000000000..35b0467da --- /dev/null +++ b/cmd/helm/testdata/testcharts/templatetest/Chart.yaml @@ -0,0 +1,3 @@ +description: A Helm chart for Kubernetes +name: templatetest +version: 0.1.0 diff --git a/cmd/helm/testdata/testcharts/templatetest/other_values.yaml b/cmd/helm/testdata/testcharts/templatetest/other_values.yaml new file mode 100644 index 000000000..1c5163f2c --- /dev/null +++ b/cmd/helm/testdata/testcharts/templatetest/other_values.yaml @@ -0,0 +1 @@ +name: othername diff --git a/cmd/helm/testdata/testcharts/templatetest/templates/NOTES.txt b/cmd/helm/testdata/testcharts/templatetest/templates/NOTES.txt new file mode 100644 index 000000000..b65d7fee0 --- /dev/null +++ b/cmd/helm/testdata/testcharts/templatetest/templates/NOTES.txt @@ -0,0 +1 @@ +1. These are the notes diff --git a/cmd/helm/testdata/testcharts/templatetest/templates/deployment.yaml b/cmd/helm/testdata/testcharts/templatetest/templates/deployment.yaml new file mode 100644 index 000000000..fa5ef7367 --- /dev/null +++ b/cmd/helm/testdata/testcharts/templatetest/templates/deployment.yaml @@ -0,0 +1,4 @@ +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: {{ .Values.name }} diff --git a/cmd/helm/testdata/testcharts/templatetest/templates/service.yaml b/cmd/helm/testdata/testcharts/templatetest/templates/service.yaml new file mode 100644 index 000000000..4aba456dd --- /dev/null +++ b/cmd/helm/testdata/testcharts/templatetest/templates/service.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Values.name }} diff --git a/cmd/helm/testdata/testcharts/templatetest/values.yaml b/cmd/helm/testdata/testcharts/templatetest/values.yaml new file mode 100644 index 000000000..a635d3b69 --- /dev/null +++ b/cmd/helm/testdata/testcharts/templatetest/values.yaml @@ -0,0 +1 @@ +name: defaultname From 780188d20f90d5335066563c9de2b65387fd8963 Mon Sep 17 00:00:00 2001 From: John Bonham Date: Fri, 1 Sep 2017 16:25:33 -0700 Subject: [PATCH 2/2] Updates helm docs --- docs/helm/helm.md | 3 ++- docs/helm/helm_template.md | 49 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 docs/helm/helm_template.md diff --git a/docs/helm/helm.md b/docs/helm/helm.md index 2b15a3b12..0b70607ed 100644 --- a/docs/helm/helm.md +++ b/docs/helm/helm.md @@ -61,9 +61,10 @@ Environment: * [helm search](helm_search.md) - search for a keyword in charts * [helm serve](helm_serve.md) - start a local http web server * [helm status](helm_status.md) - displays the status of the named release +* [helm template](helm_template.md) - locally render templates * [helm test](helm_test.md) - test a release * [helm upgrade](helm_upgrade.md) - upgrade a release * [helm verify](helm_verify.md) - verify that a chart at the given path has been signed and is valid * [helm version](helm_version.md) - print the client/server version information -###### Auto generated by spf13/cobra on 23-Jun-2017 +###### Auto generated by spf13/cobra on 1-Sep-2017 diff --git a/docs/helm/helm_template.md b/docs/helm/helm_template.md new file mode 100644 index 000000000..0d719360a --- /dev/null +++ b/docs/helm/helm_template.md @@ -0,0 +1,49 @@ +## helm template + +locally render templates + +### Synopsis + + + +Render chart templates locally and display the output. + +This does not require Tiller. However, any values that would normally be +looked up or retrieved in-cluster will be faked locally. Additionally, none +of the server-side testing of chart validity (e.g. whether an API is supported) +is done. + +To render just one template in a chart, use '-x': + $ helm template mychart -x mychart/templates/deployment.yaml + + +``` +helm template [flags] CHART +``` + +### Options + +``` + -x, --execute stringArray only execute the given templates. + -n, --namespace string namespace (default "NAMESPACE") + --notes show the computed NOTES.txt file as well. + -r, --release string release name (default "RELEASE-NAME") + --set stringArray set values on the command line. See 'helm install -h' + -f, --values valueFiles specify one or more YAML files of values (default []) + -v, --verbose show the computed YAML values as well. +``` + +### Options inherited from parent commands + +``` + --debug enable verbose output + --home string location of your Helm config. Overrides $HELM_HOME (default "$HOME/.helm") + --host string address of Tiller. Overrides $HELM_HOST + --kube-context string name of the kubeconfig context to use + --tiller-namespace string namespace of Tiller (default "kube-system") +``` + +### SEE ALSO +* [helm](helm.md) - The Helm package manager for Kubernetes. + +###### Auto generated by spf13/cobra on 1-Sep-2017