WIP feat(helm): add `template` command
This adds the functionality from the helm-template plugin to allow the rendering of templates without Tiller. Closes #2755
This commit is contained in:
parent
1dbbace831
commit
4a02a71f1e
|
@ -125,6 +125,7 @@ func newRootCmd(args []string) *cobra.Command {
|
|||
newHomeCmd(out),
|
||||
newInitCmd(out),
|
||||
newPluginCmd(out),
|
||||
newTemplateCmd(out),
|
||||
|
||||
// Hidden documentation generator command: 'helm docs'
|
||||
newDocsCmd(out),
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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 8-Aug-2017
|
||||
|
|
|
@ -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
|
|
@ -3,7 +3,7 @@
|
|||
# Declare variables to be passed into your templates.
|
||||
# subchartA
|
||||
service:
|
||||
name: nginx
|
||||
name: apache
|
||||
type: ClusterIP
|
||||
externalPort: 80
|
||||
internalPort: 80
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Sample notes for {{ .Chart.Name }}
|
|
@ -4,6 +4,8 @@ metadata:
|
|||
name: {{ .Chart.Name }}
|
||||
labels:
|
||||
chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
|
||||
namespace: "{{ .Release.Namespace }}"
|
||||
release-name: "{{ .Release.Name }}"
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
|
|
|
@ -50,16 +50,16 @@ var deletePolices = map[string]release.Hook_DeletePolicy{
|
|||
hooks.HookFailed: release.Hook_FAILED,
|
||||
}
|
||||
|
||||
// manifest represents a manifest file, which has a name and some content.
|
||||
type manifest struct {
|
||||
name string
|
||||
content string
|
||||
head *util.SimpleHead
|
||||
// Manifest represents a manifest file, which has a name and some content.
|
||||
type Manifest struct {
|
||||
Name string
|
||||
Content string
|
||||
Head *util.SimpleHead
|
||||
}
|
||||
|
||||
type result struct {
|
||||
hooks []*release.Hook
|
||||
generic []manifest
|
||||
generic []Manifest
|
||||
}
|
||||
|
||||
type manifestFile struct {
|
||||
|
@ -77,7 +77,7 @@ type manifestFile struct {
|
|||
//
|
||||
// Files that do not parse into the expected format are simply placed into a map and
|
||||
// returned.
|
||||
func sortManifests(files map[string]string, apis chartutil.VersionSet, sort SortOrder) ([]*release.Hook, []manifest, error) {
|
||||
func sortManifests(files map[string]string, apis chartutil.VersionSet, sort SortOrder) ([]*release.Hook, []Manifest, error) {
|
||||
result := &result{}
|
||||
|
||||
for filePath, c := range files {
|
||||
|
@ -141,20 +141,20 @@ func (file *manifestFile) sort(result *result) error {
|
|||
}
|
||||
|
||||
if !hasAnyAnnotation(entry) {
|
||||
result.generic = append(result.generic, manifest{
|
||||
name: file.path,
|
||||
content: m,
|
||||
head: &entry,
|
||||
result.generic = append(result.generic, Manifest{
|
||||
Name: file.path,
|
||||
Content: m,
|
||||
Head: &entry,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
hookTypes, ok := entry.Metadata.Annotations[hooks.HookAnno]
|
||||
if !ok {
|
||||
result.generic = append(result.generic, manifest{
|
||||
name: file.path,
|
||||
content: m,
|
||||
head: &entry,
|
||||
result.generic = append(result.generic, Manifest{
|
||||
Name: file.path,
|
||||
Content: m,
|
||||
Head: &entry,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
|
|
@ -194,7 +194,7 @@ metadata:
|
|||
}
|
||||
|
||||
// Verify the sort order
|
||||
sorted := []manifest{}
|
||||
sorted := []Manifest{}
|
||||
for _, s := range data {
|
||||
manifests := util.SplitManifests(s.manifest)
|
||||
|
||||
|
@ -211,10 +211,10 @@ metadata:
|
|||
|
||||
//only keep track of non-hook manifests
|
||||
if err == nil && s.hooks[name] == nil {
|
||||
another := manifest{
|
||||
content: m,
|
||||
name: name,
|
||||
head: &sh,
|
||||
another := Manifest{
|
||||
Content: m,
|
||||
Name: name,
|
||||
Head: &sh,
|
||||
}
|
||||
sorted = append(sorted, another)
|
||||
}
|
||||
|
@ -223,8 +223,8 @@ metadata:
|
|||
|
||||
sorted = sortByKind(sorted, InstallOrder)
|
||||
for i, m := range generic {
|
||||
if m.content != sorted[i].content {
|
||||
t.Errorf("Expected %q, got %q", m.content, sorted[i].content)
|
||||
if m.Content != sorted[i].Content {
|
||||
t.Errorf("Expected %q, got %q", m.Content, sorted[i].Content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -84,7 +84,7 @@ var UninstallOrder SortOrder = []string{
|
|||
// sortByKind does an in-place sort of manifests by Kind.
|
||||
//
|
||||
// Results are sorted by 'ordering'
|
||||
func sortByKind(manifests []manifest, ordering SortOrder) []manifest {
|
||||
func sortByKind(manifests []Manifest, ordering SortOrder) []Manifest {
|
||||
ks := newKindSorter(manifests, ordering)
|
||||
sort.Sort(ks)
|
||||
return ks.manifests
|
||||
|
@ -92,10 +92,10 @@ func sortByKind(manifests []manifest, ordering SortOrder) []manifest {
|
|||
|
||||
type kindSorter struct {
|
||||
ordering map[string]int
|
||||
manifests []manifest
|
||||
manifests []Manifest
|
||||
}
|
||||
|
||||
func newKindSorter(m []manifest, s SortOrder) *kindSorter {
|
||||
func newKindSorter(m []Manifest, s SortOrder) *kindSorter {
|
||||
o := make(map[string]int, len(s))
|
||||
for v, k := range s {
|
||||
o[k] = v
|
||||
|
@ -114,11 +114,11 @@ func (k *kindSorter) Swap(i, j int) { k.manifests[i], k.manifests[j] = k.manifes
|
|||
func (k *kindSorter) Less(i, j int) bool {
|
||||
a := k.manifests[i]
|
||||
b := k.manifests[j]
|
||||
first, aok := k.ordering[a.head.Kind]
|
||||
second, bok := k.ordering[b.head.Kind]
|
||||
first, aok := k.ordering[a.Head.Kind]
|
||||
second, bok := k.ordering[b.Head.Kind]
|
||||
if first == second {
|
||||
// same kind (including unknown) so sub sort alphanumeric
|
||||
return a.name < b.name
|
||||
return a.Name < b.Name
|
||||
}
|
||||
// unknown kind is last
|
||||
if !aok {
|
||||
|
@ -130,3 +130,11 @@ func (k *kindSorter) Less(i, j int) bool {
|
|||
// sort different kinds
|
||||
return first < second
|
||||
}
|
||||
|
||||
// SortByKind sorts manifests in InstallOrder
|
||||
func SortByKind(manifests []Manifest) []Manifest {
|
||||
ordering := InstallOrder
|
||||
ks := newKindSorter(manifests, ordering)
|
||||
sort.Sort(ks)
|
||||
return ks.manifests
|
||||
}
|
||||
|
|
|
@ -24,103 +24,103 @@ import (
|
|||
)
|
||||
|
||||
func TestKindSorter(t *testing.T) {
|
||||
manifests := []manifest{
|
||||
manifests := []Manifest{
|
||||
{
|
||||
name: "i",
|
||||
head: &util.SimpleHead{Kind: "ClusterRole"},
|
||||
Name: "i",
|
||||
Head: &util.SimpleHead{Kind: "ClusterRole"},
|
||||
},
|
||||
{
|
||||
name: "j",
|
||||
head: &util.SimpleHead{Kind: "ClusterRoleBinding"},
|
||||
Name: "j",
|
||||
Head: &util.SimpleHead{Kind: "ClusterRoleBinding"},
|
||||
},
|
||||
{
|
||||
name: "e",
|
||||
head: &util.SimpleHead{Kind: "ConfigMap"},
|
||||
Name: "e",
|
||||
Head: &util.SimpleHead{Kind: "ConfigMap"},
|
||||
},
|
||||
{
|
||||
name: "u",
|
||||
head: &util.SimpleHead{Kind: "CronJob"},
|
||||
Name: "u",
|
||||
Head: &util.SimpleHead{Kind: "CronJob"},
|
||||
},
|
||||
{
|
||||
name: "n",
|
||||
head: &util.SimpleHead{Kind: "DaemonSet"},
|
||||
Name: "n",
|
||||
Head: &util.SimpleHead{Kind: "DaemonSet"},
|
||||
},
|
||||
{
|
||||
name: "r",
|
||||
head: &util.SimpleHead{Kind: "Deployment"},
|
||||
Name: "r",
|
||||
Head: &util.SimpleHead{Kind: "Deployment"},
|
||||
},
|
||||
{
|
||||
name: "!",
|
||||
head: &util.SimpleHead{Kind: "HonkyTonkSet"},
|
||||
Name: "!",
|
||||
Head: &util.SimpleHead{Kind: "HonkyTonkSet"},
|
||||
},
|
||||
{
|
||||
name: "v",
|
||||
head: &util.SimpleHead{Kind: "Ingress"},
|
||||
Name: "v",
|
||||
Head: &util.SimpleHead{Kind: "Ingress"},
|
||||
},
|
||||
{
|
||||
name: "t",
|
||||
head: &util.SimpleHead{Kind: "Job"},
|
||||
Name: "t",
|
||||
Head: &util.SimpleHead{Kind: "Job"},
|
||||
},
|
||||
{
|
||||
name: "c",
|
||||
head: &util.SimpleHead{Kind: "LimitRange"},
|
||||
Name: "c",
|
||||
Head: &util.SimpleHead{Kind: "LimitRange"},
|
||||
},
|
||||
{
|
||||
name: "a",
|
||||
head: &util.SimpleHead{Kind: "Namespace"},
|
||||
Name: "a",
|
||||
Head: &util.SimpleHead{Kind: "Namespace"},
|
||||
},
|
||||
{
|
||||
name: "f",
|
||||
head: &util.SimpleHead{Kind: "PersistentVolume"},
|
||||
Name: "f",
|
||||
Head: &util.SimpleHead{Kind: "PersistentVolume"},
|
||||
},
|
||||
{
|
||||
name: "g",
|
||||
head: &util.SimpleHead{Kind: "PersistentVolumeClaim"},
|
||||
Name: "g",
|
||||
Head: &util.SimpleHead{Kind: "PersistentVolumeClaim"},
|
||||
},
|
||||
{
|
||||
name: "o",
|
||||
head: &util.SimpleHead{Kind: "Pod"},
|
||||
Name: "o",
|
||||
Head: &util.SimpleHead{Kind: "Pod"},
|
||||
},
|
||||
{
|
||||
name: "q",
|
||||
head: &util.SimpleHead{Kind: "ReplicaSet"},
|
||||
Name: "q",
|
||||
Head: &util.SimpleHead{Kind: "ReplicaSet"},
|
||||
},
|
||||
{
|
||||
name: "p",
|
||||
head: &util.SimpleHead{Kind: "ReplicationController"},
|
||||
Name: "p",
|
||||
Head: &util.SimpleHead{Kind: "ReplicationController"},
|
||||
},
|
||||
{
|
||||
name: "b",
|
||||
head: &util.SimpleHead{Kind: "ResourceQuota"},
|
||||
Name: "b",
|
||||
Head: &util.SimpleHead{Kind: "ResourceQuota"},
|
||||
},
|
||||
{
|
||||
name: "k",
|
||||
head: &util.SimpleHead{Kind: "Role"},
|
||||
Name: "k",
|
||||
Head: &util.SimpleHead{Kind: "Role"},
|
||||
},
|
||||
{
|
||||
name: "l",
|
||||
head: &util.SimpleHead{Kind: "RoleBinding"},
|
||||
Name: "l",
|
||||
Head: &util.SimpleHead{Kind: "RoleBinding"},
|
||||
},
|
||||
{
|
||||
name: "d",
|
||||
head: &util.SimpleHead{Kind: "Secret"},
|
||||
Name: "d",
|
||||
Head: &util.SimpleHead{Kind: "Secret"},
|
||||
},
|
||||
{
|
||||
name: "m",
|
||||
head: &util.SimpleHead{Kind: "Service"},
|
||||
Name: "m",
|
||||
Head: &util.SimpleHead{Kind: "Service"},
|
||||
},
|
||||
{
|
||||
name: "h",
|
||||
head: &util.SimpleHead{Kind: "ServiceAccount"},
|
||||
Name: "h",
|
||||
Head: &util.SimpleHead{Kind: "ServiceAccount"},
|
||||
},
|
||||
{
|
||||
name: "s",
|
||||
head: &util.SimpleHead{Kind: "StatefulSet"},
|
||||
Name: "s",
|
||||
Head: &util.SimpleHead{Kind: "StatefulSet"},
|
||||
},
|
||||
{
|
||||
name: "w",
|
||||
content: "",
|
||||
head: &util.SimpleHead{Kind: "APIService"},
|
||||
Name: "w",
|
||||
Content: "",
|
||||
Head: &util.SimpleHead{Kind: "APIService"},
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -139,7 +139,7 @@ func TestKindSorter(t *testing.T) {
|
|||
}
|
||||
defer buf.Reset()
|
||||
for _, r := range sortByKind(manifests, test.order) {
|
||||
buf.WriteString(r.name)
|
||||
buf.WriteString(r.Name)
|
||||
}
|
||||
if got := buf.String(); got != test.expected {
|
||||
t.Errorf("Expected %q, got %q", test.expected, got)
|
||||
|
@ -150,42 +150,42 @@ func TestKindSorter(t *testing.T) {
|
|||
|
||||
// TestKindSorterSubSort verifies manifests of same kind are also sorted alphanumeric
|
||||
func TestKindSorterSubSort(t *testing.T) {
|
||||
manifests := []manifest{
|
||||
manifests := []Manifest{
|
||||
{
|
||||
name: "a",
|
||||
head: &util.SimpleHead{Kind: "ClusterRole"},
|
||||
Name: "a",
|
||||
Head: &util.SimpleHead{Kind: "ClusterRole"},
|
||||
},
|
||||
{
|
||||
name: "A",
|
||||
head: &util.SimpleHead{Kind: "ClusterRole"},
|
||||
Name: "A",
|
||||
Head: &util.SimpleHead{Kind: "ClusterRole"},
|
||||
},
|
||||
{
|
||||
name: "0",
|
||||
head: &util.SimpleHead{Kind: "ConfigMap"},
|
||||
Name: "0",
|
||||
Head: &util.SimpleHead{Kind: "ConfigMap"},
|
||||
},
|
||||
{
|
||||
name: "1",
|
||||
head: &util.SimpleHead{Kind: "ConfigMap"},
|
||||
Name: "1",
|
||||
Head: &util.SimpleHead{Kind: "ConfigMap"},
|
||||
},
|
||||
{
|
||||
name: "z",
|
||||
head: &util.SimpleHead{Kind: "ClusterRoleBinding"},
|
||||
Name: "z",
|
||||
Head: &util.SimpleHead{Kind: "ClusterRoleBinding"},
|
||||
},
|
||||
{
|
||||
name: "!",
|
||||
head: &util.SimpleHead{Kind: "ClusterRoleBinding"},
|
||||
Name: "!",
|
||||
Head: &util.SimpleHead{Kind: "ClusterRoleBinding"},
|
||||
},
|
||||
{
|
||||
name: "u3",
|
||||
head: &util.SimpleHead{Kind: "Unknown"},
|
||||
Name: "u3",
|
||||
Head: &util.SimpleHead{Kind: "Unknown"},
|
||||
},
|
||||
{
|
||||
name: "u1",
|
||||
head: &util.SimpleHead{Kind: "Unknown"},
|
||||
Name: "u1",
|
||||
Head: &util.SimpleHead{Kind: "Unknown"},
|
||||
},
|
||||
{
|
||||
name: "u2",
|
||||
head: &util.SimpleHead{Kind: "Unknown"},
|
||||
Name: "u2",
|
||||
Head: &util.SimpleHead{Kind: "Unknown"},
|
||||
},
|
||||
}
|
||||
for _, test := range []struct {
|
||||
|
@ -200,7 +200,7 @@ func TestKindSorterSubSort(t *testing.T) {
|
|||
t.Run(test.description, func(t *testing.T) {
|
||||
defer buf.Reset()
|
||||
for _, r := range sortByKind(manifests, test.order) {
|
||||
buf.WriteString(r.name)
|
||||
buf.WriteString(r.Name)
|
||||
}
|
||||
if got := buf.String(); got != test.expected {
|
||||
t.Errorf("Expected %q, got %q", test.expected, got)
|
||||
|
|
|
@ -166,7 +166,7 @@ func DeleteRelease(rel *release.Release, vs chartutil.VersionSet, kubeClient env
|
|||
|
||||
errs = []error{}
|
||||
for _, file := range filesToDelete {
|
||||
b := bytes.NewBufferString(strings.TrimSpace(file.content))
|
||||
b := bytes.NewBufferString(strings.TrimSpace(file.Content))
|
||||
if b.Len() == 0 {
|
||||
continue
|
||||
}
|
||||
|
|
|
@ -300,8 +300,8 @@ func (s *ReleaseServer) renderResources(ch *chart.Chart, values chartutil.Values
|
|||
// Aggregate all valid manifests into one big doc.
|
||||
b := bytes.NewBuffer(nil)
|
||||
for _, m := range manifests {
|
||||
b.WriteString("\n---\n# Source: " + m.name + "\n")
|
||||
b.WriteString(m.content)
|
||||
b.WriteString("\n---\n# Source: " + m.Name + "\n")
|
||||
b.WriteString(m.Content)
|
||||
}
|
||||
|
||||
return hooks, b, notes, nil
|
||||
|
|
|
@ -29,18 +29,18 @@ const resourcePolicyAnno = "helm.sh/resource-policy"
|
|||
// during an uninstallRelease action.
|
||||
const keepPolicy = "keep"
|
||||
|
||||
func filterManifestsToKeep(manifests []manifest) ([]manifest, []manifest) {
|
||||
remaining := []manifest{}
|
||||
keep := []manifest{}
|
||||
func filterManifestsToKeep(manifests []Manifest) ([]Manifest, []Manifest) {
|
||||
remaining := []Manifest{}
|
||||
keep := []Manifest{}
|
||||
|
||||
for _, m := range manifests {
|
||||
|
||||
if m.head.Metadata == nil || m.head.Metadata.Annotations == nil || len(m.head.Metadata.Annotations) == 0 {
|
||||
if m.Head.Metadata == nil || m.Head.Metadata.Annotations == nil || len(m.Head.Metadata.Annotations) == 0 {
|
||||
remaining = append(remaining, m)
|
||||
continue
|
||||
}
|
||||
|
||||
resourcePolicyType, ok := m.head.Metadata.Annotations[resourcePolicyAnno]
|
||||
resourcePolicyType, ok := m.Head.Metadata.Annotations[resourcePolicyAnno]
|
||||
if !ok {
|
||||
remaining = append(remaining, m)
|
||||
continue
|
||||
|
@ -55,10 +55,10 @@ func filterManifestsToKeep(manifests []manifest) ([]manifest, []manifest) {
|
|||
return keep, remaining
|
||||
}
|
||||
|
||||
func summarizeKeptManifests(manifests []manifest) string {
|
||||
func summarizeKeptManifests(manifests []Manifest) string {
|
||||
message := "These resources were kept due to the resource policy:\n"
|
||||
for _, m := range manifests {
|
||||
details := "[" + m.head.Kind + "] " + m.head.Metadata.Name + "\n"
|
||||
details := "[" + m.Head.Kind + "] " + m.Head.Metadata.Name + "\n"
|
||||
message = message + details
|
||||
}
|
||||
return message
|
||||
|
|
Loading…
Reference in New Issue