diff --git a/pkg/action/action_test.go b/pkg/action/action_test.go index c4ef6c056..28ec80dfb 100644 --- a/pkg/action/action_test.go +++ b/pkg/action/action_test.go @@ -64,6 +64,30 @@ metadata: data: name: value` +var manifestWithPreInstallHook = `kind: ConfigMap +metadata: + name: test-cm-2 + annotations: + "helm.sh/hook": pre-install +data: + name: value` + +var manifestWithPreUpgradeHook = `kind: ConfigMap +metadata: + name: test-cm-2 + annotations: + "helm.sh/hook": pre-upgrade +data: + name: value` + +var manifestWithPreDeleteHook = `kind: ConfigMap + metadata: + name: test-cm-2 + annotations: + "helm.sh/hook": pre-delete + data: + name: value` + var manifestWithTestHook = `kind: Pod metadata: name: finding-nemo, @@ -182,6 +206,36 @@ func withMetadataDependency(dependency chart.Dependency) chartOption { } } +func withPreInstallHook() chartOption { + return func(opts *chartOptions) { + hookTemplates := []*chart.File{ + // This adds pre-install hook. + {Name: "templates/pre-install-hook", Data: []byte(manifestWithPreInstallHook)}, + } + opts.Templates = hookTemplates + } +} + +func withPreUpgradeHook() chartOption { + return func(opts *chartOptions) { + hookTemplates := []*chart.File{ + // This adds pre-install hook. + {Name: "templates/pre-upgrade-hook", Data: []byte(manifestWithPreUpgradeHook)}, + } + opts.Templates = hookTemplates + } +} + +func withPreDeleteHook() chartOption { + return func(opts *chartOptions) { + hookTemplates := []*chart.File{ + // This adds pre-install hook. + {Name: "templates/pre-delete-hook", Data: []byte(manifestWithPreDeleteHook)}, + } + opts.Templates = hookTemplates + } +} + func withSampleTemplates() chartOption { return func(opts *chartOptions) { sampleTemplates := []*chart.File{ diff --git a/pkg/action/install_test.go b/pkg/action/install_test.go index bc0890115..0fca8a456 100644 --- a/pkg/action/install_test.go +++ b/pkg/action/install_test.go @@ -322,6 +322,27 @@ func TestInstallRelease_FailedHooks(t *testing.T) { is.Equal(release.StatusFailed, res.Info.Status) } +func TestInstallRelease_FailedTimeout(t *testing.T) { + is := assert.New(t) + instAction := installAction(t) + instAction.ReleaseName = "failed-timeout" + instAction.Wait = true + instAction.WaitForJobs = true + instAction.DisableHooks = true + instAction.Timeout = 5 * time.Second + failer := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient) + failer.WaitError = fmt.Errorf("I timed out") + instAction.cfg.KubeClient = failer + + vals := map[string]interface{}{} + start := time.Now() + res, err := instAction.Run(buildChart(), vals) + is.Equal(instAction.Timeout, time.Since(start).Round(time.Second)) + is.Error(err) + is.Contains(res.Info.Description, "failed: I timed out") + is.Equal(release.StatusFailed, res.Info.Status) +} + func TestInstallRelease_ReplaceRelease(t *testing.T) { is := assert.New(t) instAction := installAction(t) @@ -404,6 +425,56 @@ func TestInstallRelease_Wait_Interrupted(t *testing.T) { time.Sleep(10 * time.Second) // wait for goroutine to finish is.Equal(goroutines, runtime.NumGoroutine()) } + +func TestInstallRelease_Wait_Interrupted_Hooks(t *testing.T) { + is := assert.New(t) + instAction := installAction(t) + instAction.ReleaseName = "interrupted-hooks-release" + failer := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient) + failer.WatchUntilReadyError = fmt.Errorf("I timed out") + instAction.cfg.KubeClient = failer + instAction.Wait = true + instAction.HookTimeout = 5 * time.Second + instAction.Timeout = 3 * time.Second + vals := map[string]interface{}{} + + start := time.Now() + res, err := instAction.Run(buildChart(withPreInstallHook()), vals) + executionTime := time.Since(start) + is.Equal(instAction.HookTimeout, executionTime.Round(time.Second)) + is.Error(err) + is.Contains(res.Info.Description, "Release \"interrupted-hooks-release\" failed: failed pre-install: I timed out") + is.Equal(res.Info.Status, release.StatusFailed) + + is.Len(res.Hooks, 1) + is.Equal(res.Hooks[0].Manifest, manifestWithPreInstallHook) + is.Equal(res.Hooks[0].Events[0], release.HookPreInstall) +} + +func TestInstallRelease_Wait_TimeoutUsedWhenHookTimeoutNotSet(t *testing.T) { + is := assert.New(t) + instAction := installAction(t) + instAction.ReleaseName = "interrupted-hooks-release" + failer := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient) + failer.WatchUntilReadyError = fmt.Errorf("I timed out") + instAction.cfg.KubeClient = failer + instAction.Wait = true + instAction.Timeout = 3 * time.Second + vals := map[string]interface{}{} + + start := time.Now() + res, err := instAction.Run(buildChart(withPreInstallHook()), vals) + executionTime := time.Since(start) + is.Equal(instAction.Timeout, executionTime.Round(time.Second)) + is.Error(err) + is.Contains(res.Info.Description, "Release \"interrupted-hooks-release\" failed: failed pre-install: I timed out") + is.Equal(res.Info.Status, release.StatusFailed) + + is.Len(res.Hooks, 1) + is.Equal(res.Hooks[0].Manifest, manifestWithPreInstallHook) + is.Equal(res.Hooks[0].Events[0], release.HookPreInstall) +} + func TestInstallRelease_WaitForJobs(t *testing.T) { is := assert.New(t) instAction := installAction(t) diff --git a/pkg/action/uninstall_test.go b/pkg/action/uninstall_test.go index 869ffb8c7..b346c4ea0 100644 --- a/pkg/action/uninstall_test.go +++ b/pkg/action/uninstall_test.go @@ -19,6 +19,7 @@ package action import ( "fmt" "testing" + "time" "github.com/stretchr/testify/assert" @@ -138,3 +139,50 @@ func TestUninstallRelease_Cascade(t *testing.T) { is.Error(err) is.Contains(err.Error(), "failed to delete release: come-fail-away") } + +func TestUninstallRelease_Wait_Interrupted_Hooks(t *testing.T) { + is := assert.New(t) + unAction := uninstallAction(t) + rel := releaseStub() + rel.Name = "interrupted-hooks-release" + rel.Info.Status = release.StatusDeployed + rel.Chart = buildChart(withPreDeleteHook()) + unAction.cfg.Releases.Create(rel) + failer := unAction.cfg.KubeClient.(*kubefake.FailingKubeClient) + failer.WatchUntilReadyError = fmt.Errorf("I timed out") + unAction.cfg.KubeClient = failer + unAction.Wait = true + unAction.HookTimeout = 5 * time.Second + unAction.Timeout = 3 * time.Second + + start := time.Now() + res, err := unAction.Run(rel.Name) + executionTime := time.Since(start) + is.Equal(unAction.HookTimeout, executionTime.Round(time.Second)) + is.Error(err) + is.Contains(res.Release.Info.Description, "Deletion in progress (or silently failed)") + is.Equal(res.Release.Info.Status, release.StatusUninstalling) +} + +func TestUninstallRelease_Wait_TimeoutUsedWhenHookTimeoutNotSet(t *testing.T) { + is := assert.New(t) + unAction := uninstallAction(t) + rel := releaseStub() + rel.Name = "interrupted-hooks-release" + rel.Info.Status = release.StatusDeployed + rel.Chart = buildChart(withPreDeleteHook()) + unAction.cfg.Releases.Create(rel) + failer := unAction.cfg.KubeClient.(*kubefake.FailingKubeClient) + failer.WatchUntilReadyError = fmt.Errorf("I timed out") + unAction.cfg.KubeClient = failer + unAction.Wait = true + unAction.Timeout = 3 * time.Second + + start := time.Now() + res, err := unAction.Run(rel.Name) + executionTime := time.Since(start) + is.Equal(unAction.Timeout, executionTime.Round(time.Second)) + is.Error(err) + is.Contains(res.Release.Info.Description, "Deletion in progress (or silently failed)") + is.Equal(res.Release.Info.Status, release.StatusUninstalling) +} diff --git a/pkg/action/upgrade_test.go b/pkg/action/upgrade_test.go index 77656e1c5..694530ea7 100644 --- a/pkg/action/upgrade_test.go +++ b/pkg/action/upgrade_test.go @@ -114,6 +114,30 @@ func TestUpgradeRelease_WaitForJobs(t *testing.T) { is.Equal(res.Info.Status, release.StatusFailed) } +func TestUpgradeRelease_FailedTimeout(t *testing.T) { + is := assert.New(t) + upAction := upgradeAction(t) + rel := releaseStub() + rel.Name = "failed-timeout" + rel.Info.Status = release.StatusDeployed + upAction.cfg.Releases.Create(rel) + upAction.Wait = true + upAction.WaitForJobs = true + upAction.DisableHooks = true + upAction.Timeout = 5 * time.Second + failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient) + failer.WaitError = fmt.Errorf("I timed out") + upAction.cfg.KubeClient = failer + + vals := map[string]interface{}{} + start := time.Now() + res, err := upAction.Run(rel.Name, buildChart(), vals) + is.Equal(upAction.Timeout, time.Since(start).Round(time.Second)) + is.Error(err) + is.Contains(res.Info.Description, "failed: I timed out") + is.Equal(release.StatusFailed, res.Info.Status) +} + func TestUpgradeRelease_CleanupOnFail(t *testing.T) { is := assert.New(t) req := require.New(t) @@ -357,6 +381,61 @@ func TestUpgradeRelease_Interrupted_Wait(t *testing.T) { } +func TestUpgradeRelease_Wait_Interrupted_Hooks(t *testing.T) { + is := assert.New(t) + upAction := upgradeAction(t) + rel := releaseStub() + rel.Name = "interrupted-hooks-release" + rel.Info.Status = release.StatusDeployed + upAction.cfg.Releases.Create(rel) + failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient) + failer.WatchUntilReadyError = fmt.Errorf("I timed out") + upAction.cfg.KubeClient = failer + upAction.Wait = true + upAction.HookTimeout = 5 * time.Second + upAction.Timeout = 3 * time.Second + vals := map[string]interface{}{} + + start := time.Now() + res, err := upAction.Run(rel.Name, buildChart(withPreUpgradeHook()), vals) + executionTime := time.Since(start) + is.Equal(upAction.HookTimeout, executionTime.Round(time.Second)) + is.Error(err) + is.Contains(res.Info.Description, "Upgrade \"interrupted-hooks-release\" failed: pre-upgrade hooks failed: I timed out") + is.Equal(res.Info.Status, release.StatusFailed) + + is.Len(res.Hooks, 1) + is.Equal(res.Hooks[0].Manifest, manifestWithPreUpgradeHook) + is.Equal(res.Hooks[0].Events[0], release.HookPreUpgrade) +} + +func TestUpgradeRelease_Wait_TimeoutUsedWhenHookTimeoutNotSet(t *testing.T) { + is := assert.New(t) + upAction := upgradeAction(t) + rel := releaseStub() + rel.Name = "interrupted-hooks-release" + rel.Info.Status = release.StatusDeployed + upAction.cfg.Releases.Create(rel) + failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient) + failer.WatchUntilReadyError = fmt.Errorf("I timed out") + upAction.cfg.KubeClient = failer + upAction.Wait = true + upAction.Timeout = 3 * time.Second + vals := map[string]interface{}{} + + start := time.Now() + res, err := upAction.Run(rel.Name, buildChart(withPreUpgradeHook()), vals) + executionTime := time.Since(start) + is.Equal(upAction.Timeout, executionTime.Round(time.Second)) + is.Error(err) + is.Contains(res.Info.Description, "Upgrade \"interrupted-hooks-release\" failed: pre-upgrade hooks failed: I timed out") + is.Equal(res.Info.Status, release.StatusFailed) + + is.Len(res.Hooks, 1) + is.Equal(res.Hooks[0].Manifest, manifestWithPreUpgradeHook) + is.Equal(res.Hooks[0].Events[0], release.HookPreUpgrade) +} + func TestUpgradeRelease_Interrupted_Atomic(t *testing.T) { is := assert.New(t) diff --git a/pkg/kube/fake/fake.go b/pkg/kube/fake/fake.go index 267020d57..5e41311ad 100644 --- a/pkg/kube/fake/fake.go +++ b/pkg/kube/fake/fake.go @@ -67,7 +67,11 @@ func (f *FailingKubeClient) Get(resources kube.ResourceList, related bool) (map[ // Waits the amount of time defined on f.WaitDuration, then returns the configured error if set or prints. func (f *FailingKubeClient) Wait(resources kube.ResourceList, d time.Duration) error { - time.Sleep(f.WaitDuration) + if f.WaitDuration != 0*time.Second { + time.Sleep(f.WaitDuration) + } else { + time.Sleep(d) + } if f.WaitError != nil { return f.WaitError } @@ -76,6 +80,11 @@ func (f *FailingKubeClient) Wait(resources kube.ResourceList, d time.Duration) e // WaitWithJobs returns the configured error if set or prints func (f *FailingKubeClient) WaitWithJobs(resources kube.ResourceList, d time.Duration) error { + if f.WaitDuration != 0*time.Second { + time.Sleep(f.WaitDuration) + } else { + time.Sleep(d) + } if f.WaitError != nil { return f.WaitError } @@ -98,8 +107,9 @@ func (f *FailingKubeClient) Delete(resources kube.ResourceList) (*kube.Result, [ return f.PrintingKubeClient.Delete(resources) } -// WatchUntilReady returns the configured error if set or prints +// WatchUntilReady waits for the duration provided and returns the configured error if set or prints func (f *FailingKubeClient) WatchUntilReady(resources kube.ResourceList, d time.Duration) error { + time.Sleep(d) if f.WatchUntilReadyError != nil { return f.WatchUntilReadyError }