diff --git a/api/v1alpha1/spec.go b/api/v1alpha1/spec.go index b88aa69a..9744d0fb 100644 --- a/api/v1alpha1/spec.go +++ b/api/v1alpha1/spec.go @@ -155,11 +155,6 @@ type HelmOptions struct { // +optional SkipCRDs bool `json:"skipCRDs,omitempty"` - // Create the release namespace if not present. Defaults to true - // +kubebuilder:default:=true - // +optional - CreateNamespace bool `json:"createNamespace,omitempty"` - // if set, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment, StatefulSet, or ReplicaSet // are in a ready state before marking the release as successful. It will wait for as long as --timeout // Default to false @@ -178,7 +173,7 @@ type HelmOptions struct { // +optional Timeout *metav1.Duration `json:"timeout,omitempty"` - // prevent hooks from running during install + // prevent hooks from running during install/upgrade/uninstall // Default to false // +kubebuilder:default:=false // +optional @@ -190,7 +185,7 @@ type HelmOptions struct { // +optional DisableOpenAPIValidation bool `json:"disableOpenAPIValidation,omitempty"` - // if set, the installation process deletes the installation on failure. + // if set, the installation process deletes the installation/upgrades on failure. // The --wait flag will be set automatically if --atomic is used // Default to false // +kubebuilder:default:=false @@ -211,6 +206,95 @@ type HelmOptions struct { // +kubebuilder:default=false // +optional EnableClientCache bool `json:"enableClientCache,omitempty"` + + // Description is the description of an helm operation + // +optional + Description string `json:"description,omitempty"` + + // HelmInstallOptions are options specific to helm install + // +optional + InstallOptions HelmInstallOptions `json:"installOptions,omitempty"` + + // HelmUpgradeOptions are options specific to helm upgrade + // +optional + UpgradeOptions HelmUpgradeOptions `json:"upgradeOptions,omitempty"` + + // HelmUninstallOptions are options specific to helm uninstall + // +optional + UninstallOptions HelmUninstallOptions `json:"uninstallOptions,omitempty"` +} + +type HelmInstallOptions struct { + // Create the release namespace if not present. Defaults to true + // +kubebuilder:default:=true + // +optional + CreateNamespace bool `json:"createNamespace,omitempty"` + + // Replaces if set indicates to replace an older release with this one + // +kubebuilder:default:=true + // +optional + Replace bool `json:"replace,omitempty"` +} + +type HelmUpgradeOptions struct { + // Force will, if set to `true`, ignore certain warnings and perform the upgrade anyway. + // This should be used with caution. + // +kubebuilder:default:=false + // +optional + Force bool `json:"force,omitempty"` + + // ResetValues will reset the values to the chart's built-ins rather than merging with existing. + // +kubebuilder:default:=false + // +optional + ResetValues bool `json:"resetValues,omitempty"` + + // ReuseValues copies values from the current release to a new release if the + // new release does not have any values. If the request already has values, + // or if there are no values in the current release, this does nothing. + // This is skipped if the ResetValues flag is set, in which case the + // request values are not altered. + // +kubebuilder:default:=false + // +optional + ReuseValues bool `json:"reuseValues,omitempty"` + + // ResetThenReuseValues will reset the values to the chart's built-ins then merge with user's last supplied values. + // +kubebuilder:default:=false + // +optional + ResetThenReuseValues bool `json:"resetThenReuseValues,omitempty"` + + // Recreate will (if true) recreate pods after a rollback. + // +kubebuilder:default:=false + // +optional + Recreate bool `json:"recreate,omitempty"` + + // MaxHistory limits the maximum number of revisions saved per release + // Default to 2 + // +kubebuilder:default=2 + // +optional + MaxHistory int `json:"maxHistory,omitempty"` + + // CleanupOnFail will, if true, cause the upgrade to delete newly-created resources on a failed update. + // +kubebuilder:default:=false + // +optional + CleanupOnFail bool `json:"cleanupOnFail,omitempty"` + + // SubNotes determines whether sub-notes are rendered in the chart. + // +kubebuilder:default:=false + // +optional + SubNotes bool `json:"subNotes,omitempty"` +} + +type HelmUninstallOptions struct { + // When uninstall a chart with this flag, Helm removes the resources associated with the chart, + // but it keeps the release information. This allows to see details about the uninstalled release + // using the helm history command. + // +optional + KeepHistory bool `json:"keepHistory,omitempty"` + + // DeletionPropagation + // +kubebuilder:validation:Enum:=orphan;foreground;background + // +optional + DeletionPropagation string `json:"deletionPropagation,omitempty"` } type HelmChart struct { diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 3c44d4b7..7bb1b4f3 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -585,6 +585,21 @@ func (in *HelmChartSummary) DeepCopy() *HelmChartSummary { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HelmInstallOptions) DeepCopyInto(out *HelmInstallOptions) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmInstallOptions. +func (in *HelmInstallOptions) DeepCopy() *HelmInstallOptions { + if in == nil { + return nil + } + out := new(HelmInstallOptions) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *HelmOptions) DeepCopyInto(out *HelmOptions) { *out = *in @@ -600,6 +615,9 @@ func (in *HelmOptions) DeepCopyInto(out *HelmOptions) { (*out)[key] = val } } + out.InstallOptions = in.InstallOptions + out.UpgradeOptions = in.UpgradeOptions + out.UninstallOptions = in.UninstallOptions } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmOptions. @@ -612,6 +630,36 @@ func (in *HelmOptions) DeepCopy() *HelmOptions { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HelmUninstallOptions) DeepCopyInto(out *HelmUninstallOptions) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmUninstallOptions. +func (in *HelmUninstallOptions) DeepCopy() *HelmUninstallOptions { + if in == nil { + return nil + } + out := new(HelmUninstallOptions) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HelmUpgradeOptions) DeepCopyInto(out *HelmUpgradeOptions) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmUpgradeOptions. +func (in *HelmUpgradeOptions) DeepCopy() *HelmUpgradeOptions { + if in == nil { + return nil + } + out := new(HelmUpgradeOptions) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *KustomizationRef) DeepCopyInto(out *KustomizationRef) { *out = *in diff --git a/config/crd/bases/config.projectsveltos.io_clusterprofiles.yaml b/config/crd/bases/config.projectsveltos.io_clusterprofiles.yaml index 321080db..6bc9f37d 100644 --- a/config/crd/bases/config.projectsveltos.io_clusterprofiles.yaml +++ b/config/crd/bases/config.projectsveltos.io_clusterprofiles.yaml @@ -160,25 +160,23 @@ spec: atomic: default: false description: |- - if set, the installation process deletes the installation on failure. + if set, the installation process deletes the installation/upgrades on failure. The --wait flag will be set automatically if --atomic is used Default to false type: boolean - createNamespace: - default: true - description: Create the release namespace if not present. - Defaults to true - type: boolean dependencyUpdate: default: false description: |- update dependencies if they are missing before installing the chart Default to false type: boolean + description: + description: Description is the description of an helm operation + type: string disableHooks: default: false description: |- - prevent hooks from running during install + prevent hooks from running during install/upgrade/uninstall Default to false type: boolean disableOpenAPIValidation: @@ -193,6 +191,21 @@ spec: client cache. If it is not specified, it will be set to false. type: boolean + installOptions: + description: HelmInstallOptions are options specific to + helm install + properties: + createNamespace: + default: true + description: Create the release namespace if not present. + Defaults to true + type: boolean + replace: + default: true + description: Replaces if set indicates to replace an + older release with this one + type: boolean + type: object labels: additionalProperties: type: string @@ -208,6 +221,77 @@ spec: description: time to wait for any individual Kubernetes operation (like Jobs for hooks) (default 5m0s) type: string + uninstallOptions: + description: HelmUninstallOptions are options specific to + helm uninstall + properties: + deletionPropagation: + description: DeletionPropagation + enum: + - orphan + - foreground + - background + type: string + keepHistory: + description: |- + When uninstall a chart with this flag, Helm removes the resources associated with the chart, + but it keeps the release information. This allows to see details about the uninstalled release + using the helm history command. + type: boolean + type: object + upgradeOptions: + description: HelmUpgradeOptions are options specific to + helm upgrade + properties: + cleanupOnFail: + default: false + description: CleanupOnFail will, if true, cause the + upgrade to delete newly-created resources on a failed + update. + type: boolean + force: + default: false + description: |- + Force will, if set to `true`, ignore certain warnings and perform the upgrade anyway. + This should be used with caution. + type: boolean + maxHistory: + default: 2 + description: |- + MaxHistory limits the maximum number of revisions saved per release + Default to 2 + type: integer + recreate: + default: false + description: Recreate will (if true) recreate pods after + a rollback. + type: boolean + resetThenReuseValues: + default: false + description: ResetThenReuseValues will reset the values + to the chart's built-ins then merge with user's last + supplied values. + type: boolean + resetValues: + default: false + description: ResetValues will reset the values to the + chart's built-ins rather than merging with existing. + type: boolean + reuseValues: + default: false + description: |- + ReuseValues copies values from the current release to a new release if the + new release does not have any values. If the request already has values, + or if there are no values in the current release, this does nothing. + This is skipped if the ResetValues flag is set, in which case the + request values are not altered. + type: boolean + subNotes: + default: false + description: SubNotes determines whether sub-notes are + rendered in the chart. + type: boolean + type: object wait: default: false description: |- diff --git a/config/crd/bases/config.projectsveltos.io_clustersummaries.yaml b/config/crd/bases/config.projectsveltos.io_clustersummaries.yaml index 1b192393..7390dd30 100644 --- a/config/crd/bases/config.projectsveltos.io_clustersummaries.yaml +++ b/config/crd/bases/config.projectsveltos.io_clustersummaries.yaml @@ -176,25 +176,24 @@ spec: atomic: default: false description: |- - if set, the installation process deletes the installation on failure. + if set, the installation process deletes the installation/upgrades on failure. The --wait flag will be set automatically if --atomic is used Default to false type: boolean - createNamespace: - default: true - description: Create the release namespace if not present. - Defaults to true - type: boolean dependencyUpdate: default: false description: |- update dependencies if they are missing before installing the chart Default to false type: boolean + description: + description: Description is the description of an helm + operation + type: string disableHooks: default: false description: |- - prevent hooks from running during install + prevent hooks from running during install/upgrade/uninstall Default to false type: boolean disableOpenAPIValidation: @@ -209,6 +208,21 @@ spec: client cache. If it is not specified, it will be set to false. type: boolean + installOptions: + description: HelmInstallOptions are options specific + to helm install + properties: + createNamespace: + default: true + description: Create the release namespace if not + present. Defaults to true + type: boolean + replace: + default: true + description: Replaces if set indicates to replace + an older release with this one + type: boolean + type: object labels: additionalProperties: type: string @@ -224,6 +238,78 @@ spec: description: time to wait for any individual Kubernetes operation (like Jobs for hooks) (default 5m0s) type: string + uninstallOptions: + description: HelmUninstallOptions are options specific + to helm uninstall + properties: + deletionPropagation: + description: DeletionPropagation + enum: + - orphan + - foreground + - background + type: string + keepHistory: + description: |- + When uninstall a chart with this flag, Helm removes the resources associated with the chart, + but it keeps the release information. This allows to see details about the uninstalled release + using the helm history command. + type: boolean + type: object + upgradeOptions: + description: HelmUpgradeOptions are options specific + to helm upgrade + properties: + cleanupOnFail: + default: false + description: CleanupOnFail will, if true, cause + the upgrade to delete newly-created resources + on a failed update. + type: boolean + force: + default: false + description: |- + Force will, if set to `true`, ignore certain warnings and perform the upgrade anyway. + This should be used with caution. + type: boolean + maxHistory: + default: 2 + description: |- + MaxHistory limits the maximum number of revisions saved per release + Default to 2 + type: integer + recreate: + default: false + description: Recreate will (if true) recreate pods + after a rollback. + type: boolean + resetThenReuseValues: + default: false + description: ResetThenReuseValues will reset the + values to the chart's built-ins then merge with + user's last supplied values. + type: boolean + resetValues: + default: false + description: ResetValues will reset the values to + the chart's built-ins rather than merging with + existing. + type: boolean + reuseValues: + default: false + description: |- + ReuseValues copies values from the current release to a new release if the + new release does not have any values. If the request already has values, + or if there are no values in the current release, this does nothing. + This is skipped if the ResetValues flag is set, in which case the + request values are not altered. + type: boolean + subNotes: + default: false + description: SubNotes determines whether sub-notes + are rendered in the chart. + type: boolean + type: object wait: default: false description: |- diff --git a/config/crd/bases/config.projectsveltos.io_profiles.yaml b/config/crd/bases/config.projectsveltos.io_profiles.yaml index 92fb4d9c..d85348ab 100644 --- a/config/crd/bases/config.projectsveltos.io_profiles.yaml +++ b/config/crd/bases/config.projectsveltos.io_profiles.yaml @@ -160,25 +160,23 @@ spec: atomic: default: false description: |- - if set, the installation process deletes the installation on failure. + if set, the installation process deletes the installation/upgrades on failure. The --wait flag will be set automatically if --atomic is used Default to false type: boolean - createNamespace: - default: true - description: Create the release namespace if not present. - Defaults to true - type: boolean dependencyUpdate: default: false description: |- update dependencies if they are missing before installing the chart Default to false type: boolean + description: + description: Description is the description of an helm operation + type: string disableHooks: default: false description: |- - prevent hooks from running during install + prevent hooks from running during install/upgrade/uninstall Default to false type: boolean disableOpenAPIValidation: @@ -193,6 +191,21 @@ spec: client cache. If it is not specified, it will be set to false. type: boolean + installOptions: + description: HelmInstallOptions are options specific to + helm install + properties: + createNamespace: + default: true + description: Create the release namespace if not present. + Defaults to true + type: boolean + replace: + default: true + description: Replaces if set indicates to replace an + older release with this one + type: boolean + type: object labels: additionalProperties: type: string @@ -208,6 +221,77 @@ spec: description: time to wait for any individual Kubernetes operation (like Jobs for hooks) (default 5m0s) type: string + uninstallOptions: + description: HelmUninstallOptions are options specific to + helm uninstall + properties: + deletionPropagation: + description: DeletionPropagation + enum: + - orphan + - foreground + - background + type: string + keepHistory: + description: |- + When uninstall a chart with this flag, Helm removes the resources associated with the chart, + but it keeps the release information. This allows to see details about the uninstalled release + using the helm history command. + type: boolean + type: object + upgradeOptions: + description: HelmUpgradeOptions are options specific to + helm upgrade + properties: + cleanupOnFail: + default: false + description: CleanupOnFail will, if true, cause the + upgrade to delete newly-created resources on a failed + update. + type: boolean + force: + default: false + description: |- + Force will, if set to `true`, ignore certain warnings and perform the upgrade anyway. + This should be used with caution. + type: boolean + maxHistory: + default: 2 + description: |- + MaxHistory limits the maximum number of revisions saved per release + Default to 2 + type: integer + recreate: + default: false + description: Recreate will (if true) recreate pods after + a rollback. + type: boolean + resetThenReuseValues: + default: false + description: ResetThenReuseValues will reset the values + to the chart's built-ins then merge with user's last + supplied values. + type: boolean + resetValues: + default: false + description: ResetValues will reset the values to the + chart's built-ins rather than merging with existing. + type: boolean + reuseValues: + default: false + description: |- + ReuseValues copies values from the current release to a new release if the + new release does not have any values. If the request already has values, + or if there are no values in the current release, this does nothing. + This is skipped if the ResetValues flag is set, in which case the + request values are not altered. + type: boolean + subNotes: + default: false + description: SubNotes determines whether sub-notes are + rendered in the chart. + type: boolean + type: object wait: default: false description: |- diff --git a/controllers/clusterprofile_predicates.go b/controllers/clusterprofile_predicates.go index 262c7b65..4d650759 100644 --- a/controllers/clusterprofile_predicates.go +++ b/controllers/clusterprofile_predicates.go @@ -33,10 +33,10 @@ type ClusterPredicate struct { Logger logr.Logger } -func (c ClusterPredicate) Create(obj event.TypedCreateEvent[*clusterv1.Cluster]) bool { +func (p ClusterPredicate) Create(obj event.TypedCreateEvent[*clusterv1.Cluster]) bool { cluster := obj.Object - log := c.Logger.WithValues("predicate", "createEvent", + log := p.Logger.WithValues("predicate", "createEvent", "namespace", cluster.Namespace, "cluster", cluster.Name, ) @@ -54,11 +54,11 @@ func (c ClusterPredicate) Create(obj event.TypedCreateEvent[*clusterv1.Cluster]) return false } -func (c ClusterPredicate) Update(obj event.TypedUpdateEvent[*clusterv1.Cluster]) bool { +func (p ClusterPredicate) Update(obj event.TypedUpdateEvent[*clusterv1.Cluster]) bool { newCluster := obj.ObjectNew oldCluster := obj.ObjectOld - log := c.Logger.WithValues("predicate", "updateEvent", + log := p.Logger.WithValues("predicate", "updateEvent", "namespace", newCluster.Namespace, "cluster", newCluster.Name, ) @@ -113,8 +113,8 @@ func (c ClusterPredicate) Update(obj event.TypedUpdateEvent[*clusterv1.Cluster]) return false } -func (c ClusterPredicate) Delete(obj event.TypedDeleteEvent[*clusterv1.Cluster]) bool { - log := c.Logger.WithValues("predicate", "deleteEvent", +func (p ClusterPredicate) Delete(obj event.TypedDeleteEvent[*clusterv1.Cluster]) bool { + log := p.Logger.WithValues("predicate", "deleteEvent", "namespace", obj.Object.GetNamespace(), "cluster", obj.Object.GetName(), ) @@ -231,9 +231,9 @@ type MachinePredicate struct { Logger logr.Logger } -func (c MachinePredicate) Create(obj event.TypedCreateEvent[*clusterv1.Machine]) bool { +func (p MachinePredicate) Create(obj event.TypedCreateEvent[*clusterv1.Machine]) bool { machine := obj.Object - log := c.Logger.WithValues("predicate", "createEvent", + log := p.Logger.WithValues("predicate", "createEvent", "namespace", machine.Namespace, "machine", machine.Name, ) @@ -248,10 +248,10 @@ func (c MachinePredicate) Create(obj event.TypedCreateEvent[*clusterv1.Machine]) return false } -func (c MachinePredicate) Update(obj event.TypedUpdateEvent[*clusterv1.Machine]) bool { +func (p MachinePredicate) Update(obj event.TypedUpdateEvent[*clusterv1.Machine]) bool { newMachine := obj.ObjectNew oldMachine := obj.ObjectOld - log := c.Logger.WithValues("predicate", "updateEvent", + log := p.Logger.WithValues("predicate", "updateEvent", "namespace", newMachine.Namespace, "machine", newMachine.Name, ) @@ -278,8 +278,8 @@ func (c MachinePredicate) Update(obj event.TypedUpdateEvent[*clusterv1.Machine]) return false } -func (c MachinePredicate) Delete(obj event.TypedDeleteEvent[*clusterv1.Machine]) bool { - log := c.Logger.WithValues("predicate", "deleteEvent", +func (p MachinePredicate) Delete(obj event.TypedDeleteEvent[*clusterv1.Machine]) bool { + log := p.Logger.WithValues("predicate", "deleteEvent", "namespace", obj.Object.GetNamespace(), "machine", obj.Object.GetName(), ) @@ -288,8 +288,8 @@ func (c MachinePredicate) Delete(obj event.TypedDeleteEvent[*clusterv1.Machine]) return false } -func (c MachinePredicate) Generic(obj event.TypedGenericEvent[*clusterv1.Machine]) bool { - log := c.Logger.WithValues("predicate", "genericEvent", +func (p MachinePredicate) Generic(obj event.TypedGenericEvent[*clusterv1.Machine]) bool { + log := p.Logger.WithValues("predicate", "genericEvent", "namespace", obj.Object.GetNamespace(), "machine", obj.Object.GetName(), ) diff --git a/controllers/handlers_helm.go b/controllers/handlers_helm.go index fb76ccfb..a2f208b4 100644 --- a/controllers/handlers_helm.go +++ b/controllers/handlers_helm.go @@ -71,9 +71,11 @@ var ( ) const ( - writeFilePermission = 0644 - lockTimeout = 30 - notInstalledMessage = "Not installed yet and action is uninstall" + writeFilePermission = 0644 + lockTimeout = 30 + notInstalledMessage = "Not installed yet and action is uninstall" + defaultMaxHistory = 2 + defaultDeletionPropagation = "background" ) type releaseInfo struct { @@ -305,10 +307,17 @@ func uninstallHelmCharts(ctx context.Context, c client.Client, clusterSummary *c logger.V(logs.LogInfo).Info("ClusterProfile StopMatchingBehavior set to LeavePolicies") } else { - err = doUninstallRelease(clusterSummary, currentChart, kubeconfig, logger) - if err != nil { - if !errors.Is(err, driver.ErrReleaseNotFound) { - return nil, err + currentRelease, err := getReleaseInfo(currentChart.ReleaseName, + currentChart.ReleaseNamespace, kubeconfig, getEnableClientCacheValue(currentChart.Options)) + if err != nil && !errors.Is(err, driver.ErrReleaseNotFound) { + return nil, err + } + if currentRelease != nil && currentRelease.Status != string(release.StatusUninstalled) { + err = doUninstallRelease(clusterSummary, currentChart, kubeconfig, logger) + if err != nil { + if !errors.Is(err, driver.ErrReleaseNotFound) { + return nil, err + } } } } @@ -821,11 +830,10 @@ func uninstallRelease(clusterSummary *configv1alpha1.ClusterSummary, return err } - uninstallClient := action.NewUninstall(actionConfig) - uninstallClient.DryRun = false - uninstallClient.Wait = false - uninstallClient.DisableHooks = false - uninstallClient.KeepHistory = false + uninstallClient, err := getHelmUninstallClient(helmChart, actionConfig) + if err != nil { + return err + } _, err = uninstallClient.Run(releaseName) if err != nil { @@ -1070,6 +1078,10 @@ func shouldUninstall(currentRelease *releaseInfo, requestedChart *configv1alpha1 return false } + if currentRelease.Status == string(release.StatusUninstalled) { + return false + } + if requestedChart.HelmChartAction != configv1alpha1.HelmChartActionUninstall { return false } @@ -1632,7 +1644,7 @@ func getWaitForJobsHelmValue(options *configv1alpha1.HelmOptions) bool { func getCreateNamespaceHelmValue(options *configv1alpha1.HelmOptions) bool { if options != nil { - return options.CreateNamespace + return options.InstallOptions.CreateNamespace } return true // for backward compatibility @@ -1693,6 +1705,97 @@ func getLabelsValue(options *configv1alpha1.HelmOptions) map[string]string { return map[string]string{} } +func getReplaceValue(options *configv1alpha1.HelmOptions) bool { + if options != nil { + return options.InstallOptions.Replace + } + return true +} + +func getForceValue(options *configv1alpha1.HelmOptions) bool { + if options != nil { + return options.UpgradeOptions.Force + } + return false +} + +func getReuseValues(options *configv1alpha1.HelmOptions) bool { + if options != nil { + return options.UpgradeOptions.ReuseValues + } + return false +} + +func getResetValues(options *configv1alpha1.HelmOptions) bool { + if options != nil { + return options.UpgradeOptions.ResetValues + } + return false +} + +func getResetThenReuseValues(options *configv1alpha1.HelmOptions) bool { + if options != nil { + return options.UpgradeOptions.ResetThenReuseValues + } + return false +} + +func getDescriptionValue(options *configv1alpha1.HelmOptions) string { + if options != nil { + return options.Description + } + + return "" +} + +func getKeepHistoryValue(options *configv1alpha1.HelmOptions) bool { + if options != nil { + return options.UninstallOptions.KeepHistory + } + + return false +} + +func getDeletionPropagation(options *configv1alpha1.HelmOptions) string { + if options != nil { + return options.UninstallOptions.DeletionPropagation + } + + return defaultDeletionPropagation +} + +func getMaxHistoryValue(options *configv1alpha1.HelmOptions) int { + if options != nil { + return options.UpgradeOptions.MaxHistory + } + + return defaultMaxHistory +} + +func getCleanupOnFailValue(options *configv1alpha1.HelmOptions) bool { + if options != nil { + return options.UpgradeOptions.CleanupOnFail + } + + return false +} + +func getSubNotesValue(options *configv1alpha1.HelmOptions) bool { + if options != nil { + return options.UpgradeOptions.SubNotes + } + + return false +} + +func getRecreateValue(options *configv1alpha1.HelmOptions) bool { + if options != nil { + return options.UpgradeOptions.Recreate + } + + return false +} + func getHelmInstallClient(requestedChart *configv1alpha1.HelmChart, kubeconfig string) (*action.Install, error) { actionConfig, err := actionConfigInit(requestedChart.ReleaseNamespace, kubeconfig, getEnableClientCacheValue(requestedChart.Options)) if err != nil { @@ -1716,8 +1819,9 @@ func getHelmInstallClient(requestedChart *configv1alpha1.HelmChart, kubeconfig s return nil, err } } - installClient.Replace = true + installClient.Replace = getReplaceValue(requestedChart.Options) installClient.Labels = getLabelsValue(requestedChart.Options) + installClient.Description = getDescriptionValue(requestedChart.Options) return installClient, nil } @@ -1740,12 +1844,41 @@ func getHelmUpgradeClient(requestedChart *configv1alpha1.HelmChart, actionConfig return nil, err } } + upgradeClient.ResetValues = getResetValues(requestedChart.Options) + upgradeClient.ReuseValues = getReuseValues(requestedChart.Options) + upgradeClient.ResetThenReuseValues = getResetThenReuseValues(requestedChart.Options) + upgradeClient.Force = getForceValue(requestedChart.Options) upgradeClient.Labels = getLabelsValue(requestedChart.Options) - upgradeClient.ResetValues = true + upgradeClient.Description = getDescriptionValue(requestedChart.Options) + upgradeClient.MaxHistory = getMaxHistoryValue(requestedChart.Options) + upgradeClient.CleanupOnFail = getCleanupOnFailValue(requestedChart.Options) + upgradeClient.SubNotes = getSubNotesValue(requestedChart.Options) + upgradeClient.Recreate = getRecreateValue(requestedChart.Options) return upgradeClient, nil } +func getHelmUninstallClient(requestedChart *configv1alpha1.HelmChart, actionConfig *action.Configuration) (*action.Uninstall, error) { + uninstallClient := action.NewUninstall(actionConfig) + uninstallClient.DryRun = false + if requestedChart != nil { + if timeout := getTimeoutValue(requestedChart.Options); timeout != nil { + var err error + uninstallClient.Timeout, err = time.ParseDuration(timeout.String()) + if err != nil { + return nil, err + } + } + + uninstallClient.Description = getDescriptionValue(requestedChart.Options) + uninstallClient.Wait = getWaitHelmValue(requestedChart.Options) + uninstallClient.DisableHooks = getDisableHooksHelmValue(requestedChart.Options) + uninstallClient.KeepHistory = getKeepHistoryValue(requestedChart.Options) + uninstallClient.DeletionPropagation = getDeletionPropagation(requestedChart.Options) + } + return uninstallClient, nil +} + func addExtraMetadata(ctx context.Context, requestedChart *configv1alpha1.HelmChart, clusterSummary *configv1alpha1.ClusterSummary, kubeconfig string, logger logr.Logger) error { diff --git a/examples/external_dns.yaml b/examples/external_dns.yaml new file mode 100644 index 00000000..4d98cbbc --- /dev/null +++ b/examples/external_dns.yaml @@ -0,0 +1,15 @@ +apiVersion: config.projectsveltos.io/v1alpha1 +kind: ClusterProfile +metadata: + name: external-dns +spec: + clusterSelector: env=fv + syncMode: Continuous + helmCharts: + - repositoryURL: https://kubernetes-sigs.github.io/external-dns/ + repositoryName: external-dns + chartName: external-dns/external-dns + chartVersion: 1.14.4 + releaseName: external-dns + releaseNamespace: external-dns + helmChartAction: Install \ No newline at end of file diff --git a/manifest/manifest.yaml b/manifest/manifest.yaml index 8d06a116..0b073db4 100644 --- a/manifest/manifest.yaml +++ b/manifest/manifest.yaml @@ -543,25 +543,23 @@ spec: atomic: default: false description: |- - if set, the installation process deletes the installation on failure. + if set, the installation process deletes the installation/upgrades on failure. The --wait flag will be set automatically if --atomic is used Default to false type: boolean - createNamespace: - default: true - description: Create the release namespace if not present. - Defaults to true - type: boolean dependencyUpdate: default: false description: |- update dependencies if they are missing before installing the chart Default to false type: boolean + description: + description: Description is the description of an helm operation + type: string disableHooks: default: false description: |- - prevent hooks from running during install + prevent hooks from running during install/upgrade/uninstall Default to false type: boolean disableOpenAPIValidation: @@ -576,6 +574,21 @@ spec: client cache. If it is not specified, it will be set to false. type: boolean + installOptions: + description: HelmInstallOptions are options specific to + helm install + properties: + createNamespace: + default: true + description: Create the release namespace if not present. + Defaults to true + type: boolean + replace: + default: true + description: Replaces if set indicates to replace an + older release with this one + type: boolean + type: object labels: additionalProperties: type: string @@ -591,6 +604,77 @@ spec: description: time to wait for any individual Kubernetes operation (like Jobs for hooks) (default 5m0s) type: string + uninstallOptions: + description: HelmUninstallOptions are options specific to + helm uninstall + properties: + deletionPropagation: + description: DeletionPropagation + enum: + - orphan + - foreground + - background + type: string + keepHistory: + description: |- + When uninstall a chart with this flag, Helm removes the resources associated with the chart, + but it keeps the release information. This allows to see details about the uninstalled release + using the helm history command. + type: boolean + type: object + upgradeOptions: + description: HelmUpgradeOptions are options specific to + helm upgrade + properties: + cleanupOnFail: + default: false + description: CleanupOnFail will, if true, cause the + upgrade to delete newly-created resources on a failed + update. + type: boolean + force: + default: false + description: |- + Force will, if set to `true`, ignore certain warnings and perform the upgrade anyway. + This should be used with caution. + type: boolean + maxHistory: + default: 2 + description: |- + MaxHistory limits the maximum number of revisions saved per release + Default to 2 + type: integer + recreate: + default: false + description: Recreate will (if true) recreate pods after + a rollback. + type: boolean + resetThenReuseValues: + default: false + description: ResetThenReuseValues will reset the values + to the chart's built-ins then merge with user's last + supplied values. + type: boolean + resetValues: + default: false + description: ResetValues will reset the values to the + chart's built-ins rather than merging with existing. + type: boolean + reuseValues: + default: false + description: |- + ReuseValues copies values from the current release to a new release if the + new release does not have any values. If the request already has values, + or if there are no values in the current release, this does nothing. + This is skipped if the ResetValues flag is set, in which case the + request values are not altered. + type: boolean + subNotes: + default: false + description: SubNotes determines whether sub-notes are + rendered in the chart. + type: boolean + type: object wait: default: false description: |- @@ -1769,25 +1853,24 @@ spec: atomic: default: false description: |- - if set, the installation process deletes the installation on failure. + if set, the installation process deletes the installation/upgrades on failure. The --wait flag will be set automatically if --atomic is used Default to false type: boolean - createNamespace: - default: true - description: Create the release namespace if not present. - Defaults to true - type: boolean dependencyUpdate: default: false description: |- update dependencies if they are missing before installing the chart Default to false type: boolean + description: + description: Description is the description of an helm + operation + type: string disableHooks: default: false description: |- - prevent hooks from running during install + prevent hooks from running during install/upgrade/uninstall Default to false type: boolean disableOpenAPIValidation: @@ -1802,6 +1885,21 @@ spec: client cache. If it is not specified, it will be set to false. type: boolean + installOptions: + description: HelmInstallOptions are options specific + to helm install + properties: + createNamespace: + default: true + description: Create the release namespace if not + present. Defaults to true + type: boolean + replace: + default: true + description: Replaces if set indicates to replace + an older release with this one + type: boolean + type: object labels: additionalProperties: type: string @@ -1817,6 +1915,78 @@ spec: description: time to wait for any individual Kubernetes operation (like Jobs for hooks) (default 5m0s) type: string + uninstallOptions: + description: HelmUninstallOptions are options specific + to helm uninstall + properties: + deletionPropagation: + description: DeletionPropagation + enum: + - orphan + - foreground + - background + type: string + keepHistory: + description: |- + When uninstall a chart with this flag, Helm removes the resources associated with the chart, + but it keeps the release information. This allows to see details about the uninstalled release + using the helm history command. + type: boolean + type: object + upgradeOptions: + description: HelmUpgradeOptions are options specific + to helm upgrade + properties: + cleanupOnFail: + default: false + description: CleanupOnFail will, if true, cause + the upgrade to delete newly-created resources + on a failed update. + type: boolean + force: + default: false + description: |- + Force will, if set to `true`, ignore certain warnings and perform the upgrade anyway. + This should be used with caution. + type: boolean + maxHistory: + default: 2 + description: |- + MaxHistory limits the maximum number of revisions saved per release + Default to 2 + type: integer + recreate: + default: false + description: Recreate will (if true) recreate pods + after a rollback. + type: boolean + resetThenReuseValues: + default: false + description: ResetThenReuseValues will reset the + values to the chart's built-ins then merge with + user's last supplied values. + type: boolean + resetValues: + default: false + description: ResetValues will reset the values to + the chart's built-ins rather than merging with + existing. + type: boolean + reuseValues: + default: false + description: |- + ReuseValues copies values from the current release to a new release if the + new release does not have any values. If the request already has values, + or if there are no values in the current release, this does nothing. + This is skipped if the ResetValues flag is set, in which case the + request values are not altered. + type: boolean + subNotes: + default: false + description: SubNotes determines whether sub-notes + are rendered in the chart. + type: boolean + type: object wait: default: false description: |- @@ -2552,25 +2722,23 @@ spec: atomic: default: false description: |- - if set, the installation process deletes the installation on failure. + if set, the installation process deletes the installation/upgrades on failure. The --wait flag will be set automatically if --atomic is used Default to false type: boolean - createNamespace: - default: true - description: Create the release namespace if not present. - Defaults to true - type: boolean dependencyUpdate: default: false description: |- update dependencies if they are missing before installing the chart Default to false type: boolean + description: + description: Description is the description of an helm operation + type: string disableHooks: default: false description: |- - prevent hooks from running during install + prevent hooks from running during install/upgrade/uninstall Default to false type: boolean disableOpenAPIValidation: @@ -2585,6 +2753,21 @@ spec: client cache. If it is not specified, it will be set to false. type: boolean + installOptions: + description: HelmInstallOptions are options specific to + helm install + properties: + createNamespace: + default: true + description: Create the release namespace if not present. + Defaults to true + type: boolean + replace: + default: true + description: Replaces if set indicates to replace an + older release with this one + type: boolean + type: object labels: additionalProperties: type: string @@ -2600,6 +2783,77 @@ spec: description: time to wait for any individual Kubernetes operation (like Jobs for hooks) (default 5m0s) type: string + uninstallOptions: + description: HelmUninstallOptions are options specific to + helm uninstall + properties: + deletionPropagation: + description: DeletionPropagation + enum: + - orphan + - foreground + - background + type: string + keepHistory: + description: |- + When uninstall a chart with this flag, Helm removes the resources associated with the chart, + but it keeps the release information. This allows to see details about the uninstalled release + using the helm history command. + type: boolean + type: object + upgradeOptions: + description: HelmUpgradeOptions are options specific to + helm upgrade + properties: + cleanupOnFail: + default: false + description: CleanupOnFail will, if true, cause the + upgrade to delete newly-created resources on a failed + update. + type: boolean + force: + default: false + description: |- + Force will, if set to `true`, ignore certain warnings and perform the upgrade anyway. + This should be used with caution. + type: boolean + maxHistory: + default: 2 + description: |- + MaxHistory limits the maximum number of revisions saved per release + Default to 2 + type: integer + recreate: + default: false + description: Recreate will (if true) recreate pods after + a rollback. + type: boolean + resetThenReuseValues: + default: false + description: ResetThenReuseValues will reset the values + to the chart's built-ins then merge with user's last + supplied values. + type: boolean + resetValues: + default: false + description: ResetValues will reset the values to the + chart's built-ins rather than merging with existing. + type: boolean + reuseValues: + default: false + description: |- + ReuseValues copies values from the current release to a new release if the + new release does not have any values. If the request already has values, + or if there are no values in the current release, this does nothing. + This is skipped if the ResetValues flag is set, in which case the + request values are not altered. + type: boolean + subNotes: + default: false + description: SubNotes determines whether sub-notes are + rendered in the chart. + type: boolean + type: object wait: default: false description: |- diff --git a/test/fv/dependencies_test.go b/test/fv/dependencies_test.go index f9eac357..40a032ea 100644 --- a/test/fv/dependencies_test.go +++ b/test/fv/dependencies_test.go @@ -77,10 +77,10 @@ var _ = Describe("Dependencies", func() { { RepositoryURL: "https://charts.bitnami.com/bitnami", RepositoryName: "bitnami", - ChartName: "bitnami/external-dns", - ChartVersion: "6.30.1", - ReleaseName: "external-dns", - ReleaseNamespace: "external-dns", + ChartName: "bitnami/flink", + ChartVersion: "1.1.1", + ReleaseName: "flink", + ReleaseNamespace: "flink", HelmChartAction: configv1alpha1.HelmChartActionInstall, }, } diff --git a/test/fv/helm_options_test.go b/test/fv/helm_options_test.go new file mode 100644 index 00000000..dc0c70aa --- /dev/null +++ b/test/fv/helm_options_test.go @@ -0,0 +1,178 @@ +/* +Copyright 2024. projectsveltos.io. All rights reserved. + +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 fv_test + +import ( + "context" + "strings" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + appsv1 "k8s.io/api/apps/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + + configv1alpha1 "github.com/projectsveltos/addon-controller/api/v1alpha1" + "github.com/projectsveltos/addon-controller/controllers" +) + +var _ = Describe("HelmOptions", func() { + const ( + namePrefix = "helm-options-" + ) + + It("Deploy and updates helm charts with options correctly", Label("FV", "EXTENDED"), func() { + Byf("Create a ClusterProfile matching Cluster %s/%s", kindWorkloadCluster.Namespace, kindWorkloadCluster.Name) + clusterProfile := getClusterProfile(namePrefix, map[string]string{key: value}) + clusterProfile.Spec.SyncMode = configv1alpha1.SyncModeContinuous + Expect(k8sClient.Create(context.TODO(), clusterProfile)).To(Succeed()) + + verifyClusterProfileMatches(clusterProfile) + + verifyClusterSummary(controllers.ClusterProfileLabelName, + clusterProfile.Name, &clusterProfile.Spec, kindWorkloadCluster.Namespace, kindWorkloadCluster.Name) + + Byf("Update ClusterProfile %s to deploy helm charts", clusterProfile.Name) + currentClusterProfile := &configv1alpha1.ClusterProfile{} + Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: clusterProfile.Name}, currentClusterProfile)).To(Succeed()) + currentClusterProfile.Spec.HelmCharts = []configv1alpha1.HelmChart{ + { + RepositoryURL: "https://kubernetes-sigs.github.io/external-dns/", + RepositoryName: "external-dns", + ChartName: "external-dns/external-dns", + ChartVersion: "1.14.3", + ReleaseName: "external-dns", + ReleaseNamespace: "external-dns", + HelmChartAction: configv1alpha1.HelmChartActionInstall, + Options: &configv1alpha1.HelmOptions{ + DependencyUpdate: true, + InstallOptions: configv1alpha1.HelmInstallOptions{ + Replace: false, + }, + }, + }, + } + + Expect(k8sClient.Update(context.TODO(), currentClusterProfile)).To(Succeed()) + + clusterSummary := verifyClusterSummary(controllers.ClusterProfileLabelName, + currentClusterProfile.Name, ¤tClusterProfile.Spec, + kindWorkloadCluster.Namespace, kindWorkloadCluster.Name) + + Byf("Getting client to access the workload cluster") + workloadClient, err := getKindWorkloadClusterKubeconfig() + Expect(err).To(BeNil()) + Expect(workloadClient).ToNot(BeNil()) + + Byf("Verifying external-dns deployment is created in the workload cluster") + Eventually(func() error { + depl := &appsv1.Deployment{} + return workloadClient.Get(context.TODO(), + types.NamespacedName{Namespace: "external-dns", Name: "external-dns"}, depl) + }, timeout, pollingInterval).Should(BeNil()) + + Byf("Verifying external-dns deployment image") + depl := &appsv1.Deployment{} + Expect(workloadClient.Get(context.TODO(), + types.NamespacedName{Namespace: "external-dns", Name: "external-dns"}, depl)).To(Succeed()) + Expect(len(depl.Spec.Template.Spec.Containers)).To(Equal(1)) + Expect(depl.Spec.Template.Spec.Containers[0].Image).To(ContainSubstring("v0.14.0")) + + charts := []configv1alpha1.Chart{ + {ReleaseName: "external-dns", ChartVersion: "1.14.3", Namespace: "external-dns"}, + } + + verifyClusterConfiguration(configv1alpha1.ClusterProfileKind, clusterProfile.Name, + clusterSummary.Spec.ClusterNamespace, clusterSummary.Spec.ClusterName, configv1alpha1.FeatureHelm, + nil, charts) + + Byf("Update ClusterProfile %s to upgrade external-dns helm charts", clusterProfile.Name) + Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: clusterProfile.Name}, currentClusterProfile)).To(Succeed()) + currentClusterProfile.Spec.HelmCharts = []configv1alpha1.HelmChart{ + { + RepositoryURL: "https://kubernetes-sigs.github.io/external-dns/", + RepositoryName: "external-dns", + ChartName: "external-dns/external-dns", + ChartVersion: "1.14.4", + ReleaseName: "external-dns", + ReleaseNamespace: "external-dns", + HelmChartAction: configv1alpha1.HelmChartActionInstall, + Options: &configv1alpha1.HelmOptions{ + DependencyUpdate: true, + UpgradeOptions: configv1alpha1.HelmUpgradeOptions{ + ResetValues: false, + MaxHistory: 5, + }, + }, + }, + } + + Expect(k8sClient.Update(context.TODO(), currentClusterProfile)).To(Succeed()) + + verifyClusterSummary(controllers.ClusterProfileLabelName, + currentClusterProfile.Name, ¤tClusterProfile.Spec, + kindWorkloadCluster.Namespace, kindWorkloadCluster.Name) + + Byf("Verifying external-dns deployment is upgraded in the workload cluster") + Eventually(func() bool { + depl := &appsv1.Deployment{} + err = workloadClient.Get(context.TODO(), + types.NamespacedName{Namespace: "external-dns", Name: "external-dns"}, depl) + if err != nil { + return false + } + if len(depl.Spec.Template.Spec.Containers) != 1 { + return false + } + return strings.Contains(depl.Spec.Template.Spec.Containers[0].Image, "v0.14.1") + }, timeout, pollingInterval).Should(BeTrue()) + + Byf("Update ClusterProfile %s to uninstall external-dns helm charts", clusterProfile.Name) + Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: clusterProfile.Name}, currentClusterProfile)).To(Succeed()) + currentClusterProfile.Spec.HelmCharts = []configv1alpha1.HelmChart{ + { + RepositoryURL: "https://kubernetes-sigs.github.io/external-dns/", + RepositoryName: "external-dns", + ChartName: "external-dns/external-dns", + ChartVersion: "1.14.4", + ReleaseName: "external-dns", + ReleaseNamespace: "external-dns", + HelmChartAction: configv1alpha1.HelmChartActionUninstall, + Options: &configv1alpha1.HelmOptions{ + DependencyUpdate: true, + UninstallOptions: configv1alpha1.HelmUninstallOptions{ + KeepHistory: true, + DeletionPropagation: "background", + }, + }, + }, + } + + Expect(k8sClient.Update(context.TODO(), currentClusterProfile)).To(Succeed()) + + Byf("Verifying external-dns deployment is removed from workload cluster") + Eventually(func() bool { + depl := &appsv1.Deployment{} + err = workloadClient.Get(context.TODO(), + types.NamespacedName{Namespace: "external-dns", Name: "external-dns"}, depl) + return apierrors.IsNotFound(err) + }, timeout, pollingInterval).Should(BeTrue()) + + deleteClusterProfile(clusterProfile) + }) +})