From 788652fd276cf69e8e188a96e8d1331ab24ad9ae Mon Sep 17 00:00:00 2001 From: Laszlo Uveges Date: Mon, 12 Sep 2022 15:50:50 +0200 Subject: [PATCH 1/8] Add dummy test case to prove that not all hooks are delted on failure Signed-off-by: Laszlo Uveges --- pkg/action/hooks_test.go | 319 +++++++++++++++++---------------------- 1 file changed, 139 insertions(+), 180 deletions(-) diff --git a/pkg/action/hooks_test.go b/pkg/action/hooks_test.go index 38f25d9ab..0849574cb 100644 --- a/pkg/action/hooks_test.go +++ b/pkg/action/hooks_test.go @@ -1,208 +1,167 @@ -/* -Copyright The Helm Authors. - -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 action import ( - "bytes" - "fmt" + "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/kube" + kubefake "helm.sh/helm/v3/pkg/kube/fake" + "helm.sh/helm/v3/pkg/release" + "helm.sh/helm/v3/pkg/storage" + "helm.sh/helm/v3/pkg/storage/driver" "io" + "io/ioutil" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/yaml" + "k8s.io/cli-runtime/pkg/resource" + "reflect" "testing" - - "github.com/stretchr/testify/assert" - - chart "helm.sh/helm/v4/pkg/chart/v2" - kubefake "helm.sh/helm/v4/pkg/kube/fake" - release "helm.sh/helm/v4/pkg/release/v1" + "time" ) -func podManifestWithOutputLogs(hookDefinitions []release.HookOutputLogPolicy) string { - hookDefinitionString := convertHooksToCommaSeparated(hookDefinitions) - return fmt.Sprintf(`kind: Pod -metadata: - name: finding-sharky, - annotations: - "helm.sh/hook": pre-install - "helm.sh/hook-output-log-policy": %s -spec: - containers: - - name: sharky-test - image: fake-image - cmd: fake-command`, hookDefinitionString) -} +type HookFailedError struct{} -func podManifestWithOutputLogWithNamespace(hookDefinitions []release.HookOutputLogPolicy) string { - hookDefinitionString := convertHooksToCommaSeparated(hookDefinitions) - return fmt.Sprintf(`kind: Pod -metadata: - name: finding-george - namespace: sneaky-namespace - annotations: - "helm.sh/hook": pre-install - "helm.sh/hook-output-log-policy": %s -spec: - containers: - - name: george-test - image: fake-image - cmd: fake-command`, hookDefinitionString) +func (e *HookFailedError) Error() string { + return "Hook failed!" } -func jobManifestWithOutputLog(hookDefinitions []release.HookOutputLogPolicy) string { - hookDefinitionString := convertHooksToCommaSeparated(hookDefinitions) - return fmt.Sprintf(`kind: Job -apiVersion: batch/v1 -metadata: - name: losing-religion - annotations: - "helm.sh/hook": pre-install - "helm.sh/hook-output-log-policy": %s -spec: - completions: 1 - parallelism: 1 - activeDeadlineSeconds: 30 - template: - spec: - containers: - - name: religion-container - image: religion-image - cmd: religion-command`, hookDefinitionString) +type HookFailingKubeClient struct { + kubefake.PrintingKubeClient + failOn resource.Info + deleteRecord []resource.Info } -func jobManifestWithOutputLogWithNamespace(hookDefinitions []release.HookOutputLogPolicy) string { - hookDefinitionString := convertHooksToCommaSeparated(hookDefinitions) - return fmt.Sprintf(`kind: Job -apiVersion: batch/v1 -metadata: - name: losing-religion - namespace: rem-namespace - annotations: - "helm.sh/hook": pre-install - "helm.sh/hook-output-log-policy": %s -spec: - completions: 1 - parallelism: 1 - activeDeadlineSeconds: 30 - template: - spec: - containers: - - name: religion-container - image: religion-image - cmd: religion-command`, hookDefinitionString) -} +func (_ *HookFailingKubeClient) Build(reader io.Reader, _ bool) (kube.ResourceList, error) { + configMap := &v1.ConfigMap{} -func convertHooksToCommaSeparated(hookDefinitions []release.HookOutputLogPolicy) string { - var commaSeparated string - for i, policy := range hookDefinitions { - if i+1 == len(hookDefinitions) { - commaSeparated += policy.String() - } else { - commaSeparated += policy.String() + "," - } + err := yaml.NewYAMLOrJSONDecoder(reader, 1000).Decode(configMap) + + if err != nil { + return kube.ResourceList{}, err } - return commaSeparated -} -func TestInstallRelease_HookOutputLogsOnFailure(t *testing.T) { - // Should output on failure with expected namespace if hook-failed is set - runInstallForHooksWithFailure(t, podManifestWithOutputLogs([]release.HookOutputLogPolicy{release.HookOutputOnFailed}), "spaced", true) - runInstallForHooksWithFailure(t, podManifestWithOutputLogWithNamespace([]release.HookOutputLogPolicy{release.HookOutputOnFailed}), "sneaky-namespace", true) - runInstallForHooksWithFailure(t, jobManifestWithOutputLog([]release.HookOutputLogPolicy{release.HookOutputOnFailed}), "spaced", true) - runInstallForHooksWithFailure(t, jobManifestWithOutputLogWithNamespace([]release.HookOutputLogPolicy{release.HookOutputOnFailed}), "rem-namespace", true) - - // Should not output on failure with expected namespace if hook-succeed is set - runInstallForHooksWithFailure(t, podManifestWithOutputLogs([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded}), "", false) - runInstallForHooksWithFailure(t, podManifestWithOutputLogWithNamespace([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded}), "", false) - runInstallForHooksWithFailure(t, jobManifestWithOutputLog([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded}), "", false) - runInstallForHooksWithFailure(t, jobManifestWithOutputLogWithNamespace([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded}), "", false) + return kube.ResourceList{{ + Name: configMap.Name, + Namespace: configMap.Namespace, + }}, nil } -func TestInstallRelease_HookOutputLogsOnSuccess(t *testing.T) { - // Should output on success with expected namespace if hook-succeeded is set - runInstallForHooksWithSuccess(t, podManifestWithOutputLogs([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded}), "spaced", true) - runInstallForHooksWithSuccess(t, podManifestWithOutputLogWithNamespace([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded}), "sneaky-namespace", true) - runInstallForHooksWithSuccess(t, jobManifestWithOutputLog([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded}), "spaced", true) - runInstallForHooksWithSuccess(t, jobManifestWithOutputLogWithNamespace([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded}), "rem-namespace", true) - - // Should not output on success if hook-failed is set - runInstallForHooksWithSuccess(t, podManifestWithOutputLogs([]release.HookOutputLogPolicy{release.HookOutputOnFailed}), "", false) - runInstallForHooksWithSuccess(t, podManifestWithOutputLogWithNamespace([]release.HookOutputLogPolicy{release.HookOutputOnFailed}), "", false) - runInstallForHooksWithSuccess(t, jobManifestWithOutputLog([]release.HookOutputLogPolicy{release.HookOutputOnFailed}), "", false) - runInstallForHooksWithSuccess(t, jobManifestWithOutputLogWithNamespace([]release.HookOutputLogPolicy{release.HookOutputOnFailed}), "", false) -} +func (h *HookFailingKubeClient) WatchUntilReady(resources kube.ResourceList, duration time.Duration) error { + for _, res := range resources { + if res.Name == h.failOn.Name && res.Namespace == h.failOn.Namespace { + return &HookFailedError{} + } + } -func TestInstallRelease_HooksOutputLogsOnSuccessAndFailure(t *testing.T) { - // Should output on success with expected namespace if hook-succeeded and hook-failed is set - runInstallForHooksWithSuccess(t, podManifestWithOutputLogs([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded, release.HookOutputOnFailed}), "spaced", true) - runInstallForHooksWithSuccess(t, podManifestWithOutputLogWithNamespace([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded, release.HookOutputOnFailed}), "sneaky-namespace", true) - runInstallForHooksWithSuccess(t, jobManifestWithOutputLog([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded, release.HookOutputOnFailed}), "spaced", true) - runInstallForHooksWithSuccess(t, jobManifestWithOutputLogWithNamespace([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded, release.HookOutputOnFailed}), "rem-namespace", true) - - // Should output on failure if hook-succeeded and hook-failed is set - runInstallForHooksWithFailure(t, podManifestWithOutputLogs([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded, release.HookOutputOnFailed}), "spaced", true) - runInstallForHooksWithFailure(t, podManifestWithOutputLogWithNamespace([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded, release.HookOutputOnFailed}), "sneaky-namespace", true) - runInstallForHooksWithFailure(t, jobManifestWithOutputLog([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded, release.HookOutputOnFailed}), "spaced", true) - runInstallForHooksWithFailure(t, jobManifestWithOutputLogWithNamespace([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded, release.HookOutputOnFailed}), "rem-namespace", true) + return h.PrintingKubeClient.WatchUntilReady(resources, duration) } -func runInstallForHooksWithSuccess(t *testing.T, manifest, expectedNamespace string, shouldOutput bool) { - var expectedOutput string - if shouldOutput { - expectedOutput = fmt.Sprintf("attempted to output logs for namespace: %s", expectedNamespace) +func (h *HookFailingKubeClient) Delete(resources kube.ResourceList) (*kube.Result, []error) { + for _, res := range resources { + h.deleteRecord = append(h.deleteRecord, resource.Info{ + Name: res.Name, + Namespace: res.Namespace, + }) } - is := assert.New(t) - instAction := installAction(t) - instAction.ReleaseName = "failed-hooks" - outBuffer := &bytes.Buffer{} - instAction.cfg.KubeClient = &kubefake.PrintingKubeClient{Out: io.Discard, LogOutput: outBuffer} - - templates := []*chart.File{ - {Name: "templates/hello", Data: []byte("hello: world")}, - {Name: "templates/hooks", Data: []byte(manifest)}, - } - vals := map[string]interface{}{} - res, err := instAction.Run(buildChartWithTemplates(templates), vals) - is.NoError(err) - is.Equal(expectedOutput, outBuffer.String()) - is.Equal(release.StatusDeployed, res.Info.Status) + return h.PrintingKubeClient.Delete(resources) } -func runInstallForHooksWithFailure(t *testing.T, manifest, expectedNamespace string, shouldOutput bool) { - var expectedOutput string - if shouldOutput { - expectedOutput = fmt.Sprintf("attempted to output logs for namespace: %s", expectedNamespace) +func TestHooksCleanUp(t *testing.T) { + hookKubeClient := &HookFailingKubeClient{kubefake.PrintingKubeClient{Out: ioutil.Discard}, resource.Info{ + Name: "build-config-2", + Namespace: "test", + }, []resource.Info{}} + + configuration := &Configuration{ + Releases: storage.Init(driver.NewMemory()), + KubeClient: hookKubeClient, + Capabilities: chartutil.DefaultCapabilities, + Log: func(format string, v ...interface{}) { + t.Helper() + if *verbose { + t.Logf(format, v...) + } + }, + } + + hookEvent := release.HookPreInstall + + r := &release.Release{ + Name: "test-release", + Namespace: "test", + Hooks: []*release.Hook{ + { + Name: "hook-1", + Kind: "ConfigMap", + Path: "templates/service_account.yaml", + Manifest: `apiVersion: v1 +kind: ConfigMap +metadata: + name: build-config-1 + namespace: test +data: + foo: bar +`, + Weight: -5, + Events: []release.HookEvent{ + hookEvent, + }, + DeletePolicies: []release.HookDeletePolicy{ + release.HookBeforeHookCreation, + release.HookSucceeded, + release.HookFailed, + }, + LastRun: release.HookExecution{ + Phase: release.HookPhaseSucceeded, + }, + }, + { + Name: "hook-2", + Kind: "ConfigMap", + Path: "templates/job.yaml", + Manifest: `apiVersion: v1 +kind: ConfigMap +metadata: + name: build-config-2 + namespace: test +data: + foo: bar +`, + Weight: 0, + Events: []release.HookEvent{ + hookEvent, + }, + DeletePolicies: []release.HookDeletePolicy{ + release.HookBeforeHookCreation, + release.HookSucceeded, + release.HookFailed, + }, + LastRun: release.HookExecution{ + Phase: release.HookPhaseFailed, + }, + }, + }, } - is := assert.New(t) - instAction := installAction(t) - instAction.ReleaseName = "failed-hooks" - failingClient := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient) - failingClient.WatchUntilReadyError = fmt.Errorf("failed watch") - instAction.cfg.KubeClient = failingClient - outBuffer := &bytes.Buffer{} - failingClient.PrintingKubeClient = kubefake.PrintingKubeClient{Out: io.Discard, LogOutput: outBuffer} - - templates := []*chart.File{ - {Name: "templates/hello", Data: []byte("hello: world")}, - {Name: "templates/hooks", Data: []byte(manifest)}, + + _ = configuration.execHook(r, hookEvent, 600) + + if !reflect.DeepEqual(hookKubeClient.deleteRecord, []resource.Info{ + { + Name: "build-config-1", + Namespace: "test", + }, + { + Name: "build-config-2", + Namespace: "test", + }, + { + Name: "build-config-2", + Namespace: "test", + }, + }) { + t.Fatalf("Got unexpected delete record") } - vals := map[string]interface{}{} - res, err := instAction.Run(buildChartWithTemplates(templates), vals) - is.Error(err) - is.Contains(res.Info.Description, "failed pre-install") - is.Equal(expectedOutput, outBuffer.String()) - is.Equal(release.StatusFailed, res.Info.Status) + //if err != nil { + // t.Fatalf("An expected error occured: %#v", err) + //} } From 0a28223ae57309f4bef7724e344fc9e8586506b1 Mon Sep 17 00:00:00 2001 From: Laszlo Uveges Date: Mon, 12 Sep 2022 16:47:07 +0200 Subject: [PATCH 2/8] Restructure hooks tests to be reusable Signed-off-by: Laszlo Uveges --- pkg/action/hooks_test.go | 169 ++++++++++++++++++++++----------------- 1 file changed, 95 insertions(+), 74 deletions(-) diff --git a/pkg/action/hooks_test.go b/pkg/action/hooks_test.go index 0849574cb..0a3df94ef 100644 --- a/pkg/action/hooks_test.go +++ b/pkg/action/hooks_test.go @@ -66,34 +66,26 @@ func (h *HookFailingKubeClient) Delete(resources kube.ResourceList) (*kube.Resul } func TestHooksCleanUp(t *testing.T) { - hookKubeClient := &HookFailingKubeClient{kubefake.PrintingKubeClient{Out: ioutil.Discard}, resource.Info{ - Name: "build-config-2", - Namespace: "test", - }, []resource.Info{}} - - configuration := &Configuration{ - Releases: storage.Init(driver.NewMemory()), - KubeClient: hookKubeClient, - Capabilities: chartutil.DefaultCapabilities, - Log: func(format string, v ...interface{}) { - t.Helper() - if *verbose { - t.Logf(format, v...) - } - }, - } - hookEvent := release.HookPreInstall - r := &release.Release{ - Name: "test-release", - Namespace: "test", - Hooks: []*release.Hook{ - { - Name: "hook-1", - Kind: "ConfigMap", - Path: "templates/service_account.yaml", - Manifest: `apiVersion: v1 + testCases := []struct { + name string + inputRelease release.Release + failOn resource.Info + expectedDeleteRecord []resource.Info + expectError bool + }{ + { + "Deletion hook runs for previously successful hook on failure of a heavier weight hook", + release.Release{ + Name: "test-release", + Namespace: "test", + Hooks: []*release.Hook{ + { + Name: "hook-1", + Kind: "ConfigMap", + Path: "templates/service_account.yaml", + Manifest: `apiVersion: v1 kind: ConfigMap metadata: name: build-config-1 @@ -101,24 +93,24 @@ metadata: data: foo: bar `, - Weight: -5, - Events: []release.HookEvent{ - hookEvent, - }, - DeletePolicies: []release.HookDeletePolicy{ - release.HookBeforeHookCreation, - release.HookSucceeded, - release.HookFailed, - }, - LastRun: release.HookExecution{ - Phase: release.HookPhaseSucceeded, - }, - }, - { - Name: "hook-2", - Kind: "ConfigMap", - Path: "templates/job.yaml", - Manifest: `apiVersion: v1 + Weight: -5, + Events: []release.HookEvent{ + hookEvent, + }, + DeletePolicies: []release.HookDeletePolicy{ + release.HookBeforeHookCreation, + release.HookSucceeded, + release.HookFailed, + }, + LastRun: release.HookExecution{ + Phase: release.HookPhaseSucceeded, + }, + }, + { + Name: "hook-2", + Kind: "ConfigMap", + Path: "templates/job.yaml", + Manifest: `apiVersion: v1 kind: ConfigMap metadata: name: build-config-2 @@ -126,42 +118,71 @@ metadata: data: foo: bar `, - Weight: 0, - Events: []release.HookEvent{ - hookEvent, + Weight: 0, + Events: []release.HookEvent{ + hookEvent, + }, + DeletePolicies: []release.HookDeletePolicy{ + release.HookBeforeHookCreation, + release.HookSucceeded, + release.HookFailed, + }, + LastRun: release.HookExecution{ + Phase: release.HookPhaseFailed, + }, + }, + }, + }, resource.Info{ + Name: "build-config-2", + Namespace: "test", + }, []resource.Info{ + { + Name: "build-config-1", + Namespace: "test", }, - DeletePolicies: []release.HookDeletePolicy{ - release.HookBeforeHookCreation, - release.HookSucceeded, - release.HookFailed, + { + Name: "build-config-2", + Namespace: "test", }, - LastRun: release.HookExecution{ - Phase: release.HookPhaseFailed, + { + Name: "build-config-2", + Namespace: "test", }, - }, + }, true, }, } - _ = configuration.execHook(r, hookEvent, 600) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + kubeClient := &HookFailingKubeClient{ + kubefake.PrintingKubeClient{Out: ioutil.Discard}, tc.failOn, []resource.Info{}, + } - if !reflect.DeepEqual(hookKubeClient.deleteRecord, []resource.Info{ - { - Name: "build-config-1", - Namespace: "test", - }, - { - Name: "build-config-2", - Namespace: "test", - }, - { - Name: "build-config-2", - Namespace: "test", - }, - }) { - t.Fatalf("Got unexpected delete record") - } + configuration := &Configuration{ + Releases: storage.Init(driver.NewMemory()), + KubeClient: kubeClient, + Capabilities: chartutil.DefaultCapabilities, + Log: func(format string, v ...interface{}) { + t.Helper() + if *verbose { + t.Logf(format, v...) + } + }, + } + + err := configuration.execHook(&tc.inputRelease, hookEvent, 600) - //if err != nil { - // t.Fatalf("An expected error occured: %#v", err) - //} + if !reflect.DeepEqual(kubeClient.deleteRecord, tc.expectedDeleteRecord) { + t.Fatalf("Got unexpected delete record, expected: %#v, but got: %#v", kubeClient.deleteRecord, tc.expectedDeleteRecord) + } + + if err != nil && !tc.expectError { + t.Fatalf("Got an unexpected error.") + } + + if err == nil && tc.expectError { + t.Fatalf("Expected and error but did not get it.") + } + }) + } } From 2eea520ba47af957e23247c3175d558515309c1a Mon Sep 17 00:00:00 2001 From: Laszlo Uveges Date: Mon, 12 Sep 2022 16:52:14 +0200 Subject: [PATCH 3/8] Delete previously successful hooks when a later hook fails Signed-off-by: Laszlo Uveges --- pkg/action/hooks.go | 110 ++++++++------------------------------- pkg/action/hooks_test.go | 8 +++ 2 files changed, 31 insertions(+), 87 deletions(-) diff --git a/pkg/action/hooks.go b/pkg/action/hooks.go index 230e9ec81..4fbe28bbe 100644 --- a/pkg/action/hooks.go +++ b/pkg/action/hooks.go @@ -17,21 +17,13 @@ package action import ( "bytes" - "fmt" - "log" - "slices" "sort" "time" - "helm.sh/helm/v4/pkg/kube" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "github.com/pkg/errors" - "gopkg.in/yaml.v3" - release "helm.sh/helm/v4/pkg/release/v1" - helmtime "helm.sh/helm/v4/pkg/time" + "helm.sh/helm/v3/pkg/release" + helmtime "helm.sh/helm/v3/pkg/time" ) // execHook executes all of the hooks for the given hook event. @@ -49,9 +41,9 @@ func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent, // hooke are pre-ordered by kind, so keep order stable sort.Stable(hookByWeight(executingHooks)) - for _, h := range executingHooks { + for i, h := range executingHooks { // Set default delete policy to before-hook-creation - if len(h.DeletePolicies) == 0 { + if h.DeletePolicies == nil || len(h.DeletePolicies) == 0 { // TODO(jlegrone): Only apply before-hook-creation delete policy to run to completion // resources. For all other resource types update in place if a // resource with the same name already exists and is owned by the @@ -59,7 +51,7 @@ func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent, h.DeletePolicies = []release.HookDeletePolicy{release.HookBeforeHookCreation} } - if err := cfg.deleteHookByPolicy(h, release.HookBeforeHookCreation, timeout); err != nil { + if err := cfg.deleteHookByPolicy(h, release.HookBeforeHookCreation); err != nil { return err } @@ -94,33 +86,27 @@ func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent, // Mark hook as succeeded or failed if err != nil { h.LastRun.Phase = release.HookPhaseFailed - // If a hook is failed, check the annotation of the hook to determine if we should copy the logs client side - if errOutputting := cfg.outputLogsByPolicy(h, rl.Namespace, release.HookOutputOnFailed); errOutputting != nil { - // We log the error here as we want to propagate the hook failure upwards to the release object. - log.Printf("error outputting logs for hook failure: %v", errOutputting) - } // If a hook is failed, check the annotation of the hook to determine whether the hook should be deleted // under failed condition. If so, then clear the corresponding resource object in the hook - if errDeleting := cfg.deleteHookByPolicy(h, release.HookFailed, timeout); errDeleting != nil { - // We log the error here as we want to propagate the hook failure upwards to the release object. - log.Printf("error deleting the hook resource on hook failure: %v", errDeleting) + if err := cfg.deleteHookByPolicy(h, release.HookFailed); err != nil { + return err } + + // If a hook is failed, check the annotation of the previous successful hooks to determine whether the hook + // should be deleted under succeeded condition. + if err := cfg.deleteHooksByPolicy(executingHooks[0:i], release.HookSucceeded); err != nil { + return err + } + return err } h.LastRun.Phase = release.HookPhaseSucceeded } // If all hooks are successful, check the annotation of each hook to determine whether the hook should be deleted - // or output should be logged under succeeded condition. If so, then clear the corresponding resource object in each hook - for i := len(executingHooks) - 1; i >= 0; i-- { - h := executingHooks[i] - if err := cfg.outputLogsByPolicy(h, rl.Namespace, release.HookOutputOnSucceeded); err != nil { - // We log here as we still want to attempt hook resource deletion even if output logging fails. - log.Printf("error outputting logs for hook failure: %v", err) - } - if err := cfg.deleteHookByPolicy(h, release.HookSucceeded, timeout); err != nil { - return err - } + // under succeeded condition. If so, then clear the corresponding resource object in each hook + if err := cfg.deleteHooksByPolicy(executingHooks, release.HookSucceeded); err != nil { + return err } return nil @@ -139,7 +125,7 @@ func (x hookByWeight) Less(i, j int) bool { } // deleteHookByPolicy deletes a hook if the hook policy instructs it to -func (cfg *Configuration) deleteHookByPolicy(h *release.Hook, policy release.HookDeletePolicy, timeout time.Duration) error { +func (cfg *Configuration) deleteHookByPolicy(h *release.Hook, policy release.HookDeletePolicy) error { // Never delete CustomResourceDefinitions; this could cause lots of // cascading garbage collection. if h.Kind == "CustomResourceDefinition" { @@ -154,13 +140,6 @@ func (cfg *Configuration) deleteHookByPolicy(h *release.Hook, policy release.Hoo if len(errs) > 0 { return errors.New(joinErrors(errs)) } - - // wait for resources until they are deleted to avoid conflicts - if kubeClient, ok := cfg.KubeClient.(kube.InterfaceExt); ok { - if err := kubeClient.WaitForDelete(resources, timeout); err != nil { - return err - } - } } return nil } @@ -176,56 +155,13 @@ func hookHasDeletePolicy(h *release.Hook, policy release.HookDeletePolicy) bool return false } -// outputLogsByPolicy outputs a pods logs if the hook policy instructs it to -func (cfg *Configuration) outputLogsByPolicy(h *release.Hook, releaseNamespace string, policy release.HookOutputLogPolicy) error { - if !hookHasOutputLogPolicy(h, policy) { - return nil - } - namespace, err := cfg.deriveNamespace(h, releaseNamespace) - if err != nil { - return err - } - switch h.Kind { - case "Job": - return cfg.outputContainerLogsForListOptions(namespace, metav1.ListOptions{LabelSelector: fmt.Sprintf("job-name=%s", h.Name)}) - case "Pod": - return cfg.outputContainerLogsForListOptions(namespace, metav1.ListOptions{FieldSelector: fmt.Sprintf("metadata.name=%s", h.Name)}) - default: - return nil - } -} - -func (cfg *Configuration) outputContainerLogsForListOptions(namespace string, listOptions metav1.ListOptions) error { - // TODO Helm 4: Remove this check when GetPodList and OutputContainerLogsForPodList are moved from InterfaceLogs to Interface - if kubeClient, ok := cfg.KubeClient.(kube.InterfaceLogs); ok { - podList, err := kubeClient.GetPodList(namespace, listOptions) - if err != nil { +// deleteHooksByPolicy deletes all hooks if the hook policy instructs it to +func (cfg *Configuration) deleteHooksByPolicy(hooks []*release.Hook, policy release.HookDeletePolicy) error { + for _, h := range hooks { + if err := cfg.deleteHookByPolicy(h, policy); err != nil { return err } - err = kubeClient.OutputContainerLogsForPodList(podList, namespace, cfg.HookOutputFunc) - return err } - return nil -} - -func (cfg *Configuration) deriveNamespace(h *release.Hook, namespace string) (string, error) { - tmp := struct { - Metadata struct { - Namespace string - } - }{} - err := yaml.Unmarshal([]byte(h.Manifest), &tmp) - if err != nil { - return "", errors.Wrapf(err, "unable to parse metadata.namespace from kubernetes manifest for output logs hook %s", h.Path) - } - if tmp.Metadata.Namespace == "" { - return namespace, nil - } - return tmp.Metadata.Namespace, nil -} -// hookHasOutputLogPolicy determines whether the defined hook output log policy matches the hook output log policies -// supported by helm. -func hookHasOutputLogPolicy(h *release.Hook, policy release.HookOutputLogPolicy) bool { - return slices.Contains(h.OutputLogPolicies, policy) + return nil } diff --git a/pkg/action/hooks_test.go b/pkg/action/hooks_test.go index 0a3df94ef..bb9f39c1e 100644 --- a/pkg/action/hooks_test.go +++ b/pkg/action/hooks_test.go @@ -137,17 +137,25 @@ data: Namespace: "test", }, []resource.Info{ { + // This should be in the record for `before-hook-creation` Name: "build-config-1", Namespace: "test", }, { + // This should be in the record for `before-hook-creation` Name: "build-config-2", Namespace: "test", }, { + // This should be in the record for cleaning up (the failure first) Name: "build-config-2", Namespace: "test", }, + { + // This should be in the record for cleaning up (then the previously successful) + Name: "build-config-1", + Namespace: "test", + }, }, true, }, } From 940966d9c82125f095cb4e1c725902551686822e Mon Sep 17 00:00:00 2001 From: Laszlo Uveges Date: Mon, 12 Sep 2022 17:19:25 +0200 Subject: [PATCH 4/8] Fix formatting issues Signed-off-by: Laszlo Uveges --- pkg/action/hooks_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/action/hooks_test.go b/pkg/action/hooks_test.go index bb9f39c1e..75a97d031 100644 --- a/pkg/action/hooks_test.go +++ b/pkg/action/hooks_test.go @@ -29,7 +29,7 @@ type HookFailingKubeClient struct { deleteRecord []resource.Info } -func (_ *HookFailingKubeClient) Build(reader io.Reader, _ bool) (kube.ResourceList, error) { +func (*HookFailingKubeClient) Build(reader io.Reader, _ bool) (kube.ResourceList, error) { configMap := &v1.ConfigMap{} err := yaml.NewYAMLOrJSONDecoder(reader, 1000).Decode(configMap) From d03981b82c84f1682d4f6f8dbeb1dcb9a1025df8 Mon Sep 17 00:00:00 2001 From: Laszlo Uveges Date: Mon, 12 Sep 2022 17:34:30 +0200 Subject: [PATCH 5/8] Fix go imports Signed-off-by: Laszlo Uveges --- pkg/action/hooks_test.go | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/pkg/action/hooks_test.go b/pkg/action/hooks_test.go index 75a97d031..49c53ceac 100644 --- a/pkg/action/hooks_test.go +++ b/pkg/action/hooks_test.go @@ -1,20 +1,22 @@ package action import ( + "io" + "io/ioutil" + "reflect" + "testing" + "time" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/yaml" + "k8s.io/cli-runtime/pkg/resource" + "helm.sh/helm/v3/pkg/chartutil" "helm.sh/helm/v3/pkg/kube" kubefake "helm.sh/helm/v3/pkg/kube/fake" "helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/storage" "helm.sh/helm/v3/pkg/storage/driver" - "io" - "io/ioutil" - v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/util/yaml" - "k8s.io/cli-runtime/pkg/resource" - "reflect" - "testing" - "time" ) type HookFailedError struct{} From acca1b04eb800823678f0386a7d85ecabd161af1 Mon Sep 17 00:00:00 2001 From: Laszlo Uveges Date: Mon, 12 Sep 2022 17:40:58 +0200 Subject: [PATCH 6/8] Add missing license header Signed-off-by: Laszlo Uveges --- pkg/action/hooks_test.go | 60 +++++++++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 22 deletions(-) diff --git a/pkg/action/hooks_test.go b/pkg/action/hooks_test.go index 49c53ceac..9c279bc1a 100644 --- a/pkg/action/hooks_test.go +++ b/pkg/action/hooks_test.go @@ -1,3 +1,19 @@ +/* +Copyright The Helm Authors. + +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 action import ( @@ -135,30 +151,30 @@ data: }, }, }, resource.Info{ + Name: "build-config-2", + Namespace: "test", + }, []resource.Info{ + { + // This should be in the record for `before-hook-creation` + Name: "build-config-1", + Namespace: "test", + }, + { + // This should be in the record for `before-hook-creation` Name: "build-config-2", Namespace: "test", - }, []resource.Info{ - { - // This should be in the record for `before-hook-creation` - Name: "build-config-1", - Namespace: "test", - }, - { - // This should be in the record for `before-hook-creation` - Name: "build-config-2", - Namespace: "test", - }, - { - // This should be in the record for cleaning up (the failure first) - Name: "build-config-2", - Namespace: "test", - }, - { - // This should be in the record for cleaning up (then the previously successful) - Name: "build-config-1", - Namespace: "test", - }, - }, true, + }, + { + // This should be in the record for cleaning up (the failure first) + Name: "build-config-2", + Namespace: "test", + }, + { + // This should be in the record for cleaning up (then the previously successful) + Name: "build-config-1", + Namespace: "test", + }, + }, true, }, } From 63b615316325ced44b55ceb7a01f9185c6b8600d Mon Sep 17 00:00:00 2001 From: Laszlo Uveges Date: Mon, 12 Sep 2022 17:45:43 +0200 Subject: [PATCH 7/8] More formatting Signed-off-by: Laszlo Uveges --- pkg/action/hooks_test.go | 44 ++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/pkg/action/hooks_test.go b/pkg/action/hooks_test.go index 9c279bc1a..3ef1c17cb 100644 --- a/pkg/action/hooks_test.go +++ b/pkg/action/hooks_test.go @@ -151,30 +151,30 @@ data: }, }, }, resource.Info{ - Name: "build-config-2", - Namespace: "test", - }, []resource.Info{ - { - // This should be in the record for `before-hook-creation` - Name: "build-config-1", - Namespace: "test", - }, - { - // This should be in the record for `before-hook-creation` - Name: "build-config-2", - Namespace: "test", - }, - { - // This should be in the record for cleaning up (the failure first) Name: "build-config-2", Namespace: "test", - }, - { - // This should be in the record for cleaning up (then the previously successful) - Name: "build-config-1", - Namespace: "test", - }, - }, true, + }, []resource.Info{ + { + // This should be in the record for `before-hook-creation` + Name: "build-config-1", + Namespace: "test", + }, + { + // This should be in the record for `before-hook-creation` + Name: "build-config-2", + Namespace: "test", + }, + { + // This should be in the record for cleaning up (the failure first) + Name: "build-config-2", + Namespace: "test", + }, + { + // This should be in the record for cleaning up (then the previously successful) + Name: "build-config-1", + Namespace: "test", + }, + }, true, }, } From aa9e4bb42dfc99b4d5144bbcf6f2cf9b2395ba2b Mon Sep 17 00:00:00 2001 From: Gerard Nguyen Date: Sat, 15 Mar 2025 09:11:45 +1100 Subject: [PATCH 8/8] rebase Signed-off-by: Gerard Nguyen --- pkg/action/hooks.go | 116 +++++++++++++++++++---- pkg/action/hooks_test.go | 197 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 288 insertions(+), 25 deletions(-) diff --git a/pkg/action/hooks.go b/pkg/action/hooks.go index 4fbe28bbe..d9c0eb085 100644 --- a/pkg/action/hooks.go +++ b/pkg/action/hooks.go @@ -17,13 +17,21 @@ package action import ( "bytes" + "fmt" + "log" + "slices" "sort" "time" + "helm.sh/helm/v4/pkg/kube" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/pkg/errors" + "gopkg.in/yaml.v3" - "helm.sh/helm/v3/pkg/release" - helmtime "helm.sh/helm/v3/pkg/time" + release "helm.sh/helm/v4/pkg/release/v1" + helmtime "helm.sh/helm/v4/pkg/time" ) // execHook executes all of the hooks for the given hook event. @@ -43,7 +51,7 @@ func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent, for i, h := range executingHooks { // Set default delete policy to before-hook-creation - if h.DeletePolicies == nil || len(h.DeletePolicies) == 0 { + if len(h.DeletePolicies) == 0 { // TODO(jlegrone): Only apply before-hook-creation delete policy to run to completion // resources. For all other resource types update in place if a // resource with the same name already exists and is owned by the @@ -51,7 +59,7 @@ func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent, h.DeletePolicies = []release.HookDeletePolicy{release.HookBeforeHookCreation} } - if err := cfg.deleteHookByPolicy(h, release.HookBeforeHookCreation); err != nil { + if err := cfg.deleteHookByPolicy(h, release.HookBeforeHookCreation, timeout); err != nil { return err } @@ -86,15 +94,21 @@ func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent, // Mark hook as succeeded or failed if err != nil { h.LastRun.Phase = release.HookPhaseFailed + // If a hook is failed, check the annotation of the hook to determine if we should copy the logs client side + if errOutputting := cfg.outputLogsByPolicy(h, rl.Namespace, release.HookOutputOnFailed); errOutputting != nil { + // We log the error here as we want to propagate the hook failure upwards to the release object. + log.Printf("error outputting logs for hook failure: %v", errOutputting) + } // If a hook is failed, check the annotation of the hook to determine whether the hook should be deleted // under failed condition. If so, then clear the corresponding resource object in the hook - if err := cfg.deleteHookByPolicy(h, release.HookFailed); err != nil { - return err + if errDeleting := cfg.deleteHookByPolicy(h, release.HookFailed, timeout); errDeleting != nil { + // We log the error here as we want to propagate the hook failure upwards to the release object. + log.Printf("error deleting the hook resource on hook failure: %v", errDeleting) } - // If a hook is failed, check the annotation of the previous successful hooks to determine whether the hook + // If a hook is failed, check the annotation of the previous successful hooks to determine whether the hooks // should be deleted under succeeded condition. - if err := cfg.deleteHooksByPolicy(executingHooks[0:i], release.HookSucceeded); err != nil { + if err := cfg.deleteHooksByPolicy(executingHooks[0:i], release.HookSucceeded, timeout); err != nil { return err } @@ -104,9 +118,16 @@ func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent, } // If all hooks are successful, check the annotation of each hook to determine whether the hook should be deleted - // under succeeded condition. If so, then clear the corresponding resource object in each hook - if err := cfg.deleteHooksByPolicy(executingHooks, release.HookSucceeded); err != nil { - return err + // or output should be logged under succeeded condition. If so, then clear the corresponding resource object in each hook + for i := len(executingHooks) - 1; i >= 0; i-- { + h := executingHooks[i] + if err := cfg.outputLogsByPolicy(h, rl.Namespace, release.HookOutputOnSucceeded); err != nil { + // We log here as we still want to attempt hook resource deletion even if output logging fails. + log.Printf("error outputting logs for hook failure: %v", err) + } + if err := cfg.deleteHookByPolicy(h, release.HookSucceeded, timeout); err != nil { + return err + } } return nil @@ -125,7 +146,7 @@ func (x hookByWeight) Less(i, j int) bool { } // deleteHookByPolicy deletes a hook if the hook policy instructs it to -func (cfg *Configuration) deleteHookByPolicy(h *release.Hook, policy release.HookDeletePolicy) error { +func (cfg *Configuration) deleteHookByPolicy(h *release.Hook, policy release.HookDeletePolicy, timeout time.Duration) error { // Never delete CustomResourceDefinitions; this could cause lots of // cascading garbage collection. if h.Kind == "CustomResourceDefinition" { @@ -140,7 +161,25 @@ func (cfg *Configuration) deleteHookByPolicy(h *release.Hook, policy release.Hoo if len(errs) > 0 { return errors.New(joinErrors(errs)) } + + // wait for resources until they are deleted to avoid conflicts + if kubeClient, ok := cfg.KubeClient.(kube.InterfaceExt); ok { + if err := kubeClient.WaitForDelete(resources, timeout); err != nil { + return err + } + } + } + return nil +} + +// deleteHooksByPolicy deletes all hooks if the hook policy instructs it to +func (cfg *Configuration) deleteHooksByPolicy(hooks []*release.Hook, policy release.HookDeletePolicy, timeout time.Duration) error { + for _, h := range hooks { + if err := cfg.deleteHookByPolicy(h, policy, timeout); err != nil { + return err + } } + return nil } @@ -155,13 +194,56 @@ func hookHasDeletePolicy(h *release.Hook, policy release.HookDeletePolicy) bool return false } -// deleteHooksByPolicy deletes all hooks if the hook policy instructs it to -func (cfg *Configuration) deleteHooksByPolicy(hooks []*release.Hook, policy release.HookDeletePolicy) error { - for _, h := range hooks { - if err := cfg.deleteHookByPolicy(h, policy); err != nil { +// outputLogsByPolicy outputs a pods logs if the hook policy instructs it to +func (cfg *Configuration) outputLogsByPolicy(h *release.Hook, releaseNamespace string, policy release.HookOutputLogPolicy) error { + if !hookHasOutputLogPolicy(h, policy) { + return nil + } + namespace, err := cfg.deriveNamespace(h, releaseNamespace) + if err != nil { + return err + } + switch h.Kind { + case "Job": + return cfg.outputContainerLogsForListOptions(namespace, metav1.ListOptions{LabelSelector: fmt.Sprintf("job-name=%s", h.Name)}) + case "Pod": + return cfg.outputContainerLogsForListOptions(namespace, metav1.ListOptions{FieldSelector: fmt.Sprintf("metadata.name=%s", h.Name)}) + default: + return nil + } +} + +func (cfg *Configuration) outputContainerLogsForListOptions(namespace string, listOptions metav1.ListOptions) error { + // TODO Helm 4: Remove this check when GetPodList and OutputContainerLogsForPodList are moved from InterfaceLogs to Interface + if kubeClient, ok := cfg.KubeClient.(kube.InterfaceLogs); ok { + podList, err := kubeClient.GetPodList(namespace, listOptions) + if err != nil { return err } + err = kubeClient.OutputContainerLogsForPodList(podList, namespace, cfg.HookOutputFunc) + return err } - return nil } + +func (cfg *Configuration) deriveNamespace(h *release.Hook, namespace string) (string, error) { + tmp := struct { + Metadata struct { + Namespace string + } + }{} + err := yaml.Unmarshal([]byte(h.Manifest), &tmp) + if err != nil { + return "", errors.Wrapf(err, "unable to parse metadata.namespace from kubernetes manifest for output logs hook %s", h.Path) + } + if tmp.Metadata.Namespace == "" { + return namespace, nil + } + return tmp.Metadata.Namespace, nil +} + +// hookHasOutputLogPolicy determines whether the defined hook output log policy matches the hook output log policies +// supported by helm. +func hookHasOutputLogPolicy(h *release.Hook, policy release.HookOutputLogPolicy) bool { + return slices.Contains(h.OutputLogPolicies, policy) +} diff --git a/pkg/action/hooks_test.go b/pkg/action/hooks_test.go index 3ef1c17cb..68379add8 100644 --- a/pkg/action/hooks_test.go +++ b/pkg/action/hooks_test.go @@ -17,24 +17,205 @@ limitations under the License. package action import ( + "bytes" + "fmt" "io" - "io/ioutil" "reflect" "testing" "time" + "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/yaml" "k8s.io/cli-runtime/pkg/resource" - "helm.sh/helm/v3/pkg/chartutil" - "helm.sh/helm/v3/pkg/kube" - kubefake "helm.sh/helm/v3/pkg/kube/fake" - "helm.sh/helm/v3/pkg/release" - "helm.sh/helm/v3/pkg/storage" - "helm.sh/helm/v3/pkg/storage/driver" + chart "helm.sh/helm/v4/pkg/chart/v2" + chartutil "helm.sh/helm/v4/pkg/chart/v2/util" + "helm.sh/helm/v4/pkg/kube" + kubefake "helm.sh/helm/v4/pkg/kube/fake" + release "helm.sh/helm/v4/pkg/release/v1" + "helm.sh/helm/v4/pkg/storage" + "helm.sh/helm/v4/pkg/storage/driver" ) +func podManifestWithOutputLogs(hookDefinitions []release.HookOutputLogPolicy) string { + hookDefinitionString := convertHooksToCommaSeparated(hookDefinitions) + return fmt.Sprintf(`kind: Pod +metadata: + name: finding-sharky, + annotations: + "helm.sh/hook": pre-install + "helm.sh/hook-output-log-policy": %s +spec: + containers: + - name: sharky-test + image: fake-image + cmd: fake-command`, hookDefinitionString) +} + +func podManifestWithOutputLogWithNamespace(hookDefinitions []release.HookOutputLogPolicy) string { + hookDefinitionString := convertHooksToCommaSeparated(hookDefinitions) + return fmt.Sprintf(`kind: Pod +metadata: + name: finding-george + namespace: sneaky-namespace + annotations: + "helm.sh/hook": pre-install + "helm.sh/hook-output-log-policy": %s +spec: + containers: + - name: george-test + image: fake-image + cmd: fake-command`, hookDefinitionString) +} + +func jobManifestWithOutputLog(hookDefinitions []release.HookOutputLogPolicy) string { + hookDefinitionString := convertHooksToCommaSeparated(hookDefinitions) + return fmt.Sprintf(`kind: Job +apiVersion: batch/v1 +metadata: + name: losing-religion + annotations: + "helm.sh/hook": pre-install + "helm.sh/hook-output-log-policy": %s +spec: + completions: 1 + parallelism: 1 + activeDeadlineSeconds: 30 + template: + spec: + containers: + - name: religion-container + image: religion-image + cmd: religion-command`, hookDefinitionString) +} + +func jobManifestWithOutputLogWithNamespace(hookDefinitions []release.HookOutputLogPolicy) string { + hookDefinitionString := convertHooksToCommaSeparated(hookDefinitions) + return fmt.Sprintf(`kind: Job +apiVersion: batch/v1 +metadata: + name: losing-religion + namespace: rem-namespace + annotations: + "helm.sh/hook": pre-install + "helm.sh/hook-output-log-policy": %s +spec: + completions: 1 + parallelism: 1 + activeDeadlineSeconds: 30 + template: + spec: + containers: + - name: religion-container + image: religion-image + cmd: religion-command`, hookDefinitionString) +} + +func convertHooksToCommaSeparated(hookDefinitions []release.HookOutputLogPolicy) string { + var commaSeparated string + for i, policy := range hookDefinitions { + if i+1 == len(hookDefinitions) { + commaSeparated += policy.String() + } else { + commaSeparated += policy.String() + "," + } + } + return commaSeparated +} + +func TestInstallRelease_HookOutputLogsOnFailure(t *testing.T) { + // Should output on failure with expected namespace if hook-failed is set + runInstallForHooksWithFailure(t, podManifestWithOutputLogs([]release.HookOutputLogPolicy{release.HookOutputOnFailed}), "spaced", true) + runInstallForHooksWithFailure(t, podManifestWithOutputLogWithNamespace([]release.HookOutputLogPolicy{release.HookOutputOnFailed}), "sneaky-namespace", true) + runInstallForHooksWithFailure(t, jobManifestWithOutputLog([]release.HookOutputLogPolicy{release.HookOutputOnFailed}), "spaced", true) + runInstallForHooksWithFailure(t, jobManifestWithOutputLogWithNamespace([]release.HookOutputLogPolicy{release.HookOutputOnFailed}), "rem-namespace", true) + + // Should not output on failure with expected namespace if hook-succeed is set + runInstallForHooksWithFailure(t, podManifestWithOutputLogs([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded}), "", false) + runInstallForHooksWithFailure(t, podManifestWithOutputLogWithNamespace([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded}), "", false) + runInstallForHooksWithFailure(t, jobManifestWithOutputLog([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded}), "", false) + runInstallForHooksWithFailure(t, jobManifestWithOutputLogWithNamespace([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded}), "", false) +} + +func TestInstallRelease_HookOutputLogsOnSuccess(t *testing.T) { + // Should output on success with expected namespace if hook-succeeded is set + runInstallForHooksWithSuccess(t, podManifestWithOutputLogs([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded}), "spaced", true) + runInstallForHooksWithSuccess(t, podManifestWithOutputLogWithNamespace([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded}), "sneaky-namespace", true) + runInstallForHooksWithSuccess(t, jobManifestWithOutputLog([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded}), "spaced", true) + runInstallForHooksWithSuccess(t, jobManifestWithOutputLogWithNamespace([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded}), "rem-namespace", true) + + // Should not output on success if hook-failed is set + runInstallForHooksWithSuccess(t, podManifestWithOutputLogs([]release.HookOutputLogPolicy{release.HookOutputOnFailed}), "", false) + runInstallForHooksWithSuccess(t, podManifestWithOutputLogWithNamespace([]release.HookOutputLogPolicy{release.HookOutputOnFailed}), "", false) + runInstallForHooksWithSuccess(t, jobManifestWithOutputLog([]release.HookOutputLogPolicy{release.HookOutputOnFailed}), "", false) + runInstallForHooksWithSuccess(t, jobManifestWithOutputLogWithNamespace([]release.HookOutputLogPolicy{release.HookOutputOnFailed}), "", false) +} + +func TestInstallRelease_HooksOutputLogsOnSuccessAndFailure(t *testing.T) { + // Should output on success with expected namespace if hook-succeeded and hook-failed is set + runInstallForHooksWithSuccess(t, podManifestWithOutputLogs([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded, release.HookOutputOnFailed}), "spaced", true) + runInstallForHooksWithSuccess(t, podManifestWithOutputLogWithNamespace([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded, release.HookOutputOnFailed}), "sneaky-namespace", true) + runInstallForHooksWithSuccess(t, jobManifestWithOutputLog([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded, release.HookOutputOnFailed}), "spaced", true) + runInstallForHooksWithSuccess(t, jobManifestWithOutputLogWithNamespace([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded, release.HookOutputOnFailed}), "rem-namespace", true) + + // Should output on failure if hook-succeeded and hook-failed is set + runInstallForHooksWithFailure(t, podManifestWithOutputLogs([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded, release.HookOutputOnFailed}), "spaced", true) + runInstallForHooksWithFailure(t, podManifestWithOutputLogWithNamespace([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded, release.HookOutputOnFailed}), "sneaky-namespace", true) + runInstallForHooksWithFailure(t, jobManifestWithOutputLog([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded, release.HookOutputOnFailed}), "spaced", true) + runInstallForHooksWithFailure(t, jobManifestWithOutputLogWithNamespace([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded, release.HookOutputOnFailed}), "rem-namespace", true) +} + +func runInstallForHooksWithSuccess(t *testing.T, manifest, expectedNamespace string, shouldOutput bool) { + var expectedOutput string + if shouldOutput { + expectedOutput = fmt.Sprintf("attempted to output logs for namespace: %s", expectedNamespace) + } + is := assert.New(t) + instAction := installAction(t) + instAction.ReleaseName = "failed-hooks" + outBuffer := &bytes.Buffer{} + instAction.cfg.KubeClient = &kubefake.PrintingKubeClient{Out: io.Discard, LogOutput: outBuffer} + + templates := []*chart.File{ + {Name: "templates/hello", Data: []byte("hello: world")}, + {Name: "templates/hooks", Data: []byte(manifest)}, + } + vals := map[string]interface{}{} + + res, err := instAction.Run(buildChartWithTemplates(templates), vals) + is.NoError(err) + is.Equal(expectedOutput, outBuffer.String()) + is.Equal(release.StatusDeployed, res.Info.Status) +} + +func runInstallForHooksWithFailure(t *testing.T, manifest, expectedNamespace string, shouldOutput bool) { + var expectedOutput string + if shouldOutput { + expectedOutput = fmt.Sprintf("attempted to output logs for namespace: %s", expectedNamespace) + } + is := assert.New(t) + instAction := installAction(t) + instAction.ReleaseName = "failed-hooks" + failingClient := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient) + failingClient.WatchUntilReadyError = fmt.Errorf("failed watch") + instAction.cfg.KubeClient = failingClient + outBuffer := &bytes.Buffer{} + failingClient.PrintingKubeClient = kubefake.PrintingKubeClient{Out: io.Discard, LogOutput: outBuffer} + + templates := []*chart.File{ + {Name: "templates/hello", Data: []byte("hello: world")}, + {Name: "templates/hooks", Data: []byte(manifest)}, + } + vals := map[string]interface{}{} + + res, err := instAction.Run(buildChartWithTemplates(templates), vals) + is.Error(err) + is.Contains(res.Info.Description, "failed pre-install") + is.Equal(expectedOutput, outBuffer.String()) + is.Equal(release.StatusFailed, res.Info.Status) +} + type HookFailedError struct{} func (e *HookFailedError) Error() string { @@ -181,7 +362,7 @@ data: for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { kubeClient := &HookFailingKubeClient{ - kubefake.PrintingKubeClient{Out: ioutil.Discard}, tc.failOn, []resource.Info{}, + kubefake.PrintingKubeClient{Out: io.Discard}, tc.failOn, []resource.Info{}, } configuration := &Configuration{