From e73b02153cf0ba56c45143ca93f7f96f9ee42150 Mon Sep 17 00:00:00 2001 From: Max Goltzsche Date: Sun, 23 May 2021 02:15:24 +0200 Subject: [PATCH] feat: make chart hook output configurable. (#18) Also logs a warning when chart hooks are contained within the output. Relates to #16 --- README.md | 1 + cmd/khelm/fn.go | 3 +- cmd/khelm/template.go | 3 + cmd/khelm/template_test.go | 10 +++ e2e/cli-tests.bats | 3 +- example/chart-hooks-disabled/generator.yaml | 6 ++ .../chart-hooks-disabled/kustomization.yaml | 2 + example/chart-hooks/Chart.yaml | 4 + example/chart-hooks/README.md | 5 ++ example/chart-hooks/generator.yaml | 8 ++ example/chart-hooks/kustomization.yaml | 2 + example/chart-hooks/templates/configmap.yaml | 7 ++ .../templates/post-delete-hook.yaml | 21 ++++++ .../templates/post-install-hook.yaml | 21 ++++++ .../templates/post-rollback-hook.yaml | 16 ++++ .../templates/post-upgrade-hook.yaml | 16 ++++ .../templates/pre-delete-hook.yaml | 16 ++++ .../templates/pre-install-hook.yaml | 16 ++++ .../templates/pre-rollback-hook.yaml | 16 ++++ .../templates/pre-upgrade-hook.yaml | 16 ++++ example/chart-hooks/templates/test-hook.yaml | 16 ++++ internal/matcher/matcher.go | 74 +++++++++++++++---- internal/matcher/matcher_test.go | 28 +++---- pkg/config/config.go | 23 ++---- pkg/helm/render.go | 16 +++- pkg/helm/render_test.go | 72 +++++++++++------- pkg/helm/transform.go | 7 +- 27 files changed, 348 insertions(+), 80 deletions(-) create mode 100644 example/chart-hooks-disabled/generator.yaml create mode 100644 example/chart-hooks-disabled/kustomization.yaml create mode 100644 example/chart-hooks/Chart.yaml create mode 100644 example/chart-hooks/README.md create mode 100644 example/chart-hooks/generator.yaml create mode 100644 example/chart-hooks/kustomization.yaml create mode 100644 example/chart-hooks/templates/configmap.yaml create mode 100644 example/chart-hooks/templates/post-delete-hook.yaml create mode 100644 example/chart-hooks/templates/post-install-hook.yaml create mode 100644 example/chart-hooks/templates/post-rollback-hook.yaml create mode 100644 example/chart-hooks/templates/post-upgrade-hook.yaml create mode 100644 example/chart-hooks/templates/pre-delete-hook.yaml create mode 100644 example/chart-hooks/templates/pre-install-hook.yaml create mode 100644 example/chart-hooks/templates/pre-rollback-hook.yaml create mode 100644 example/chart-hooks/templates/pre-upgrade-hook.yaml create mode 100644 example/chart-hooks/templates/test-hook.yaml diff --git a/README.md b/README.md index 7ae7a13..f851d29 100644 --- a/README.md +++ b/README.md @@ -191,6 +191,7 @@ It exposes a `Helm` struct that provides a `Render()` function that returns the | `exclude[].kind` | | Excludes resources by kind. | | `exclude[].namespace` | | Excludes resources by namespace. | | `exclude[].name` | | Excludes resources by name. | +| `excludeHooks` | `--no-hooks` | If enabled excludes chart hooks from the output. | | `namespace` | `--namespace` | Set the namespace used by Helm templates. | | `namespacedOnly` | `--namespaced-only` | If enabled fail on known cluster-scoped resources and those of unknown kinds. | | `forceNamespace` | `--force-namespace` | Set namespace on all namespaced resources (and those of unknown kinds). | diff --git a/cmd/khelm/fn.go b/cmd/khelm/fn.go index e9a575e..05bd4bc 100644 --- a/cmd/khelm/fn.go +++ b/cmd/khelm/fn.go @@ -151,10 +151,9 @@ func mapOutputPaths(resources []*yaml.RNode, outputMappings []kptFnOutputMapping continue } - resID := meta.GetIdentifier() outPath := defaultOutputPath for i, m := range matchers { - if m.Match(&resID) { + if m.Match(&meta) { outPath = outputMappings[i].OutputPath break } diff --git a/cmd/khelm/template.go b/cmd/khelm/template.go index b8883e3..2095c02 100644 --- a/cmd/khelm/template.go +++ b/cmd/khelm/template.go @@ -68,6 +68,9 @@ func templateCommand(h *helm.Helm, writer io.Writer) *cobra.Command { f.StringSliceVarP(&req.ValueFiles, "values", "f", nil, "Specify values in a YAML file or a URL (can specify multiple)") f.StringSliceVar(&req.APIVersions, "api-versions", nil, "Kubernetes api versions used for Capabilities.APIVersions") f.StringVar(&req.KubeVersion, "kube-version", req.KubeVersion, "Kubernetes version used as Capabilities.KubeVersion.Major/Minor") + f.BoolVar(&req.ExcludeHooks, "no-hooks", req.ExcludeHooks, "If enabled hooks are omitted from the output") + f.BoolVar(&req.ExcludeHooks, "exclude-hooks", req.ExcludeHooks, "If enabled hooks are omitted from the output") + f.Lookup("exclude-hooks").Hidden = true f.StringVarP(&outOpts.FileOrDir, "output", "o", "-", "Write rendered output to given file or directory (as kustomization)") f.BoolVar(&outOpts.Replace, "output-replace", false, "Delete and recreate the whole output directory or file") return cmd diff --git a/cmd/khelm/template_test.go b/cmd/khelm/template_test.go index 072aa13..79e5e3d 100644 --- a/cmd/khelm/template_test.go +++ b/cmd/khelm/template_test.go @@ -84,6 +84,16 @@ func TestTemplateCommand(t *testing.T) { []string{filepath.Join(exampleDir, "force-namespace"), "--force-namespace=forced-namespace"}, 5, "namespace: forced-namespace", }, + { + "chart-hooks", + []string{filepath.Join(exampleDir, "chart-hooks")}, + 10, "helm.sh/hook", + }, + { + "chart-hooks-excluded", + []string{filepath.Join(exampleDir, "chart-hooks"), "--no-hooks"}, + 1, "myvalue", + }, } { t.Run(c.name, func(t *testing.T) { var out bytes.Buffer diff --git a/e2e/cli-tests.bats b/e2e/cli-tests.bats index 9e75567..1ee0be7 100755 --- a/e2e/cli-tests.bats +++ b/e2e/cli-tests.bats @@ -1,6 +1,7 @@ #!/usr/bin/env bats IMAGE=${IMAGE:-mgoltzsche/khelm:latest} +EXAMPLE_DIR="$(pwd)/example" OUT_DIR="$(mktemp -d)" teardown() { @@ -18,7 +19,7 @@ teardown() { } @test "CLI should output kustomization" { - docker run --rm -u $(id -u):$(id -g) -v "$OUT_DIR:/out" -v "$(pwd)/example/namespace:/chart" "$IMAGE" template /chart \ + docker run --rm -u $(id -u):$(id -g) -v "$OUT_DIR:/out" -v "$EXAMPLE_DIR/namespace:/chart" "$IMAGE" template /chart \ --output /out/kdir/ \ --debug ls -la "$OUT_DIR" "$OUT_DIR/kdir" >&2 diff --git a/example/chart-hooks-disabled/generator.yaml b/example/chart-hooks-disabled/generator.yaml new file mode 100644 index 0000000..1e90f2f --- /dev/null +++ b/example/chart-hooks-disabled/generator.yaml @@ -0,0 +1,6 @@ +apiVersion: khelm.mgoltzsche.github.com/v1 +kind: ChartRenderer +metadata: + name: chart-hooks-disabled +chart: ../chart-hooks +excludeHooks: true diff --git a/example/chart-hooks-disabled/kustomization.yaml b/example/chart-hooks-disabled/kustomization.yaml new file mode 100644 index 0000000..318ffad --- /dev/null +++ b/example/chart-hooks-disabled/kustomization.yaml @@ -0,0 +1,2 @@ +generators: +- generator.yaml diff --git a/example/chart-hooks/Chart.yaml b/example/chart-hooks/Chart.yaml new file mode 100644 index 0000000..ee3d67d --- /dev/null +++ b/example/chart-hooks/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +description: example chart with helm hooks +name: hooks +version: 0.1.0 diff --git a/example/chart-hooks/README.md b/example/chart-hooks/README.md new file mode 100644 index 0000000..d549ae5 --- /dev/null +++ b/example/chart-hooks/README.md @@ -0,0 +1,5 @@ +# Example with Helm Chart Hooks + +This is an example kustomization that renders a local Helm chart that contains [hooks](https://helm.sh/docs/topics/charts_hooks/). + +Corresponding to the `helm template` behaviour khelm returns all hook resources unless hooks are disabled explicitly. diff --git a/example/chart-hooks/generator.yaml b/example/chart-hooks/generator.yaml new file mode 100644 index 0000000..9c858c6 --- /dev/null +++ b/example/chart-hooks/generator.yaml @@ -0,0 +1,8 @@ +apiVersion: khelm.mgoltzsche.github.com/v1 +kind: ChartRenderer +metadata: + name: chart-hooks +chart: . +exclude: +- kind: Job + name: chart-hooks-post-rollback diff --git a/example/chart-hooks/kustomization.yaml b/example/chart-hooks/kustomization.yaml new file mode 100644 index 0000000..318ffad --- /dev/null +++ b/example/chart-hooks/kustomization.yaml @@ -0,0 +1,2 @@ +generators: +- generator.yaml diff --git a/example/chart-hooks/templates/configmap.yaml b/example/chart-hooks/templates/configmap.yaml new file mode 100644 index 0000000..0c35b5b --- /dev/null +++ b/example/chart-hooks/templates/configmap.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: "{{ .Release.Name }}-myconfig" + namespace: {{ .Release.Namespace }} +data: + key: myvalue diff --git a/example/chart-hooks/templates/post-delete-hook.yaml b/example/chart-hooks/templates/post-delete-hook.yaml new file mode 100644 index 0000000..6feef0f --- /dev/null +++ b/example/chart-hooks/templates/post-delete-hook.yaml @@ -0,0 +1,21 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: "{{ .Release.Name }}-post-delete" + namespace: {{ .Release.Namespace }} + labels: + app.kubernetes.io/managed-by: {{ .Release.Service | quote }} + app.kubernetes.io/instance: {{ .Release.Name | quote }} + app.kubernetes.io/version: {{ .Chart.AppVersion }} + helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + annotations: + "helm.sh/hook": post-delete + "helm.sh/hook-weight": "-5" + "helm.sh/hook-delete-policy": hook-succeeded +spec: + template: + spec: + restartPolicy: Never + containers: + - name: task + image: "alpine:3.13" diff --git a/example/chart-hooks/templates/post-install-hook.yaml b/example/chart-hooks/templates/post-install-hook.yaml new file mode 100644 index 0000000..f15a1a9 --- /dev/null +++ b/example/chart-hooks/templates/post-install-hook.yaml @@ -0,0 +1,21 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: "{{ .Release.Name }}-post-install" + namespace: {{ .Release.Namespace }} + labels: + app.kubernetes.io/managed-by: {{ .Release.Service | quote }} + app.kubernetes.io/instance: {{ .Release.Name | quote }} + app.kubernetes.io/version: {{ .Chart.AppVersion }} + helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + annotations: + "helm.sh/hook": post-install,post-upgrade + "helm.sh/hook-weight": "-5" + "helm.sh/hook-delete-policy": hook-succeeded +spec: + template: + spec: + restartPolicy: Never + containers: + - name: task + image: "alpine:3.13" diff --git a/example/chart-hooks/templates/post-rollback-hook.yaml b/example/chart-hooks/templates/post-rollback-hook.yaml new file mode 100644 index 0000000..84f798b --- /dev/null +++ b/example/chart-hooks/templates/post-rollback-hook.yaml @@ -0,0 +1,16 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: "{{ .Release.Name }}-post-rollback" + namespace: {{ .Release.Namespace }} + annotations: + "helm.sh/hook": post-rollback + "helm.sh/hook-weight": "-5" + "helm.sh/hook-delete-policy": hook-succeeded +spec: + template: + spec: + restartPolicy: Never + containers: + - name: task + image: "alpine:3.13" diff --git a/example/chart-hooks/templates/post-upgrade-hook.yaml b/example/chart-hooks/templates/post-upgrade-hook.yaml new file mode 100644 index 0000000..0a95064 --- /dev/null +++ b/example/chart-hooks/templates/post-upgrade-hook.yaml @@ -0,0 +1,16 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: "{{ .Release.Name }}-post-upgrade" + namespace: {{ .Release.Namespace }} + annotations: + "helm.sh/hook": post-upgrade + "helm.sh/hook-weight": "-5" + "helm.sh/hook-delete-policy": hook-succeeded +spec: + template: + spec: + restartPolicy: Never + containers: + - name: task + image: "alpine:3.13" diff --git a/example/chart-hooks/templates/pre-delete-hook.yaml b/example/chart-hooks/templates/pre-delete-hook.yaml new file mode 100644 index 0000000..793bbc7 --- /dev/null +++ b/example/chart-hooks/templates/pre-delete-hook.yaml @@ -0,0 +1,16 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: "{{ .Release.Name }}-pre-delete" + namespace: {{ .Release.Namespace }} + annotations: + "helm.sh/hook": pre-delete + "helm.sh/hook-weight": "-5" + "helm.sh/hook-delete-policy": hook-succeeded +spec: + template: + spec: + restartPolicy: Never + containers: + - name: task + image: "alpine:3.13" diff --git a/example/chart-hooks/templates/pre-install-hook.yaml b/example/chart-hooks/templates/pre-install-hook.yaml new file mode 100644 index 0000000..1166e88 --- /dev/null +++ b/example/chart-hooks/templates/pre-install-hook.yaml @@ -0,0 +1,16 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: "{{ .Release.Name }}-pre-install" + namespace: {{ .Release.Namespace }} + annotations: + "helm.sh/hook": pre-install + "helm.sh/hook-weight": "-5" + "helm.sh/hook-delete-policy": hook-succeeded +spec: + template: + spec: + restartPolicy: Never + containers: + - name: task + image: "alpine:3.13" diff --git a/example/chart-hooks/templates/pre-rollback-hook.yaml b/example/chart-hooks/templates/pre-rollback-hook.yaml new file mode 100644 index 0000000..4468747 --- /dev/null +++ b/example/chart-hooks/templates/pre-rollback-hook.yaml @@ -0,0 +1,16 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: "{{ .Release.Name }}-pre-rollback" + namespace: {{ .Release.Namespace }} + annotations: + "helm.sh/hook": pre-rollback + "helm.sh/hook-weight": "-5" + "helm.sh/hook-delete-policy": hook-succeeded +spec: + template: + spec: + restartPolicy: Never + containers: + - name: task + image: "alpine:3.13" diff --git a/example/chart-hooks/templates/pre-upgrade-hook.yaml b/example/chart-hooks/templates/pre-upgrade-hook.yaml new file mode 100644 index 0000000..ee433ab --- /dev/null +++ b/example/chart-hooks/templates/pre-upgrade-hook.yaml @@ -0,0 +1,16 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: "{{ .Release.Name }}-pre-upgrade" + namespace: {{ .Release.Namespace }} + annotations: + "helm.sh/hook": pre-upgrade + "helm.sh/hook-weight": "-5" + "helm.sh/hook-delete-policy": hook-succeeded +spec: + template: + spec: + restartPolicy: Never + containers: + - name: task + image: "alpine:3.13" diff --git a/example/chart-hooks/templates/test-hook.yaml b/example/chart-hooks/templates/test-hook.yaml new file mode 100644 index 0000000..83c1070 --- /dev/null +++ b/example/chart-hooks/templates/test-hook.yaml @@ -0,0 +1,16 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: "{{ .Release.Name }}-test" + namespace: {{ .Release.Namespace }} + annotations: + "helm.sh/hook": test + "helm.sh/hook-weight": "-5" + "helm.sh/hook-delete-policy": hook-succeeded +spec: + template: + spec: + restartPolicy: Never + containers: + - name: task + image: "alpine:3.13" diff --git a/internal/matcher/matcher.go b/internal/matcher/matcher.go index cb5c1c7..734a6a1 100644 --- a/internal/matcher/matcher.go +++ b/internal/matcher/matcher.go @@ -2,23 +2,19 @@ package matcher import ( "fmt" + "sort" "strings" "github.com/mgoltzsche/khelm/pkg/config" "github.com/pkg/errors" + "sigs.k8s.io/kustomize/kyaml/yaml" ) -// KubernetesResourceMeta represents a kubernetes resource's meta data -type KubernetesResourceMeta interface { - GetAPIVersion() string - GetKind() string - GetNamespace() string - GetName() string -} +const annotationHelmHook = "helm.sh/hook" // ResourceMatchers is a group of matchers type ResourceMatchers interface { - Match(o KubernetesResourceMeta) bool + Match(o *yaml.ResourceMeta) bool RequireAllMatched() error } @@ -29,8 +25,8 @@ func Any() ResourceMatchers { type matchAny struct{} -func (m *matchAny) RequireAllMatched() error { return nil } -func (m *matchAny) Match(KubernetesResourceMeta) bool { return true } +func (m *matchAny) RequireAllMatched() error { return nil } +func (m *matchAny) Match(*yaml.ResourceMeta) bool { return true } type resourceMatchers []*resourceMatcher @@ -53,10 +49,10 @@ func (m resourceMatchers) RequireAllMatched() error { return nil } -// MatchAny returns true if any matches matches the given object -func (m resourceMatchers) Match(o KubernetesResourceMeta) bool { +// Match returns true if any matches matches the given object +func (m resourceMatchers) Match(o *yaml.ResourceMeta) bool { for _, e := range m { - if e.ResourceSelector.Match(o) { + if matchSelector(&e.ResourceSelector, o) { e.Matched = true return true } @@ -64,6 +60,14 @@ func (m resourceMatchers) Match(o KubernetesResourceMeta) bool { return false } +// matchSelector returns true if all non-empty fields of the selector match the ones in the provided object +func matchSelector(id *config.ResourceSelector, o *yaml.ResourceMeta) bool { + return (id.APIVersion == "" || id.APIVersion == o.APIVersion) && + (id.Kind == "" || id.Kind == o.Kind) && + (id.Namespace == "" || id.Namespace == o.Namespace) && + (id.Name == "" || id.Name == o.Name) +} + // FromResourceSelectors creates matchers from the provided selectors func FromResourceSelectors(selectors []config.ResourceSelector) ResourceMatchers { matchers := make([]*resourceMatcher, len(selectors)) @@ -72,3 +76,47 @@ func FromResourceSelectors(selectors []config.ResourceSelector) ResourceMatchers } return resourceMatchers(matchers) } + +// ChartHookMatcher matches chart hook resources when the delegated matcher doesn't match +type ChartHookMatcher struct { + ResourceMatchers + delegateOnly bool + hooks map[string]struct{} +} + +// NewChartHookMatcher creates +func NewChartHookMatcher(delegate ResourceMatchers, delegateOnly bool) *ChartHookMatcher { + return &ChartHookMatcher{ + ResourceMatchers: delegate, + delegateOnly: delegateOnly, + hooks: map[string]struct{}{}, + } +} + +// FoundHooks returns all hooks that weren't matched by the delegate matcher +func (m *ChartHookMatcher) FoundHooks() []string { + hooks := make([]string, 0, len(m.hooks)) + for hook := range m.hooks { + hooks = append(hooks, hook) + } + sort.Strings(hooks) + return hooks +} + +// Match returns true if any matches matches the given object +func (m *ChartHookMatcher) Match(o *yaml.ResourceMeta) bool { + if m.ResourceMatchers.Match(o) { + return true + } + + isHook := false + if a := o.Annotations; a != nil { + for _, hook := range strings.Split(a[annotationHelmHook], ",") { + if hook = strings.TrimSpace(hook); hook != "" { + m.hooks[hook] = struct{}{} + isHook = true + } + } + } + return isHook && !m.delegateOnly +} diff --git a/internal/matcher/matcher_test.go b/internal/matcher/matcher_test.go index 4c67784..280fdff 100644 --- a/internal/matcher/matcher_test.go +++ b/internal/matcher/matcher_test.go @@ -10,8 +10,8 @@ import ( func TestMatchAll(t *testing.T) { testee := Any() - resID := testResource("someapi/v1", "SomeKind", "no-match", "").GetIdentifier() - matched := testee.Match(&resID) + resID := testResource("someapi/v1", "SomeKind", "no-match", "") + matched := testee.Match(resID) require.True(t, matched, "matched") err := testee.RequireAllMatched() require.NoError(t, err, "RequireAllMatched") @@ -47,8 +47,7 @@ func TestMatchAny(t *testing.T) { testee := FromResourceSelectors(c.selectors) matched := []string{} for _, o := range input { - resID := o.GetIdentifier() - if testee.Match(&resID) { + if testee.Match(o) { matched = append(matched, o.Name) } } @@ -58,28 +57,29 @@ func TestMatchAny(t *testing.T) { func TestRequireAllMatched(t *testing.T) { testee := FromResourceSelectors([]config.ResourceSelector{{Name: "myresource1"}, {Name: "myresource2"}}) - input := testResource("someapi/v1", "SomeKind", "no-match", "").GetIdentifier() - matched := testee.Match(&input) + input := testResource("someapi/v1", "SomeKind", "no-match", "") + matched := testee.Match(input) require.False(t, matched, "matched") err := testee.RequireAllMatched() require.Error(t, err) - input = testResource("someapi/v1", "SomeKind", "myresource1", "").GetIdentifier() - matched = testee.Match(&input) + input = testResource("someapi/v1", "SomeKind", "myresource1", "") + matched = testee.Match(input) require.True(t, matched, "matched") err = testee.RequireAllMatched() require.Error(t, err) - input = testResource("someapi/v1", "SomeKind", "myresource2", "").GetIdentifier() - matched = testee.Match(&input) + input = testResource("someapi/v1", "SomeKind", "myresource2", "") + matched = testee.Match(input) require.True(t, matched, "matched") err = testee.RequireAllMatched() require.NoError(t, err) } func testResource(apiVersion, kind, name, namespace string) *yaml.ResourceMeta { - return &yaml.ResourceMeta{TypeMeta: yaml.TypeMeta{ - APIVersion: apiVersion, - Kind: kind, - }, + return &yaml.ResourceMeta{ + TypeMeta: yaml.TypeMeta{ + APIVersion: apiVersion, + Kind: kind, + }, ObjectMeta: yaml.ObjectMeta{NameMeta: yaml.NameMeta{ Name: name, Namespace: namespace, diff --git a/pkg/config/config.go b/pkg/config/config.go index 8681971..f143db3 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -51,6 +51,9 @@ func NewChartConfig() (cfg *ChartConfig) { } func (cfg *ChartConfig) applyDefaults() { + if cfg.Namespace == "" { + cfg.Namespace = "default" + } if cfg.KubeVersion == "" { cfg.KubeVersion = fmt.Sprintf("%s.%s", chartutil.DefaultKubeVersion.Major, chartutil.DefaultKubeVersion.Minor) } @@ -82,6 +85,7 @@ type RendererConfig struct { APIVersions []string `yaml:"apiVersions,omitempty"` Include []ResourceSelector `yaml:"include,omitempty"` Exclude []ResourceSelector `yaml:"exclude,omitempty"` + ExcludeHooks bool `yaml:"excludeHooks,omitempty"` NamespacedOnly bool `yaml:"namespacedOnly,omitempty"` ForceNamespace string `yaml:"forceNamespace,omitempty"` } @@ -94,22 +98,6 @@ type ResourceSelector struct { Name string `yaml:"name,omitempty"` } -// KubernetesResourceMeta represents a kubernetes resource's meta data -type KubernetesResourceMeta interface { - GetAPIVersion() string - GetKind() string - GetNamespace() string - GetName() string -} - -// Match returns true if all non-empty fields match the ones in the provided object -func (id *ResourceSelector) Match(o KubernetesResourceMeta) bool { - return (id.APIVersion == "" || id.APIVersion == o.GetAPIVersion()) && - (id.Kind == "" || id.Kind == o.GetKind()) && - (id.Namespace == "" || id.Namespace == o.GetNamespace()) && - (id.Name == "" || id.Name == o.GetName()) -} - // Validate validates the chart renderer config func (cfg *ChartConfig) Validate() (errs []string) { if cfg.Chart == "" { @@ -118,6 +106,9 @@ func (cfg *ChartConfig) Validate() (errs []string) { if cfg.Name == "" { errs = append(errs, "release name not specified") } + if cfg.Namespace == "" { + errs = append(errs, "release namespace not specified") + } return } diff --git a/pkg/helm/render.go b/pkg/helm/render.go index 3ea2613..6d776cf 100644 --- a/pkg/helm/render.go +++ b/pkg/helm/render.go @@ -39,7 +39,7 @@ func (h *Helm) Render(ctx context.Context, req *config.ChartConfig) (r []*yaml.R chartRequested, err := h.loadChart(ctx, req) if err != nil { - return nil, err + return nil, errors.Wrapf(err, "load chart %s", req.Chart) } log.Printf("Rendering chart %s %s with name %q and namespace %q", chartRequested.Metadata.Name, chartRequested.Metadata.Version, req.Name, req.Namespace) @@ -65,6 +65,7 @@ func renderChart(chrt *chart.Chart, req *config.ChartConfig, getters getter.Prov ReleaseOptions: chartutil.ReleaseOptions{ Name: req.Name, Namespace: namespace, + IsInstall: true, }, KubeVersion: req.KubeVersion, } @@ -79,7 +80,7 @@ func renderChart(chrt *chart.Chart, req *config.ChartConfig, getters getter.Prov renderedTemplates, err := renderutil.Render(chrt, config, renderOpts) if err != nil { - return nil, errors.Wrap(err, "render chart") + return nil, errors.Wrapf(err, "render chart %s", chrt.Metadata.Name) } manifests := manifest.SplitManifests(renderedTemplates) @@ -98,8 +99,9 @@ func renderChart(chrt *chart.Chart, req *config.ChartConfig, getters getter.Prov Includes: inclusions, Excludes: matcher.FromResourceSelectors(req.Exclude), NamespacedOnly: req.NamespacedOnly, - OutputPath: "khelm-output", } + chartHookMatcher := matcher.NewChartHookMatcher(transformer.Excludes, !req.ExcludeHooks) + transformer.Excludes = chartHookMatcher r = make([]*yaml.RNode, 0, len(manifests)) for _, m := range sortByKind(manifests) { @@ -122,5 +124,13 @@ func renderChart(chrt *chart.Chart, req *config.ChartConfig, getters getter.Prov return nil, errors.Wrap(err, "resource exclusion") } + if len(r) == 0 { + return nil, errors.Errorf("no output since all resources were excluded") + } + + if hooks := chartHookMatcher.FoundHooks(); !req.ExcludeHooks && len(hooks) > 0 { + log.Printf("WARNING: The chart output contains the following hooks: %s", strings.Join(hooks, ", ")) + } + return } diff --git a/pkg/helm/render_test.go b/pkg/helm/render_test.go index 5583d70..a5122d7 100644 --- a/pkg/helm/render_test.go +++ b/pkg/helm/render_test.go @@ -37,27 +37,40 @@ var rootDir = func() string { func TestRender(t *testing.T) { expectedJenkinsContained := "- host: \"jenkins.example.org\"\n" for _, c := range []struct { - name string - file string - expectedNamespaces []string - expectedContained string + name string + file string + expectedNamespaces []string + expectedContained string + expectedResourceNames []string }{ - {"jenkins", "example/jenkins/generator.yaml", []string{"jenkins"}, expectedJenkinsContained}, - {"values-external", "pkg/helm/generatorwithextvalues.yaml", []string{"jenkins"}, expectedJenkinsContained}, - {"rook-ceph-version-range", "example/rook-ceph/operator/generator.yaml", []string{}, "rook-ceph-v0.9.3"}, - {"cert-manager", "example/cert-manager/generator.yaml", []string{"cert-manager", "kube-system"}, " name: cert-manager-webhook"}, - {"apiversions-condition", "example/apiversions-condition/generator.yaml", []string{}, " config: fancy-config"}, - {"expand-list", "example/expand-list/generator.yaml", []string{"ns1", "ns2", "ns3"}, "\n name: myserviceaccount2\n"}, - {"namespace", "example/namespace/generator.yaml", []string{"default-namespace", "cluster-role-binding-ns"}, " key: b"}, - {"force-namespace", "example/force-namespace/generator.yaml", []string{"forced-namespace"}, " key: b"}, - {"kubeVersion", "example/release-name/generator.yaml", []string{}, " k8sVersion: v1.17.0"}, - {"release-name", "example/release-name/generator.yaml", []string{}, " name: my-release-name-config"}, - {"exclude", "example/exclude/generator.yaml", []string{"cluster-role-binding-ns"}, " key: b"}, - {"include", "example/include/generator.yaml", []string{}, " key: b"}, - {"local-chart-with-local-dependency-and-transitive-remote", "example/localrefref/generator.yaml", []string{}, "rook-ceph-v0.9.3"}, - {"local-chart-with-remote-dependency", "example/localref/generator.yaml", []string{}, "rook-ceph-v0.9.3"}, - {"values-inheritance", "example/values-inheritance/generator.yaml", []string{}, " inherited: inherited value\n fileoverwrite: overwritten by file\n valueoverwrite: overwritten by generator config"}, - {"cluster-scoped", "example/cluster-scoped/generator.yaml", []string{}, "myrolebinding"}, + {"jenkins", "example/jenkins/generator.yaml", []string{"jenkins"}, expectedJenkinsContained, nil}, + {"values-external", "pkg/helm/generatorwithextvalues.yaml", []string{"jenkins"}, expectedJenkinsContained, nil}, + {"rook-ceph-version-range", "example/rook-ceph/operator/generator.yaml", []string{}, "rook-ceph-v0.9.3", nil}, + {"cert-manager", "example/cert-manager/generator.yaml", []string{"cert-manager", "kube-system"}, " name: cert-manager-webhook", nil}, + {"apiversions-condition", "example/apiversions-condition/generator.yaml", []string{}, " config: fancy-config", nil}, + {"expand-list", "example/expand-list/generator.yaml", []string{"ns1", "ns2", "ns3"}, "\n name: myserviceaccount2\n", nil}, + {"namespace", "example/namespace/generator.yaml", []string{"default-namespace", "cluster-role-binding-ns"}, " key: b", nil}, + {"force-namespace", "example/force-namespace/generator.yaml", []string{"forced-namespace"}, " key: b", nil}, + {"kubeVersion", "example/release-name/generator.yaml", []string{}, " k8sVersion: v1.17.0", nil}, + {"release-name", "example/release-name/generator.yaml", []string{}, " name: my-release-name-config", nil}, + {"exclude", "example/exclude/generator.yaml", []string{"cluster-role-binding-ns"}, " key: b", nil}, + {"include", "example/include/generator.yaml", []string{}, " key: b", nil}, + {"local-chart-with-local-dependency-and-transitive-remote", "example/localrefref/generator.yaml", []string{}, "rook-ceph-v0.9.3", nil}, + {"local-chart-with-remote-dependency", "example/localref/generator.yaml", []string{}, "rook-ceph-v0.9.3", nil}, + {"values-inheritance", "example/values-inheritance/generator.yaml", []string{}, " inherited: inherited value\n fileoverwrite: overwritten by file\n valueoverwrite: overwritten by generator config", nil}, + {"cluster-scoped", "example/cluster-scoped/generator.yaml", []string{}, "myrolebinding", nil}, + {"chart-hooks", "example/chart-hooks/generator.yaml", []string{"default"}, " key: myvalue", []string{ + "chart-hooks-myconfig", + "chart-hooks-post-delete", + "chart-hooks-post-install", + "chart-hooks-post-upgrade", + "chart-hooks-pre-delete", + "chart-hooks-pre-install", + "chart-hooks-pre-rollback", + "chart-hooks-pre-upgrade", + "chart-hooks-test", + }}, + {"chart-hooks-disabled", "example/chart-hooks-disabled/generator.yaml", []string{"default"}, " key: myvalue", []string{"chart-hooks-disabled-myconfig"}}, } { t.Run(c.name, func(t *testing.T) { for _, cached := range []string{"", "cached "} { @@ -70,14 +83,16 @@ func TestRender(t *testing.T) { require.NoError(t, err, "rendered %syaml:\n%s", cached, b) require.True(t, len(l) > 0, "%s: rendered result of %s is empty", cached, c.file) require.Contains(t, rendered.String(), c.expectedContained, "%syaml", cached) - found := map[string]struct{}{} + foundResourceNames := []string{} + foundNamespaces := map[string]struct{}{} for _, o := range l { ns := "" meta := o["metadata"].(map[string]interface{}) + foundResourceNames = append(foundResourceNames, meta["name"].(string)) nsVal, ok := meta["namespace"] if ok { if ns, ok = nsVal.(string); ok { - found[ns] = struct{}{} + foundNamespaces[ns] = struct{}{} } require.NotEmpty(t, ns, "%s%s: output resource declares empty namespace field", cached, c.file) } @@ -86,18 +101,23 @@ func TestRender(t *testing.T) { if subject, ok := subjects[0].(map[string]interface{}); ok { if ns, ok = subject["namespace"].(string); ok { require.NotEmpty(t, ns, "%s%s: output resource has empty subjects[0].namespace set explicitly", cached, c.file) - found[ns] = struct{}{} + foundNamespaces[ns] = struct{}{} } } } } + foundNs := []string{} - for k := range found { + for k := range foundNamespaces { foundNs = append(foundNs, k) } sort.Strings(c.expectedNamespaces) sort.Strings(foundNs) require.Equal(t, c.expectedNamespaces, foundNs, "%s%s: namespaces of output resource", cached, c.file) + + if len(c.expectedResourceNames) > 0 { + require.Equal(t, c.expectedResourceNames, foundResourceNames, "resource names") + } } }) } @@ -225,7 +245,7 @@ func TestRenderRepositoryCredentials(t *testing.T) { fakeChartTgz := filepath.Join(rootDir, "example/localrefref/charts/intermediate-chart-0.1.1.tgz") // Create input chart config and fake private chart server - var cfg config.ChartConfig + cfg := config.NewChartConfig() cfg.Chart = "private-chart" cfg.Name = "myrelease" cfg.Version = fmt.Sprintf("0.0.%d", time.Now().Unix()) @@ -266,7 +286,7 @@ func TestRenderRepositoryCredentials(t *testing.T) { } { t.Run(c.name, func(t *testing.T) { cfg.Repository = c.repo - err = render(t, cfg, false, &bytes.Buffer{}) + err = render(t, *cfg, false, &bytes.Buffer{}) require.NoError(t, err, "render chart with repository credentials") }) } diff --git a/pkg/helm/transform.go b/pkg/helm/transform.go index ef5fe3f..8132b6f 100644 --- a/pkg/helm/transform.go +++ b/pkg/helm/transform.go @@ -20,7 +20,6 @@ type manifestTransformer struct { Includes matcher.ResourceMatchers Excludes matcher.ResourceMatchers NamespacedOnly bool - OutputPath string } func (t *manifestTransformer) TransformManifest(manifest io.Reader) (r []*yaml.RNode, err error) { @@ -76,15 +75,13 @@ func (t *manifestTransformer) addResources(o *yaml.RNode, r *[]*yaml.RNode, clus return nil } - resourceID := meta.GetIdentifier() - // Exclude all not explicitly included resources - if !t.Includes.Match(&resourceID) { + if !t.Includes.Match(&meta) { return nil } // Exclude resources - if t.Excludes.Match(&resourceID) { + if t.Excludes.Match(&meta) { return nil }