package sdk import ( "time" "github.com/pkg/errors" "github.com/portainer/portainer/pkg/libhelm/options" "github.com/rs/zerolog/log" "helm.sh/helm/v4/pkg/action" "helm.sh/helm/v4/pkg/kube" "helm.sh/helm/v4/pkg/storage/driver" ) // Uninstall implements the HelmPackageManager interface by using the Helm SDK to uninstall a release. func (hspm *HelmSDKPackageManager) Uninstall(uninstallOpts options.UninstallOptions) error { if uninstallOpts.Name == "" { log.Error(). Str("context", "HelmClient"). Msg("Release name is required") return errors.New("release name is required") } log.Debug(). Str("context", "HelmClient"). Str("release", uninstallOpts.Name). Str("namespace", uninstallOpts.Namespace). Msg("Uninstalling Helm release") // Initialize action configuration with kubernetes config actionConfig := new(action.Configuration) err := hspm.initActionConfig(actionConfig, uninstallOpts.Namespace, uninstallOpts.KubernetesClusterAccess) if err != nil { // error is already logged in initActionConfig return errors.Wrap(err, "failed to initialize helm configuration") } // Create uninstallClient action uninstallClient := action.NewUninstall(actionConfig) // 'foreground' means the parent object remains in a "terminating" state until all of its children are deleted. This ensures that all dependent resources are completely removed before finalizing the deletion of the parent resource. uninstallClient.DeletionPropagation = "foreground" // "background" or "orphan" if uninstallOpts.Wait { uninstallClient.WaitStrategy = kube.StatusWatcherStrategy } else { uninstallClient.WaitStrategy = kube.HookOnlyStrategy } if uninstallOpts.Timeout == 0 { uninstallClient.Timeout = 15 * time.Minute } else { uninstallClient.Timeout = uninstallOpts.Timeout } // Run the uninstallation log.Info(). Str("context", "HelmClient"). Str("release", uninstallOpts.Name). Str("namespace", uninstallOpts.Namespace). Msg("Running uninstallation") result, err := uninstallClient.Run(uninstallOpts.Name) if err != nil { log.Error(). Str("context", "HelmClient"). Str("release", uninstallOpts.Name). Str("namespace", uninstallOpts.Namespace). Err(err). Msg("Failed to uninstall helm release") return errors.Wrap(err, "failed to uninstall helm release") } if result != nil { releaseV1, err := releaserToV1Release(result.Release) if err != nil { log.Error(). Str("context", "HelmClient"). Str("release", uninstallOpts.Name). Err(err). Msg("Failed to convert release to v1") return errors.Wrap(err, "failed to convert release to v1") } log.Debug(). Str("context", "HelmClient"). Str("release", uninstallOpts.Name). Str("release_info", releaseV1.Info.Description). Msg("Uninstall result details") } return nil } // ForceRemoveRelease removes all release history (Helm secrets) without attempting // to delete Kubernetes resources. This is a last-resort recovery mechanism for when // a standard Uninstall fails because CRDs are missing and Helm can't build kubernetes // objects for deletion, leaving the release stuck with no way to recover. func (hspm *HelmSDKPackageManager) ForceRemoveRelease(uninstallOpts options.UninstallOptions) error { if uninstallOpts.Name == "" { return errors.New("release name is required") } log.Warn(). Str("context", "HelmClient"). Str("release", uninstallOpts.Name). Str("namespace", uninstallOpts.Namespace). Msg("Force-removing release history (skipping resource deletion)") actionConfig := new(action.Configuration) err := hspm.initActionConfig(actionConfig, uninstallOpts.Namespace, uninstallOpts.KubernetesClusterAccess) if err != nil { return errors.Wrap(err, "failed to initialize helm configuration for force-remove") } // Get all release versions from Helm's storage (Kubernetes secrets) versions, err := actionConfig.Releases.History(uninstallOpts.Name) if err != nil { if errors.Is(err, driver.ErrReleaseNotFound) { log.Debug(). Str("context", "HelmClient"). Str("release", uninstallOpts.Name). Msg("Release not found in storage, nothing to force-remove") return nil } return errors.Wrap(err, "failed to get release history for force-remove") } // Delete each release version from storage for _, v := range versions { releaseV1, err := releaserToV1Release(v) if err != nil { log.Error(). Str("context", "HelmClient"). Str("release", uninstallOpts.Name). Int("version", releaseV1.Version). Err(err). Msg("Failed to convert releaser version to v1 for force-remove, skipping deletion of this version") continue } if _, err := actionConfig.Releases.Delete(releaseV1.Name, releaseV1.Version); err != nil { return errors.Wrapf(err, "failed to delete release version %d for force-remove", releaseV1.Version) } } log.Info(). Str("context", "HelmClient"). Str("release", uninstallOpts.Name). Int("versions_removed", len(versions)). Msg("Successfully force-removed all release history") return nil }