diff --git a/pkg/action/uninstall.go b/pkg/action/uninstall.go index 4d2c728db..c36bf780f 100644 --- a/pkg/action/uninstall.go +++ b/pkg/action/uninstall.go @@ -193,14 +193,15 @@ func (u *Uninstall) Run(name string) (*releasei.UninstallReleaseResponse, error) } u.cfg.Logger().Debug("uninstall: deleting release", "name", name) - rel.Info.Status = common.StatusUninstalling - rel.Info.Deleted = time.Now() - rel.Info.Description = "Deletion in progress (or silently failed)" res := &releasei.UninstallReleaseResponse{Release: rel} if !u.DisableHooks { serverSideApply := true if err := u.cfg.execHook(rel, release.HookPreDelete, u.WaitStrategy, u.WaitOptions, u.Timeout, serverSideApply); err != nil { + // Persist the failed hook result without hiding the release from the + // deployed release list. This leaves both retrying the uninstall and + // upgrading the release available to the user. + u.cfg.recordRelease(rel) return res, err } } else { @@ -209,6 +210,9 @@ func (u *Uninstall) Run(name string) (*releasei.UninstallReleaseResponse, error) // From here on out, the release is currently considered to be in StatusUninstalling // state. + rel.Info.Status = common.StatusUninstalling + rel.Info.Deleted = time.Now() + rel.Info.Description = "Deletion in progress (or silently failed)" if err := u.cfg.Releases.Update(rel); err != nil { u.cfg.Logger().Debug("uninstall: Failed to store updated release", slog.Any("error", err)) } diff --git a/pkg/action/uninstall_test.go b/pkg/action/uninstall_test.go index e306935c7..aa86cfd89 100644 --- a/pkg/action/uninstall_test.go +++ b/pkg/action/uninstall_test.go @@ -29,6 +29,7 @@ import ( "helm.sh/helm/v4/pkg/kube" kubefake "helm.sh/helm/v4/pkg/kube/fake" "helm.sh/helm/v4/pkg/release/common" + release "helm.sh/helm/v4/pkg/release/v1" ) func uninstallAction(t *testing.T) *Uninstall { @@ -167,6 +168,26 @@ func TestUninstallRelease_Cascade(t *testing.T) { is.ErrorContains(err, "failed to delete release: come-fail-away") } +func TestUninstallRelease_PreDeleteHookFailureKeepsReleaseDeployed(t *testing.T) { + unAction := uninstallAction(t) + rel := releaseStub() + require.NoError(t, unAction.cfg.Releases.Create(rel)) + + failer := unAction.cfg.KubeClient.(*kubefake.FailingKubeClient) + failer.CreateError = errors.New("pre-delete hook failed") + + res, err := unAction.Run(rel.Name) + require.ErrorContains(t, err, "pre-delete hook failed") + require.NotNil(t, res) + + stored, err := unAction.cfg.Releases.Get(rel.Name, rel.Version) + require.NoError(t, err) + storedRelease, err := releaserToV1Release(stored) + require.NoError(t, err) + assert.Equal(t, common.StatusDeployed, storedRelease.Info.Status) + assert.Equal(t, release.HookPhaseFailed, storedRelease.Hooks[0].LastRun.Phase) +} + func TestUninstallRun_UnreachableKubeClient(t *testing.T) { t.Helper() config := actionConfigFixture(t)