mirror of https://github.com/helm/helm
Merge pull request #2781 from jascott1/i2755-template
feat(helm): add `template` commandpull/2897/head
commit
bb5db61325
@ -0,0 +1,236 @@
|
||||
/*
|
||||
Copyright 2016 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"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/helm/pkg/chartutil"
|
||||
"k8s.io/helm/pkg/engine"
|
||||
"k8s.io/helm/pkg/helm"
|
||||
"k8s.io/helm/pkg/proto/hapi/chart"
|
||||
"k8s.io/helm/pkg/proto/hapi/release"
|
||||
util "k8s.io/helm/pkg/releaseutil"
|
||||
"k8s.io/helm/pkg/tiller"
|
||||
"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 templates/deployment.yaml
|
||||
`
|
||||
|
||||
type templateCmd struct {
|
||||
namespace string
|
||||
valueFiles valueFiles
|
||||
chartPath string
|
||||
out io.Writer
|
||||
client helm.Interface
|
||||
values []string
|
||||
nameTemplate string
|
||||
showNotes bool
|
||||
releaseName string
|
||||
renderFiles []string
|
||||
}
|
||||
|
||||
func newTemplateCmd(out io.Writer) *cobra.Command {
|
||||
|
||||
t := &templateCmd{
|
||||
out: out,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "template [flags] CHART",
|
||||
Short: fmt.Sprintf("locally render templates"),
|
||||
Long: templateDesc,
|
||||
RunE: t.run,
|
||||
}
|
||||
|
||||
f := cmd.Flags()
|
||||
f.BoolVar(&t.showNotes, "notes", false, "show the computed NOTES.txt file as well")
|
||||
f.StringVarP(&t.releaseName, "name", "n", "RELEASE-NAME", "release name")
|
||||
f.StringArrayVarP(&t.renderFiles, "execute", "x", []string{}, "only execute the given templates")
|
||||
f.VarP(&t.valueFiles, "values", "f", "specify values in a YAML file (can specify multiple)")
|
||||
f.StringVar(&t.namespace, "namespace", "", "namespace to install the release into")
|
||||
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.StringVar(&t.nameTemplate, "name-template", "", "specify template used to name the release")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (t *templateCmd) run(cmd *cobra.Command, args []string) error {
|
||||
if len(args) < 1 {
|
||||
return errors.New("chart is required")
|
||||
}
|
||||
// verify chart path exists
|
||||
if _, err := os.Stat(args[0]); err == nil {
|
||||
if t.chartPath, err = filepath.Abs(args[0]); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
// verify specified templates exist relative to chart
|
||||
rf := []string{}
|
||||
var af string
|
||||
var err error
|
||||
if len(t.renderFiles) > 0 {
|
||||
for _, f := range t.renderFiles {
|
||||
if !filepath.IsAbs(f) {
|
||||
af, err = filepath.Abs(t.chartPath + "/" + f)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not resolve template path: %s", err)
|
||||
}
|
||||
} else {
|
||||
af = f
|
||||
}
|
||||
rf = append(rf, af)
|
||||
|
||||
if _, err := os.Stat(af); err != nil {
|
||||
return fmt.Errorf("could not resolve template path: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if t.namespace == "" {
|
||||
t.namespace = defaultNamespace()
|
||||
}
|
||||
// get combined values and create config
|
||||
rawVals, err := vals(t.valueFiles, t.values)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config := &chart.Config{Raw: string(rawVals), Values: map[string]*chart.Value{}}
|
||||
|
||||
// If template is specified, try to run the template.
|
||||
if t.nameTemplate != "" {
|
||||
t.releaseName, err = generateName(t.nameTemplate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Check chart requirements to make sure all dependencies are present in /charts
|
||||
c, err := chartutil.Load(t.chartPath)
|
||||
if err != nil {
|
||||
return prettyError(err)
|
||||
}
|
||||
|
||||
if req, err := chartutil.LoadRequirements(c); err == nil {
|
||||
if err := checkDependencies(c, req); err != nil {
|
||||
return prettyError(err)
|
||||
}
|
||||
} else if err != chartutil.ErrRequirementsNotFound {
|
||||
return fmt.Errorf("cannot load requirements: %v", err)
|
||||
}
|
||||
options := chartutil.ReleaseOptions{
|
||||
Name: t.releaseName,
|
||||
Time: timeconv.Now(),
|
||||
Namespace: t.namespace,
|
||||
}
|
||||
|
||||
err = chartutil.ProcessRequirementsEnabled(c, config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = chartutil.ProcessRequirementsImportValues(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set up engine.
|
||||
renderer := engine.New()
|
||||
|
||||
vals, err := chartutil.ToRenderValues(c, config, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out, err := renderer.Render(c, vals)
|
||||
listManifests := []tiller.Manifest{}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// extract kind and name
|
||||
re := regexp.MustCompile("kind:(.*)\n")
|
||||
for k, v := range out {
|
||||
match := re.FindStringSubmatch(v)
|
||||
h := "Unknown"
|
||||
if len(match) == 2 {
|
||||
h = strings.TrimSpace(match[1])
|
||||
}
|
||||
m := tiller.Manifest{Name: k, Content: v, Head: &util.SimpleHead{Kind: h}}
|
||||
listManifests = append(listManifests, m)
|
||||
}
|
||||
in := func(needle string, haystack []string) bool {
|
||||
// make needle path absolute
|
||||
d := strings.Split(needle, "/")
|
||||
dd := d[1:]
|
||||
an := t.chartPath + "/" + strings.Join(dd, "/")
|
||||
|
||||
for _, h := range haystack {
|
||||
if h == an {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
if settings.Debug {
|
||||
rel := &release.Release{
|
||||
Name: t.releaseName,
|
||||
Chart: c,
|
||||
Config: config,
|
||||
Version: 1,
|
||||
Namespace: t.namespace,
|
||||
Info: &release.Info{LastDeployed: timeconv.Timestamp(time.Now())},
|
||||
}
|
||||
printRelease(os.Stdout, rel)
|
||||
}
|
||||
|
||||
for _, m := range tiller.SortByKind(listManifests) {
|
||||
if len(t.renderFiles) > 0 && in(m.Name, rf) == false {
|
||||
continue
|
||||
}
|
||||
data := m.Content
|
||||
b := filepath.Base(m.Name)
|
||||
if !t.showNotes && b == "NOTES.txt" {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(b, "_") {
|
||||
continue
|
||||
}
|
||||
fmt.Printf("---\n# Source: %s\n", m.Name)
|
||||
fmt.Println(data)
|
||||
}
|
||||
return nil
|
||||
}
|
@ -0,0 +1,160 @@
|
||||
/*
|
||||
Copyright 2016 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 (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var chartPath = "./../../pkg/chartutil/testdata/subpop/charts/subchart1"
|
||||
|
||||
func TestTemplateCmd(t *testing.T) {
|
||||
absChartPath, err := filepath.Abs(chartPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
desc string
|
||||
args []string
|
||||
expectKey string
|
||||
expectValue string
|
||||
}{
|
||||
{
|
||||
name: "check_name",
|
||||
desc: "check for a known name in chart",
|
||||
args: []string{chartPath},
|
||||
expectKey: "subchart1/templates/service.yaml",
|
||||
expectValue: "protocol: TCP\n name: nginx",
|
||||
},
|
||||
{
|
||||
name: "check_set_name",
|
||||
desc: "verify --set values exist",
|
||||
args: []string{chartPath, "-x", "templates/service.yaml", "--set", "service.name=apache"},
|
||||
expectKey: "subchart1/templates/service.yaml",
|
||||
expectValue: "protocol: TCP\n name: apache",
|
||||
},
|
||||
{
|
||||
name: "check_execute",
|
||||
desc: "verify --execute single template",
|
||||
args: []string{chartPath, "-x", "templates/service.yaml", "--set", "service.name=apache"},
|
||||
expectKey: "subchart1/templates/service.yaml",
|
||||
expectValue: "protocol: TCP\n name: apache",
|
||||
},
|
||||
{
|
||||
name: "check_execute_absolute",
|
||||
desc: "verify --execute single template",
|
||||
args: []string{chartPath, "-x", absChartPath + "/" + "templates/service.yaml", "--set", "service.name=apache"},
|
||||
expectKey: "subchart1/templates/service.yaml",
|
||||
expectValue: "protocol: TCP\n name: apache",
|
||||
},
|
||||
{
|
||||
name: "check_namespace",
|
||||
desc: "verify --namespace",
|
||||
args: []string{chartPath, "--namespace", "test"},
|
||||
expectKey: "subchart1/templates/service.yaml",
|
||||
expectValue: "namespace: \"test\"",
|
||||
},
|
||||
{
|
||||
name: "check_release_name",
|
||||
desc: "verify --release exists",
|
||||
args: []string{chartPath, "--name", "test"},
|
||||
expectKey: "subchart1/templates/service.yaml",
|
||||
expectValue: "release-name: \"test\"",
|
||||
},
|
||||
{
|
||||
name: "check_notes",
|
||||
desc: "verify --notes shows notes",
|
||||
args: []string{chartPath, "--notes", "true"},
|
||||
expectKey: "subchart1/templates/NOTES.txt",
|
||||
expectValue: "Sample notes for subchart1",
|
||||
},
|
||||
{
|
||||
name: "check_values_files",
|
||||
desc: "verify --values files values exist",
|
||||
args: []string{chartPath, "--values", chartPath + "/charts/subchartA/values.yaml"},
|
||||
expectKey: "subchart1/templates/service.yaml",
|
||||
expectValue: "name: apache",
|
||||
},
|
||||
{
|
||||
name: "check_name_template",
|
||||
desc: "verify --name-template result exists",
|
||||
args: []string{chartPath, "--name-template", "foobar-{{ b64enc \"abc\" }}-baz"},
|
||||
expectKey: "subchart1/templates/service.yaml",
|
||||
expectValue: "release-name: \"foobar-YWJj-baz\"",
|
||||
},
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(T *testing.T) {
|
||||
// capture stdout
|
||||
old := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
// execute template command
|
||||
out := bytes.NewBuffer(nil)
|
||||
cmd := newTemplateCmd(out)
|
||||
cmd.SetArgs(tt.args)
|
||||
err := cmd.Execute()
|
||||
if err != nil {
|
||||
t.Errorf("expected: %v, got %v", tt.expectValue, err)
|
||||
}
|
||||
// restore stdout
|
||||
w.Close()
|
||||
os.Stdout = old
|
||||
var b bytes.Buffer
|
||||
io.Copy(&b, r)
|
||||
r.Close()
|
||||
// scan yaml into map[<path>]yaml
|
||||
scanner := bufio.NewScanner(&b)
|
||||
next := false
|
||||
lastKey := ""
|
||||
m := map[string]string{}
|
||||
for scanner.Scan() {
|
||||
if scanner.Text() == "---" {
|
||||
next = true
|
||||
} else if next {
|
||||
// remove '# Source: '
|
||||
head := "# Source: "
|
||||
lastKey = scanner.Text()[len(head):]
|
||||
next = false
|
||||
} else {
|
||||
m[lastKey] = m[lastKey] + scanner.Text() + "\n"
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "reading standard input:", err)
|
||||
}
|
||||
if v, ok := m[tt.expectKey]; ok {
|
||||
if strings.Contains(v, tt.expectValue) == false {
|
||||
t.Errorf("failed to match expected value %s in %s", tt.expectValue, v)
|
||||
}
|
||||
} else {
|
||||
t.Errorf("could not find key %s", tt.expectKey)
|
||||
}
|
||||
buf.Reset()
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
## 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 templates/deployment.yaml
|
||||
|
||||
|
||||
```
|
||||
helm template [flags] CHART
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-x, --execute stringArray only execute the given templates
|
||||
-n, --name string release name (default "RELEASE-NAME")
|
||||
--name-template string specify template used to name the release
|
||||
--namespace string namespace to install the release into
|
||||
--notes show the computed NOTES.txt file as well
|
||||
--set stringArray set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)
|
||||
-f, --values valueFiles specify values in a YAML file (can specify multiple) (default [])
|
||||
```
|
||||
|
||||
### 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 24-Aug-2017
|
@ -0,0 +1 @@
|
||||
Sample notes for {{ .Chart.Name }}
|
Loading…
Reference in new issue