diff --git a/api/v1alpha1/condition_consts.go b/api/v1alpha1/condition_consts.go index d7a337bd..4cc53de5 100644 --- a/api/v1alpha1/condition_consts.go +++ b/api/v1alpha1/condition_consts.go @@ -42,9 +42,20 @@ const ( // ClusterSelectionFailedReason indicates that the HelmChartProxy controller failed to select the workload Clusters. ClusterSelectionFailedReason = "ClusterSelectionFailed" + // HelmReleaseProxiesRolloutNotCompleteReason indicates that the initial rollout + // of HelmReleaseProxies has not been completed. + HelmReleaseProxiesRolloutNotCompleteReason = "HelmReleaseProxiesRolloutNotComplete" + + // HelmReleaseProxiesRolloutUndefinedReason indicates that HelmChartProxy doesn't + // use Rollout Step Size to reconcile HelmReleaseProxies. + HelmReleaseProxiesRolloutUndefinedReason = "HelmReleaseProxiesRolloutUndefined" + // HelmReleaseProxiesReadyCondition indicates that the HelmReleaseProxies are ready, meaning that the Helm installation, upgrade // or deletion is complete. HelmReleaseProxiesReadyCondition clusterv1.ConditionType = "HelmReleaseProxiesReady" + + // HelmReleaseProxiesRolloutCompletedCondition indicates if the initial rollout of HelmReleaseProxies is complete. + HelmReleaseProxiesRolloutCompletedCondition clusterv1.ConditionType = "HelmReleaseProxiesRolloutCompleted" ) // HelmReleaseProxy Conditions and Reasons. diff --git a/api/v1alpha1/helmchartproxy_types.go b/api/v1alpha1/helmchartproxy_types.go index 6ecfda69..02b85b3d 100644 --- a/api/v1alpha1/helmchartproxy_types.go +++ b/api/v1alpha1/helmchartproxy_types.go @@ -19,6 +19,7 @@ package v1alpha1 import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" ) @@ -84,6 +85,13 @@ type HelmChartProxySpec struct { // +optional ReconcileStrategy string `json:"reconcileStrategy,omitempty"` + // Rollout is used to define install and upgrade level rollout options that + // will be used when rolling out HelmReleaseProxy resources changes. If + // undefined, it defaults to no rollout; i.e it applies changes to all + // matching clusters at once. + // +optional + Rollout *Rollout `json:"rollout,omitempty"` + // Options represents CLI flags passed to Helm operations (i.e. install, upgrade, delete) and // include options such as wait, skipCRDs, timeout, waitForJobs, etc. // +optional @@ -98,6 +106,46 @@ type HelmChartProxySpec struct { TLSConfig *TLSConfig `json:"tlsConfig,omitempty"` } +// Rollout defines install and upgrade level rollout options when rolling out +// HelmReleaseProxy resource changes. +type Rollout struct { + // Install rollout options. If left empty, it defaults to no rollout; i.e. it + // applies changes to all matching clusters at once. + // +optional + Install *RolloutOptions `json:"install,omitempty"` + + // Upgrade rollout options. If left empty, it defaults to no rollout; i.e. it + // applies changes to all matching clusters at once. + // +optional + Upgrade *RolloutOptions `json:"upgrade,omitempty"` +} + +// RolloutOptions defines rollout options to be used when rolling out +// HelmReleaseProxy resource changes. +type RolloutOptions struct { + // StepInit defines the initial step to start from during rollout. + // e.g. an int (5) or percentage of count of total matching clusters (25%) + StepInit *intstr.IntOrString `json:"stepInit"` + + // StepIncrement defines the increment to be added to existing stepSize + // during rollout. + // If StepIncrement is undefined, step size is set to stepInit. + // e.g. an int (5) or percentage of count of total matching clusters (25%) + // +optional + StepIncrement *intstr.IntOrString `json:"stepIncrement,omitempty"` + + // StepLimit defines the upper limit on stepSize during rollout. + // If defined and computes to less than stepInit, step size can reach 100%; + // meaning that no upper limit is set. + // If stepIncrement is defined and stepLimit is omitted, step size can reach + // 100%; meaning that no upper limit is set. + // If StepIncrement is undefined and if stepLimit is omitted, step size is + // defaulted to the value computed from stepInit. + // e.g. an int (5) or percentage of count of total matching clusters (25%) + // +optional + StepLimit *intstr.IntOrString `json:"stepLimit,omitempty"` +} + type HelmOptions struct { // DisableHooks prevents hooks from running during the Helm install action. // +optional @@ -238,6 +286,11 @@ type TLSConfig struct { InsecureSkipTLSVerify bool `json:"insecureSkipTLSVerify,omitempty"` } +type RolloutStatus struct { + Count *int `json:"count,omitempty"` + StepSize *int `json:"stepSize,omitempty"` +} + // HelmChartProxyStatus defines the observed state of HelmChartProxy. type HelmChartProxyStatus struct { // Conditions defines current state of the HelmChartProxy. @@ -248,6 +301,8 @@ type HelmChartProxyStatus struct { // +optional MatchingClusters []corev1.ObjectReference `json:"matchingClusters"` + Rollout *RolloutStatus `json:"rollout,omitempty"` + // ObservedGeneration is the latest generation observed by the controller. // +optional ObservedGeneration int64 `json:"observedGeneration,omitempty"` diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 19fb7024..01be2c71 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -24,6 +24,7 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/intstr" "sigs.k8s.io/cluster-api/api/v1beta1" ) @@ -106,6 +107,11 @@ func (in *HelmChartProxyList) DeepCopyObject() runtime.Object { func (in *HelmChartProxySpec) DeepCopyInto(out *HelmChartProxySpec) { *out = *in in.ClusterSelector.DeepCopyInto(&out.ClusterSelector) + if in.Rollout != nil { + in, out := &in.Rollout, &out.Rollout + *out = new(Rollout) + (*in).DeepCopyInto(*out) + } in.Options.DeepCopyInto(&out.Options) if in.Credentials != nil { in, out := &in.Credentials, &out.Credentials @@ -144,6 +150,11 @@ func (in *HelmChartProxyStatus) DeepCopyInto(out *HelmChartProxyStatus) { *out = make([]corev1.ObjectReference, len(*in)) copy(*out, *in) } + if in.Rollout != nil { + in, out := &in.Rollout, &out.Rollout + *out = new(RolloutStatus) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmChartProxyStatus. @@ -336,6 +347,86 @@ func (in *HelmUpgradeOptions) DeepCopy() *HelmUpgradeOptions { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Rollout) DeepCopyInto(out *Rollout) { + *out = *in + if in.Install != nil { + in, out := &in.Install, &out.Install + *out = new(RolloutOptions) + (*in).DeepCopyInto(*out) + } + if in.Upgrade != nil { + in, out := &in.Upgrade, &out.Upgrade + *out = new(RolloutOptions) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Rollout. +func (in *Rollout) DeepCopy() *Rollout { + if in == nil { + return nil + } + out := new(Rollout) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RolloutOptions) DeepCopyInto(out *RolloutOptions) { + *out = *in + if in.StepInit != nil { + in, out := &in.StepInit, &out.StepInit + *out = new(intstr.IntOrString) + **out = **in + } + if in.StepIncrement != nil { + in, out := &in.StepIncrement, &out.StepIncrement + *out = new(intstr.IntOrString) + **out = **in + } + if in.StepLimit != nil { + in, out := &in.StepLimit, &out.StepLimit + *out = new(intstr.IntOrString) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RolloutOptions. +func (in *RolloutOptions) DeepCopy() *RolloutOptions { + if in == nil { + return nil + } + out := new(RolloutOptions) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RolloutStatus) DeepCopyInto(out *RolloutStatus) { + *out = *in + if in.Count != nil { + in, out := &in.Count, &out.Count + *out = new(int) + **out = **in + } + if in.StepSize != nil { + in, out := &in.StepSize, &out.StepSize + *out = new(int) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RolloutStatus. +func (in *RolloutStatus) DeepCopy() *RolloutStatus { + if in == nil { + return nil + } + out := new(RolloutStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TLSConfig) DeepCopyInto(out *TLSConfig) { *out = *in diff --git a/config/crd/bases/addons.cluster.x-k8s.io_helmchartproxies.yaml b/config/crd/bases/addons.cluster.x-k8s.io_helmchartproxies.yaml index f4fc2562..9d49f42f 100644 --- a/config/crd/bases/addons.cluster.x-k8s.io_helmchartproxies.yaml +++ b/config/crd/bases/addons.cluster.x-k8s.io_helmchartproxies.yaml @@ -279,6 +279,94 @@ spec: RepoURL is the URL of the Helm chart repository. e.g. chart-path oci://repo-url/chart-name as repoURL: oci://repo-url and https://repo-url/chart-name as repoURL: https://repo-url type: string + rollout: + description: |- + Rollout is used to define install and upgrade level rollout options that + will be used when rolling out HelmReleaseProxy resources changes. If + undefined, it defaults to no rollout; i.e it applies changes to all + matching clusters at once. + properties: + install: + description: |- + Install rollout options. If left empty, it defaults to no rollout; i.e. it + applies changes to all matching clusters at once. + properties: + stepIncrement: + anyOf: + - type: integer + - type: string + description: |- + StepIncrement defines the increment to be added to existing stepSize + during rollout. + If StepIncrement is undefined, step size is set to stepInit. + e.g. an int (5) or percentage of count of total matching clusters (25%) + x-kubernetes-int-or-string: true + stepInit: + anyOf: + - type: integer + - type: string + description: |- + StepInit defines the initial step to start from during rollout. + e.g. an int (5) or percentage of count of total matching clusters (25%) + x-kubernetes-int-or-string: true + stepLimit: + anyOf: + - type: integer + - type: string + description: |- + StepLimit defines the upper limit on stepSize during rollout. + If defined and computes to less than stepInit, step size can reach 100%; + meaning that no upper limit is set. + If stepIncrement is defined and stepLimit is omitted, step size can reach + 100%; meaning that no upper limit is set. + If StepIncrement is undefined and if stepLimit is omitted, step size is + defaulted to the value computed from stepInit. + e.g. an int (5) or percentage of count of total matching clusters (25%) + x-kubernetes-int-or-string: true + required: + - stepInit + type: object + upgrade: + description: |- + Upgrade rollout options. If left empty, it defaults to no rollout; i.e. it + applies changes to all matching clusters at once. + properties: + stepIncrement: + anyOf: + - type: integer + - type: string + description: |- + StepIncrement defines the increment to be added to existing stepSize + during rollout. + If StepIncrement is undefined, step size is set to stepInit. + e.g. an int (5) or percentage of count of total matching clusters (25%) + x-kubernetes-int-or-string: true + stepInit: + anyOf: + - type: integer + - type: string + description: |- + StepInit defines the initial step to start from during rollout. + e.g. an int (5) or percentage of count of total matching clusters (25%) + x-kubernetes-int-or-string: true + stepLimit: + anyOf: + - type: integer + - type: string + description: |- + StepLimit defines the upper limit on stepSize during rollout. + If defined and computes to less than stepInit, step size can reach 100%; + meaning that no upper limit is set. + If stepIncrement is defined and stepLimit is omitted, step size can reach + 100%; meaning that no upper limit is set. + If StepIncrement is undefined and if stepLimit is omitted, step size is + defaulted to the value computed from stepInit. + e.g. an int (5) or percentage of count of total matching clusters (25%) + x-kubernetes-int-or-string: true + required: + - stepInit + type: object + type: object tlsConfig: description: TLSConfig contains the TLS configuration for a HelmChartProxy. properties: @@ -424,6 +512,13 @@ spec: by the controller. format: int64 type: integer + rollout: + properties: + count: + type: integer + stepSize: + type: integer + type: object type: object type: object served: true diff --git a/config/samples/withrollout.yaml b/config/samples/withrollout.yaml new file mode 100644 index 00000000..f7f23c56 --- /dev/null +++ b/config/samples/withrollout.yaml @@ -0,0 +1,20 @@ +apiVersion: addons.cluster.x-k8s.io/v1alpha1 +kind: HelmChartProxy +metadata: + name: metallb +spec: + rollout: + install: + stepInit: 10% + stepIncrement: 5% + stepLimit: 25% + upgrade: + stepInit: 10% + stepIncrement: 5% + stepLimit: 25% + clusterSelector: + matchLabels: + MetalLBChart: enabled + repoURL: https://metallb.github.io/metallb + chartName: metallb + releaseName: metallb diff --git a/controllers/helmchartproxy/helmchartproxy_controller.go b/controllers/helmchartproxy/helmchartproxy_controller.go index fc970451..ee71e105 100644 --- a/controllers/helmchartproxy/helmchartproxy_controller.go +++ b/controllers/helmchartproxy/helmchartproxy_controller.go @@ -18,12 +18,17 @@ package helmchartproxy import ( "context" + "slices" "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/utils/ptr" addonsv1alpha1 "sigs.k8s.io/cluster-api-addon-provider-helm/api/v1alpha1" clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" "sigs.k8s.io/cluster-api/util/conditions" @@ -45,6 +50,27 @@ type HelmChartProxyReconciler struct { WatchFilterValue string } +// helmReleaseProxyRolloutMeta is used to gather HelmReleaseProxy rollout +// metadata for matching clusters. +type helmReleaseProxyRolloutMeta struct { + cluster clusterv1.Cluster + + // Identifies whether HelmReleaseProxy exists for the cluster. + hrpExists bool + + // Identifies whether HelmReleaseProxy's ready condition is True. + hrpReady bool +} + +// installOrUpgrade defines the install vs upgrade rolling reconcile type event. +type installOrUpgrade string + +const ( + install installOrUpgrade = "install" + upgrade installOrUpgrade = "upgrade" + hrpRolloutCompletedMsg = "HelmChartProxy does not use rollout" +) + // SetupWithManager sets up the controller with the Manager. func (r *HelmChartProxyReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, options controller.Options) error { log := ctrl.LoggerFrom(ctx) @@ -75,7 +101,8 @@ func (r *HelmChartProxyReconciler) SetupWithManager(ctx context.Context, mgr ctr // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. -func (r *HelmChartProxyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, reterr error) { +func (r *HelmChartProxyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + var reterr error log := ctrl.LoggerFrom(ctx) log.V(2).Info("Beginning reconciliation for HelmChartProxy", "requestNamespace", req.Namespace, "requestName", req.Name) @@ -167,7 +194,7 @@ func (r *HelmChartProxyReconciler) Reconcile(ctx context.Context, req ctrl.Reque } log.V(2).Info("Reconciling HelmChartProxy", "randomName", helmChartProxy.Name) - err = r.reconcileNormal(ctx, helmChartProxy, clusterList.Items, releaseList.Items) + res, err := r.reconcileNormal(ctx, helmChartProxy, clusterList.Items, releaseList.Items) if err != nil { return ctrl.Result{}, err } @@ -179,12 +206,12 @@ func (r *HelmChartProxyReconciler) Reconcile(ctx context.Context, req ctrl.Reque return ctrl.Result{}, err } - return ctrl.Result{}, nil + return res, nil } // reconcileNormal handles the reconciliation of a HelmChartProxy when it is not being deleted. It takes a list of selected Clusters and HelmReleaseProxies // to uninstall the Helm chart from any Clusters that are no longer selected and to install or update the Helm chart on any Clusters that currently selected. -func (r *HelmChartProxyReconciler) reconcileNormal(ctx context.Context, helmChartProxy *addonsv1alpha1.HelmChartProxy, clusters []clusterv1.Cluster, helmReleaseProxies []addonsv1alpha1.HelmReleaseProxy) error { +func (r *HelmChartProxyReconciler) reconcileNormal(ctx context.Context, helmChartProxy *addonsv1alpha1.HelmChartProxy, clusters []clusterv1.Cluster, helmReleaseProxies []addonsv1alpha1.HelmReleaseProxy) (ctrl.Result, error) { log := ctrl.LoggerFrom(ctx) log.V(2).Info("Starting reconcileNormal for chart proxy", "name", helmChartProxy.Name, "strategy", helmChartProxy.Spec.ReconcileStrategy) @@ -193,23 +220,304 @@ func (r *HelmChartProxyReconciler) reconcileNormal(ctx context.Context, helmChar if helmChartProxy.Spec.ReconcileStrategy != string(addonsv1alpha1.ReconcileStrategyInstallOnce) { err := r.deleteOrphanedHelmReleaseProxies(ctx, helmChartProxy, clusters, helmReleaseProxies) if err != nil { - return err + return ctrl.Result{}, err } } - for _, cluster := range clusters { - // Don't reconcile if the Cluster is being deleted - if !cluster.DeletionTimestamp.IsZero() { - continue + if helmChartProxy.Spec.Rollout == nil { + // RolloutStepSize is undefined. Set HelmReleaseProxiesRolloutCompletedCondition to True with reason. + conditions.MarkTrueWithNegativePolarity( + helmChartProxy, + addonsv1alpha1.HelmReleaseProxiesRolloutCompletedCondition, + addonsv1alpha1.HelmReleaseProxiesRolloutUndefinedReason, + clusterv1.ConditionSeverityInfo, + hrpRolloutCompletedMsg, + ) + + for _, cluster := range clusters { + err := r.reconcileForCluster(ctx, helmChartProxy, cluster) + if err != nil { + return ctrl.Result{}, err + } + } + + return ctrl.Result{}, nil + } + + if helmChartProxy.GetGeneration() == 1 { + if helmChartProxy.Spec.Rollout.Install == nil { + // RolloutStepSize is undefined. Set HelmReleaseProxiesRolloutCompletedCondition to True with reason. + conditions.MarkTrueWithNegativePolarity( + helmChartProxy, + addonsv1alpha1.HelmReleaseProxiesRolloutCompletedCondition, + addonsv1alpha1.HelmReleaseProxiesRolloutUndefinedReason, + clusterv1.ConditionSeverityInfo, + hrpRolloutCompletedMsg, + ) + + for _, cluster := range clusters { + err := r.reconcileForCluster(ctx, helmChartProxy, cluster) + if err != nil { + return ctrl.Result{}, err + } + } + + return ctrl.Result{}, nil + } + + // rollout with install rollout options. + return r.rolloutReconcile(ctx, helmChartProxy, clusters, helmReleaseProxies, install) + } + + if helmChartProxy.Spec.Rollout.Upgrade == nil { + // RolloutStepSize is undefined. Set HelmReleaseProxiesRolloutCompletedCondition to True with reason. + conditions.MarkTrueWithNegativePolarity( + helmChartProxy, + addonsv1alpha1.HelmReleaseProxiesRolloutCompletedCondition, + addonsv1alpha1.HelmReleaseProxiesRolloutUndefinedReason, + clusterv1.ConditionSeverityInfo, + hrpRolloutCompletedMsg, + ) + + for _, cluster := range clusters { + err := r.reconcileForCluster(ctx, helmChartProxy, cluster) + if err != nil { + return ctrl.Result{}, err + } + } + + return ctrl.Result{}, nil + } + + // rollout with upgrade rollout options. + return r.rolloutReconcile(ctx, helmChartProxy, clusters, helmReleaseProxies, upgrade) +} + +// rolloutReconcile is used to rollout changes to matching clusters defined as +// per rollout options corresponding to the kind of change ie; install vs +// upgrade. +// +//nolint:gocyclo +func (r *HelmChartProxyReconciler) rolloutReconcile(ctx context.Context, helmChartProxy *addonsv1alpha1.HelmChartProxy, clusters []clusterv1.Cluster, helmReleaseProxies []addonsv1alpha1.HelmReleaseProxy, installOrUpgrade installOrUpgrade) (ctrl.Result, error) { + log := ctrl.LoggerFrom(ctx) + var rolloutOptions *addonsv1alpha1.RolloutOptions + + switch installOrUpgrade { + case install: + rolloutOptions = helmChartProxy.Spec.Rollout.Install + case upgrade: + rolloutOptions = helmChartProxy.Spec.Rollout.Upgrade + } + log.V(2).Info("Starting rolloutReconcile for chart proxy", "name", helmChartProxy.Name, "installOrUpgrade", installOrUpgrade) + + // This condition won't be true in normal cases. + if rolloutOptions == nil { + return ctrl.Result{}, nil + } + + var rolloutCount int + if helmChartProxy.Status.Rollout != nil { + rolloutCount = ptr.Deref(helmChartProxy.Status.Rollout.Count, rolloutCount) + } + + if len(clusters) == rolloutCount { + // RolloutStepSize is defined and all HelmReleaseProxies have been rolled out. + conditions.MarkTrue(helmChartProxy, addonsv1alpha1.HelmReleaseProxiesRolloutCompletedCondition) + + return ctrl.Result{}, nil + } + + // Set HelmReleaseProxiesRolloutCompletedCondition to false as + // HelmReleaseProxies are being rolled out. + conditions.MarkFalse( + helmChartProxy, + addonsv1alpha1.HelmReleaseProxiesRolloutCompletedCondition, + addonsv1alpha1.HelmReleaseProxiesRolloutNotCompleteReason, + clusterv1.ConditionSeverityInfo, + "%d Helm release proxies not yet rolled out", + len(clusters)-rolloutCount, + ) + + // Identifies clusters by their NamespacedName and gathers their + // helmReleaseProxyRolloutMeta. + clusterNnRolloutMeta := map[string]*helmReleaseProxyRolloutMeta{} + for _, c := range clusters { + nn := getNamespacedNameStringFor(c.Namespace, c.Name) + clusterNnRolloutMeta[nn] = &helmReleaseProxyRolloutMeta{ + cluster: c, + } + } + for _, h := range helmReleaseProxies { + ref := h.Spec.ClusterRef + nn := getNamespacedNameStringFor(ref.Namespace, ref.Name) + meta := clusterNnRolloutMeta[nn] + meta.hrpExists = true + meta.hrpReady = conditions.IsTrue(&h, addonsv1alpha1.HelmReleaseReadyCondition) + } + + // Sort helmReleaseProxy rollout metadata by cluster namespaced name to + // ensure orderliness. + rolloutMetaSorted := make([]*helmReleaseProxyRolloutMeta, len(clusterNnRolloutMeta)) + i := 0 + for _, m := range clusterNnRolloutMeta { + rolloutMetaSorted[i] = m + i++ + } + for m := range clusterNnRolloutMeta { + delete(clusterNnRolloutMeta, m) + } + + slices.SortStableFunc(rolloutMetaSorted, func(a, b *helmReleaseProxyRolloutMeta) int { + nnA := getNamespacedNameStringFor(a.cluster.Namespace, a.cluster.Name) + nnB := getNamespacedNameStringFor(b.cluster.Namespace, b.cluster.Name) + if nnA < nnB { + return -1 } - err := r.reconcileForCluster(ctx, helmChartProxy, cluster) + if nnA > nnB { + return 1 + } + + return 0 + }) + + // If HelmReleaseProxiesReadyCondition is Unknown, create the first batch + // of HelmReleaseProxies and exit. + if conditions.IsUnknown(helmChartProxy, addonsv1alpha1.HelmReleaseProxiesReadyCondition) { + if len(helmReleaseProxies) != 0 { + return ctrl.Result{Requeue: true}, nil + } + + count := 0 + stepSize, err := intstr.GetScaledValueFromIntOrPercent(rolloutOptions.StepInit, len(clusters), true) if err != nil { - return err + return ctrl.Result{}, err + } + + defer func() { + log.V(2).Info("Updating rollout status", "name", helmChartProxy.Name, "HelmReleaseProxiesReadyCondition", corev1.ConditionUnknown, "count", count, "stepSize", stepSize) + helmChartProxy.Status.Rollout = &addonsv1alpha1.RolloutStatus{Count: ptr.To(count), StepSize: ptr.To(stepSize)} + }() + + // If HelmReleaseProxiesReadyCondition is Unknown and the first batch of HelmReleaseProxies have + // been created, then exit early. + if stepSize == len(helmReleaseProxies) { + return ctrl.Result{Requeue: true}, nil + } + + for _, meta := range rolloutMetaSorted { + // The first batch of helmReleaseProxies have been reconciled. + if count >= stepSize { + return ctrl.Result{Requeue: true}, nil + } + + err := r.reconcileForCluster(ctx, helmChartProxy, meta.cluster) + log.V(2).Info("Reconciling for cluster", "name", helmChartProxy.Name, "HelmReleaseProxiesReadyCondition", corev1.ConditionUnknown, "cluster", meta.cluster.Name) + if err != nil { + return ctrl.Result{}, err + } + count++ } + + // In cases where the count of remaining HelmReleaseProxies to be rolled + // out is less than rollout step size. + return ctrl.Result{Requeue: true}, nil } - return nil + // If HelmReleaseProxiesReadyCondition is false, reconcile existing + // HelmReleaseProxies and exit. + if conditions.IsFalse(helmChartProxy, addonsv1alpha1.HelmReleaseProxiesReadyCondition) { + log.V(2).Info("HelmReleaseProxiesReady condition false; reconciling existing HelmReleaseProxies", "name", helmChartProxy.Name) + + for _, meta := range rolloutMetaSorted { + if meta.hrpExists { + err := r.reconcileForCluster(ctx, helmChartProxy, meta.cluster) + log.V(2).Info("Reconciling for cluster", "name", helmChartProxy.Name, "HelmReleaseProxiesReadyCondition", corev1.ConditionFalse, "cluster", meta.cluster.Name) + if err != nil { + return ctrl.Result{}, err + } + } + } + + return ctrl.Result{Requeue: true}, nil + } + + log.V(2).Info("HelmReleaseProxiesReady condition true; proceeding to reconcile the next batch of HelmReleaseProxies", "name", helmChartProxy.Name) + // HelmReleaseProxyReadyCondition is True; continue with reconciling the + // next batch of HelmReleaseProxies. + var oldStepSize int + if helmChartProxy.Status.Rollout != nil { + oldStepSize = ptr.Deref(helmChartProxy.Status.Rollout.StepSize, oldStepSize) + } + + var stepIncrement int + var err error + if rolloutOptions.StepIncrement != nil { + stepIncrement, err = intstr.GetScaledValueFromIntOrPercent(rolloutOptions.StepIncrement, len(clusters), true) + if err != nil { + return ctrl.Result{}, err + } + } + + var stepLimit int + if rolloutOptions.StepLimit != nil { + stepLimit, err = intstr.GetScaledValueFromIntOrPercent(rolloutOptions.StepLimit, len(clusters), true) + if err != nil { + return ctrl.Result{}, err + } + } + + var stepInit int + if rolloutOptions.StepInit != nil { + stepInit, err = intstr.GetScaledValueFromIntOrPercent(rolloutOptions.StepInit, len(clusters), true) + if err != nil { + return ctrl.Result{}, err + } + } + + stepSize := oldStepSize + stepIncrement + if stepLimit > stepInit && stepSize > stepLimit { + stepSize = stepLimit + } + + count := 0 + defer func() { + var oldCount int + if helmChartProxy.Status.Rollout != nil { + oldCount = ptr.Deref(helmChartProxy.Status.Rollout.Count, oldCount) + } + newCount := oldCount + count + log.V(2).Info("Updating rollout status", "name", helmChartProxy.Name, "HelmReleaseProxiesReadyCondition", corev1.ConditionTrue, "count", newCount, "stepSize", stepSize) + helmChartProxy.Status.Rollout = &addonsv1alpha1.RolloutStatus{Count: ptr.To(newCount), StepSize: ptr.To(stepSize)} + }() + + for _, meta := range rolloutMetaSorted { + // Exit if HelmReleaseProxyReadyCondition has not caught up to existing + // HelmReleaseProxies status. + if meta.hrpExists && !meta.hrpReady { + return ctrl.Result{Requeue: true}, nil + } + + // The next batch of helmReleaseProxies have been reconciled. + if count >= stepSize { + return ctrl.Result{Requeue: true}, nil + } + + // Skip reconciling the cluster if its HelmReleaseProxy already exists. + if meta.hrpExists { + continue + } + err := r.reconcileForCluster(ctx, helmChartProxy, meta.cluster) + log.V(2).Info("Reconciling for cluster", "name", helmChartProxy.Name, "HelmReleaseProxiesReadyCondition", corev1.ConditionTrue, "cluster", meta.cluster.Name) + if err != nil { + return ctrl.Result{}, err + } + count++ + } + + // In cases where the count of remaining HelmReleaseProxies to be rolled + // out is less than rollout step size. + return ctrl.Result{Requeue: true}, nil } // reconcileDelete handles the deletion of a HelmChartProxy. It takes a list of HelmReleaseProxies to uninstall the Helm chart from all selected Clusters. @@ -321,6 +629,7 @@ func patchHelmChartProxy(ctx context.Context, patchHelper *patch.Helper, helmCha conditions.WithConditions( addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition, addonsv1alpha1.HelmReleaseProxiesReadyCondition, + addonsv1alpha1.HelmReleaseProxiesRolloutCompletedCondition, ), ) @@ -332,6 +641,7 @@ func patchHelmChartProxy(ctx context.Context, patchHelper *patch.Helper, helmCha clusterv1.ReadyCondition, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition, addonsv1alpha1.HelmReleaseProxiesReadyCondition, + addonsv1alpha1.HelmReleaseProxiesRolloutCompletedCondition, }}, patch.WithStatusObservedGeneration{}, ) @@ -407,3 +717,8 @@ func HelmReleaseProxyToHelmChartProxyMapper(ctx context.Context, o client.Object return nil } + +// getNamespacedNameStringFor to retrieve the namespaced name as a string. +func getNamespacedNameStringFor(namespace, name string) string { + return types.NamespacedName{Namespace: namespace, Name: name}.String() +} diff --git a/controllers/helmchartproxy/helmchartproxy_controller_phases.go b/controllers/helmchartproxy/helmchartproxy_controller_phases.go index c5f9eb3f..b19520e7 100644 --- a/controllers/helmchartproxy/helmchartproxy_controller_phases.go +++ b/controllers/helmchartproxy/helmchartproxy_controller_phases.go @@ -57,6 +57,11 @@ func (r *HelmChartProxyReconciler) deleteOrphanedHelmReleaseProxies(ctx context. // reconcileForCluster will create or update a HelmReleaseProxy for the given cluster. func (r *HelmChartProxyReconciler) reconcileForCluster(ctx context.Context, helmChartProxy *addonsv1alpha1.HelmChartProxy, cluster clusterv1.Cluster) error { + // Don't reconcile if the Cluster is being deleted + if !cluster.DeletionTimestamp.IsZero() { + return nil + } + log := ctrl.LoggerFrom(ctx) // Don't reconcile if the Cluster or the helmChartProxy is paused. diff --git a/controllers/helmchartproxy/helmchartproxy_controller_test.go b/controllers/helmchartproxy/helmchartproxy_controller_test.go index 852f514d..091f822c 100644 --- a/controllers/helmchartproxy/helmchartproxy_controller_test.go +++ b/controllers/helmchartproxy/helmchartproxy_controller_test.go @@ -24,6 +24,7 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/client-go/kubernetes/scheme" "k8s.io/utils/ptr" addonsv1alpha1 "sigs.k8s.io/cluster-api-addon-provider-helm/api/v1alpha1" @@ -336,316 +337,1247 @@ var ( } ) +// TestRolloutReconcile cluster metadata. +var ( + cluster5 = &clusterv1.Cluster{ + TypeMeta: metav1.TypeMeta{ + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cluster-5", + Namespace: "test-namespace", + Labels: map[string]string{ + "test-label": "rollout-value", + }, + }, + Spec: clusterv1.ClusterSpec{ + ClusterNetwork: &clusterv1.ClusterNetwork{ + APIServerPort: ptr.To(int32(1234)), + ServiceDomain: "test-domain-1", + }, + }, + } + + cluster6 = &clusterv1.Cluster{ + TypeMeta: metav1.TypeMeta{ + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cluster-6", + Namespace: "test-namespace", + Labels: map[string]string{ + "test-label": "rollout-value", + }, + }, + Spec: clusterv1.ClusterSpec{ + ClusterNetwork: &clusterv1.ClusterNetwork{ + APIServerPort: ptr.To(int32(1234)), + ServiceDomain: "test-domain-1", + }, + }, + } + + cluster7 = &clusterv1.Cluster{ + TypeMeta: metav1.TypeMeta{ + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cluster-7", + Namespace: "test-namespace", + Labels: map[string]string{ + "test-label": "rollout-value", + }, + }, + Spec: clusterv1.ClusterSpec{ + ClusterNetwork: &clusterv1.ClusterNetwork{ + APIServerPort: ptr.To(int32(1234)), + ServiceDomain: "test-domain-1", + }, + }, + } + + cluster8 = &clusterv1.Cluster{ + TypeMeta: metav1.TypeMeta{ + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cluster-8", + Namespace: "test-namespace", + Labels: map[string]string{ + "test-label": "rollout-value", + }, + }, + Spec: clusterv1.ClusterSpec{ + ClusterNetwork: &clusterv1.ClusterNetwork{ + APIServerPort: ptr.To(int32(1234)), + ServiceDomain: "test-domain-1", + }, + }, + } + + hrpReady5 = &addonsv1alpha1.HelmReleaseProxy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-hrp-5", + Namespace: "test-namespace", + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: addonsv1alpha1.GroupVersion.String(), + Kind: "HelmChartProxy", + Name: "test-hcp", + Controller: ptr.To(true), + BlockOwnerDeletion: ptr.To(true), + }, + }, + Labels: map[string]string{ + clusterv1.ClusterNameLabel: "test-cluster-5", + addonsv1alpha1.HelmChartProxyLabelName: "test-hcp", + }, + Annotations: map[string]string{ + addonsv1alpha1.ReleaseSuccessfullyInstalledAnnotation: "true", + }, + }, + Spec: addonsv1alpha1.HelmReleaseProxySpec{ + ClusterRef: corev1.ObjectReference{ + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: "test-cluster-5", + Namespace: "test-namespace", + }, + ReleaseName: "test-release-name", + ChartName: "test-chart-name", + RepoURL: "https://test-repo-url", + ReleaseNamespace: "test-release-namespace", + Version: "test-version", + Values: "apiServerPort: 1234", + Options: addonsv1alpha1.HelmOptions{}, + }, + Status: addonsv1alpha1.HelmReleaseProxyStatus{ + Conditions: []clusterv1.Condition{ + { + Type: addonsv1alpha1.HelmReleaseReadyCondition, + Status: corev1.ConditionTrue, + }, + }, + }, + } + + hrpNotReady5 = &addonsv1alpha1.HelmReleaseProxy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-hrp-5", + Namespace: "test-namespace", + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: addonsv1alpha1.GroupVersion.String(), + Kind: "HelmChartProxy", + Name: "test-hcp", + Controller: ptr.To(true), + BlockOwnerDeletion: ptr.To(true), + }, + }, + Labels: map[string]string{ + clusterv1.ClusterNameLabel: "test-cluster-5", + addonsv1alpha1.HelmChartProxyLabelName: "test-hcp", + }, + Annotations: map[string]string{ + addonsv1alpha1.ReleaseSuccessfullyInstalledAnnotation: "true", + }, + }, + Spec: addonsv1alpha1.HelmReleaseProxySpec{ + ClusterRef: corev1.ObjectReference{ + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: "test-cluster-5", + Namespace: "test-namespace", + }, + ReleaseName: "test-release-name", + ChartName: "test-chart-name", + RepoURL: "https://test-repo-url", + ReleaseNamespace: "test-release-namespace", + Version: "test-version", + Values: "apiServerPort: 1234", + Options: addonsv1alpha1.HelmOptions{}, + }, + Status: addonsv1alpha1.HelmReleaseProxyStatus{ + Conditions: []clusterv1.Condition{ + { + Type: addonsv1alpha1.HelmReleaseReadyCondition, + Status: corev1.ConditionFalse, + Severity: clusterv1.ConditionSeverityInfo, + }, + }, + }, + } + + hrpReady6 = &addonsv1alpha1.HelmReleaseProxy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-hrp-6", + Namespace: "test-namespace", + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: addonsv1alpha1.GroupVersion.String(), + Kind: "HelmChartProxy", + Name: "test-hcp", + Controller: ptr.To(true), + BlockOwnerDeletion: ptr.To(true), + }, + }, + Labels: map[string]string{ + clusterv1.ClusterNameLabel: "test-cluster-6", + addonsv1alpha1.HelmChartProxyLabelName: "test-hcp", + }, + Annotations: map[string]string{ + addonsv1alpha1.ReleaseSuccessfullyInstalledAnnotation: "true", + }, + }, + Spec: addonsv1alpha1.HelmReleaseProxySpec{ + ClusterRef: corev1.ObjectReference{ + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: "test-cluster-6", + Namespace: "test-namespace", + }, + ReleaseName: "test-release-name", + ChartName: "test-chart-name", + RepoURL: "https://test-repo-url", + ReleaseNamespace: "test-release-namespace", + Version: "test-version", + Values: "apiServerPort: 1234", + Options: addonsv1alpha1.HelmOptions{}, + }, + Status: addonsv1alpha1.HelmReleaseProxyStatus{ + Conditions: []clusterv1.Condition{ + { + Type: addonsv1alpha1.HelmReleaseReadyCondition, + Status: corev1.ConditionTrue, + }, + }, + }, + } + + hrpReady7 = &addonsv1alpha1.HelmReleaseProxy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-hrp-7", + Namespace: "test-namespace", + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: addonsv1alpha1.GroupVersion.String(), + Kind: "HelmChartProxy", + Name: "test-hcp", + Controller: ptr.To(true), + BlockOwnerDeletion: ptr.To(true), + }, + }, + Labels: map[string]string{ + clusterv1.ClusterNameLabel: "test-cluster-7", + addonsv1alpha1.HelmChartProxyLabelName: "test-hcp", + }, + Annotations: map[string]string{ + addonsv1alpha1.ReleaseSuccessfullyInstalledAnnotation: "true", + }, + }, + Spec: addonsv1alpha1.HelmReleaseProxySpec{ + ClusterRef: corev1.ObjectReference{ + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: "test-cluster-7", + Namespace: "test-namespace", + }, + ReleaseName: "test-release-name", + ChartName: "test-chart-name", + RepoURL: "https://test-repo-url", + ReleaseNamespace: "test-release-namespace", + Version: "test-version", + Values: "apiServerPort: 1234", + Options: addonsv1alpha1.HelmOptions{}, + }, + Status: addonsv1alpha1.HelmReleaseProxyStatus{ + Conditions: []clusterv1.Condition{ + { + Type: addonsv1alpha1.HelmReleaseReadyCondition, + Status: corev1.ConditionTrue, + }, + }, + }, + } + + hrpReady8 = &addonsv1alpha1.HelmReleaseProxy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-hrp-8", + Namespace: "test-namespace", + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: addonsv1alpha1.GroupVersion.String(), + Kind: "HelmChartProxy", + Name: "test-hcp", + Controller: ptr.To(true), + BlockOwnerDeletion: ptr.To(true), + }, + }, + Labels: map[string]string{ + clusterv1.ClusterNameLabel: "test-cluster-8", + addonsv1alpha1.HelmChartProxyLabelName: "test-hcp", + }, + Annotations: map[string]string{ + addonsv1alpha1.ReleaseSuccessfullyInstalledAnnotation: "true", + }, + }, + Spec: addonsv1alpha1.HelmReleaseProxySpec{ + ClusterRef: corev1.ObjectReference{ + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: "test-cluster-8", + Namespace: "test-namespace", + }, + ReleaseName: "test-release-name", + ChartName: "test-chart-name", + RepoURL: "https://test-repo-url", + ReleaseNamespace: "test-release-namespace", + Version: "test-version", + Values: "apiServerPort: 1234", + Options: addonsv1alpha1.HelmOptions{}, + }, + Status: addonsv1alpha1.HelmReleaseProxyStatus{ + Conditions: []clusterv1.Condition{ + { + Type: addonsv1alpha1.HelmReleaseReadyCondition, + Status: corev1.ConditionTrue, + }, + }, + }, + } +) + +type rolloutProxyOption func(h *addonsv1alpha1.HelmChartProxy) + +func newRolloutProxy(opts ...rolloutProxyOption) *addonsv1alpha1.HelmChartProxy { + h := &addonsv1alpha1.HelmChartProxy{ + TypeMeta: metav1.TypeMeta{ + APIVersion: addonsv1alpha1.GroupVersion.String(), + Kind: "HelmChartProxy", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-hcp", + Namespace: "test-namespace", + Generation: 1, + }, + Spec: addonsv1alpha1.HelmChartProxySpec{ + ClusterSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "test-label": "rollout-value", + }, + }, + ReleaseName: "test-release-name", + ChartName: "test-chart-name", + RepoURL: "https://test-repo-url", + ReleaseNamespace: "test-release-namespace", + Version: "test-version", + ValuesTemplate: "apiServerPort: {{ .Cluster.spec.clusterNetwork.apiServerPort }}", + ReconcileStrategy: string(addonsv1alpha1.ReconcileStrategyContinuous), + Options: addonsv1alpha1.HelmOptions{}, + }, + } + + for _, o := range opts { + o(h) + } + + return h +} + +func withRollout(r *addonsv1alpha1.Rollout) rolloutProxyOption { + return func(h *addonsv1alpha1.HelmChartProxy) { + h.Spec.Rollout = r + } +} + +func withGeneration(g int64) rolloutProxyOption { + return func(h *addonsv1alpha1.HelmChartProxy) { + h.Generation = g + } +} + +func withRolloutStatus(r *addonsv1alpha1.RolloutStatus) rolloutProxyOption { + return func(h *addonsv1alpha1.HelmChartProxy) { + h.Status.Rollout = r + } +} + +func withConditions(cs []clusterv1.Condition) rolloutProxyOption { + return func(h *addonsv1alpha1.HelmChartProxy) { + h.Status.Conditions = cs + } +} + func TestReconcileNormal(t *testing.T) { t.Parallel() testcases := []struct { - name string - helmChartProxy *addonsv1alpha1.HelmChartProxy - objects []client.Object - expect func(g *WithT, c client.Client, hcp *addonsv1alpha1.HelmChartProxy) - expectedError string + name string + helmChartProxy *addonsv1alpha1.HelmChartProxy + objects []client.Object + expect func(g *WithT, c client.Client, hcp *addonsv1alpha1.HelmChartProxy) + expectedError string + }{ + { + name: "successfully select clusters and install HelmReleaseProxies for Continuous strategy", + helmChartProxy: continuousProxy, + objects: []client.Object{cluster1, cluster2, cluster3, cluster4}, + expect: func(g *WithT, c client.Client, hcp *addonsv1alpha1.HelmChartProxy) { + g.Expect(hcp.Status.MatchingClusters).To(BeEquivalentTo([]corev1.ObjectReference{ + { + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: "test-cluster-1", + Namespace: "test-namespace", + }, + { + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: "test-cluster-2", + Namespace: "test-namespace", + }, + })) + g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition)).To(BeTrue()) + g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition)).To(BeTrue()) + // This is false as the HelmReleaseProxies won't be ready until the HelmReleaseProxy controller runs. + g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxiesReadyCondition)).To(BeFalse()) + }, + expectedError: "", + }, + { + name: "successfully select clusters and install HelmReleaseProxies for unset strategy", + helmChartProxy: unsetProxy, + objects: []client.Object{cluster1, cluster2, cluster3, cluster4}, + expect: func(g *WithT, c client.Client, hcp *addonsv1alpha1.HelmChartProxy) { + g.Expect(hcp.Status.MatchingClusters).To(BeEquivalentTo([]corev1.ObjectReference{ + { + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: "test-cluster-1", + Namespace: "test-namespace", + }, + { + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: "test-cluster-2", + Namespace: "test-namespace", + }, + })) + g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition)).To(BeTrue()) + g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition)).To(BeTrue()) + // This is false as the HelmReleaseProxies won't be ready until the HelmReleaseProxy controller runs. + g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxiesReadyCondition)).To(BeFalse()) + }, + expectedError: "", + }, + { + name: "successfully select clusters and install HelmReleaseProxies for InstallOnce strategy", + helmChartProxy: installOnceProxy, + objects: []client.Object{cluster1, cluster2, cluster3, cluster4}, + expect: func(g *WithT, c client.Client, hcp *addonsv1alpha1.HelmChartProxy) { + g.Expect(hcp.Status.MatchingClusters).To(BeEquivalentTo([]corev1.ObjectReference{ + { + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: "test-cluster-1", + Namespace: "test-namespace", + }, + { + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: "test-cluster-2", + Namespace: "test-namespace", + }, + })) + g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition)).To(BeTrue()) + g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition)).To(BeTrue()) + // This is false as the HelmReleaseProxies won't be ready until the HelmReleaseProxy controller runs. + g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxiesReadyCondition)).To(BeFalse()) + }, + expectedError: "", + }, + { + name: "mark HelmChartProxy as ready once HelmReleaseProxies ready conditions are true for Continuous strategy", + helmChartProxy: continuousProxy, + objects: []client.Object{cluster1, cluster2, hrpReady1, hrpReady2}, + expect: func(g *WithT, c client.Client, hcp *addonsv1alpha1.HelmChartProxy) { + g.Expect(hcp.Status.MatchingClusters).To(BeEquivalentTo([]corev1.ObjectReference{ + { + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: "test-cluster-1", + Namespace: "test-namespace", + }, + { + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: "test-cluster-2", + Namespace: "test-namespace", + }, + })) + g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition)).To(BeTrue()) + g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition)).To(BeTrue()) + g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxiesReadyCondition)).To(BeTrue()) + g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxiesReadyCondition)).To(BeTrue()) + g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxiesRolloutCompletedCondition)).To(BeTrue()) + g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxiesRolloutCompletedCondition)).To(BeTrue()) + g.Expect(conditions.GetReason(hcp, addonsv1alpha1.HelmReleaseProxiesRolloutCompletedCondition)).To(BeIdenticalTo(addonsv1alpha1.HelmReleaseProxiesRolloutUndefinedReason)) + g.Expect(conditions.Has(hcp, clusterv1.ReadyCondition)).To(BeTrue()) + g.Expect(conditions.IsTrue(hcp, clusterv1.ReadyCondition)).To(BeTrue()) + g.Expect(hcp.Status.ObservedGeneration).To(Equal(hcp.Generation)) + }, + expectedError: "", + }, + { + name: "mark HelmChartProxy as ready once HelmReleaseProxies ready conditions are true for unset strategy", + helmChartProxy: unsetProxy, + objects: []client.Object{cluster1, cluster2, hrpReady1, hrpReady2}, + expect: func(g *WithT, c client.Client, hcp *addonsv1alpha1.HelmChartProxy) { + g.Expect(hcp.Status.MatchingClusters).To(BeEquivalentTo([]corev1.ObjectReference{ + { + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: "test-cluster-1", + Namespace: "test-namespace", + }, + { + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: "test-cluster-2", + Namespace: "test-namespace", + }, + })) + g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition)).To(BeTrue()) + g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition)).To(BeTrue()) + g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxiesReadyCondition)).To(BeTrue()) + g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxiesReadyCondition)).To(BeTrue()) + g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxiesRolloutCompletedCondition)).To(BeTrue()) + g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxiesRolloutCompletedCondition)).To(BeTrue()) + g.Expect(conditions.GetReason(hcp, addonsv1alpha1.HelmReleaseProxiesRolloutCompletedCondition)).To(BeIdenticalTo(addonsv1alpha1.HelmReleaseProxiesRolloutUndefinedReason)) + g.Expect(conditions.Has(hcp, clusterv1.ReadyCondition)).To(BeTrue()) + g.Expect(conditions.IsTrue(hcp, clusterv1.ReadyCondition)).To(BeTrue()) + g.Expect(hcp.Status.ObservedGeneration).To(Equal(hcp.Generation)) + }, + expectedError: "", + }, + { + // TODO: how to make sure HelmReleaseProxySpecsUpToDateCondition stays true even after they get deleted? + name: "mark HelmChartProxy as ready once HelmReleaseProxies ready conditions are true for InstallOnce strategy", + helmChartProxy: installOnceProxy, + objects: []client.Object{cluster1, cluster2, hrpReady1, hrpReady2}, + expect: func(g *WithT, c client.Client, hcp *addonsv1alpha1.HelmChartProxy) { + g.Expect(hcp.Status.MatchingClusters).To(BeEquivalentTo([]corev1.ObjectReference{ + { + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: "test-cluster-1", + Namespace: "test-namespace", + }, + { + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: "test-cluster-2", + Namespace: "test-namespace", + }, + })) + g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition)).To(BeTrue()) + g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition)).To(BeTrue()) + g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxiesReadyCondition)).To(BeTrue()) + g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxiesReadyCondition)).To(BeTrue()) + g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxiesRolloutCompletedCondition)).To(BeTrue()) + g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxiesRolloutCompletedCondition)).To(BeTrue()) + g.Expect(conditions.GetReason(hcp, addonsv1alpha1.HelmReleaseProxiesRolloutCompletedCondition)).To(BeIdenticalTo(addonsv1alpha1.HelmReleaseProxiesRolloutUndefinedReason)) + g.Expect(conditions.Has(hcp, clusterv1.ReadyCondition)).To(BeTrue()) + g.Expect(conditions.IsTrue(hcp, clusterv1.ReadyCondition)).To(BeTrue()) + g.Expect(hcp.Status.ObservedGeneration).To(Equal(hcp.Generation)) + }, + expectedError: "", + }, + { + name: "successfully delete orphaned HelmReleaseProxies for Continuous strategy", + helmChartProxy: continuousProxy, + objects: []client.Object{hrpReady1, hrpReady2}, + expect: func(g *WithT, c client.Client, hcp *addonsv1alpha1.HelmChartProxy) { + g.Expect(hcp.Status.MatchingClusters).To(BeEmpty()) + g.Expect(c.Get(ctx, client.ObjectKey{Namespace: hrpReady1.Namespace, Name: hrpReady1.Name}, &addonsv1alpha1.HelmReleaseProxy{})).ToNot(Succeed()) + g.Expect(c.Get(ctx, client.ObjectKey{Namespace: hrpReady2.Namespace, Name: hrpReady2.Name}, &addonsv1alpha1.HelmReleaseProxy{})).ToNot(Succeed()) + + // Vacuously true as there are no HRPs + g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition)).To(BeTrue()) + g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition)).To(BeTrue()) + g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxiesReadyCondition)).To(BeTrue()) + g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxiesReadyCondition)).To(BeTrue()) + g.Expect(conditions.Has(hcp, clusterv1.ReadyCondition)).To(BeTrue()) + g.Expect(conditions.IsTrue(hcp, clusterv1.ReadyCondition)).To(BeTrue()) + g.Expect(hcp.Status.ObservedGeneration).To(Equal(hcp.Generation)) + }, + expectedError: "", + }, + { + name: "successfully delete orphaned HelmReleaseProxies for unset strategy", + helmChartProxy: unsetProxy, + objects: []client.Object{hrpReady1, hrpReady2}, + expect: func(g *WithT, c client.Client, hcp *addonsv1alpha1.HelmChartProxy) { + g.Expect(hcp.Status.MatchingClusters).To(BeEmpty()) + g.Expect(c.Get(ctx, client.ObjectKey{Namespace: hrpReady1.Namespace, Name: hrpReady1.Name}, &addonsv1alpha1.HelmReleaseProxy{})).ToNot(Succeed()) + g.Expect(c.Get(ctx, client.ObjectKey{Namespace: hrpReady2.Namespace, Name: hrpReady2.Name}, &addonsv1alpha1.HelmReleaseProxy{})).ToNot(Succeed()) + + // Vacuously true as there are no HRPs + g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition)).To(BeTrue()) + g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition)).To(BeTrue()) + g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxiesReadyCondition)).To(BeTrue()) + g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxiesReadyCondition)).To(BeTrue()) + g.Expect(conditions.Has(hcp, clusterv1.ReadyCondition)).To(BeTrue()) + g.Expect(conditions.IsTrue(hcp, clusterv1.ReadyCondition)).To(BeTrue()) + g.Expect(hcp.Status.ObservedGeneration).To(Equal(hcp.Generation)) + }, + expectedError: "", + }, + { + name: "do not delete orphaned HelmReleaseProxies for InstallOnce strategy", + helmChartProxy: installOnceProxy, + objects: []client.Object{hrpReady1, hrpReady2}, + expect: func(g *WithT, c client.Client, hcp *addonsv1alpha1.HelmChartProxy) { + g.Expect(hcp.Status.MatchingClusters).To(BeEmpty()) + g.Expect(c.Get(ctx, client.ObjectKey{Namespace: hrpReady1.Namespace, Name: hrpReady1.Name}, &addonsv1alpha1.HelmReleaseProxy{})).To(Succeed()) + g.Expect(c.Get(ctx, client.ObjectKey{Namespace: hrpReady2.Namespace, Name: hrpReady2.Name}, &addonsv1alpha1.HelmReleaseProxy{})).To(Succeed()) + + g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition)).To(BeTrue()) + g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition)).To(BeTrue()) + g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxiesReadyCondition)).To(BeTrue()) + g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxiesReadyCondition)).To(BeTrue()) + g.Expect(conditions.Has(hcp, clusterv1.ReadyCondition)).To(BeTrue()) + g.Expect(conditions.IsTrue(hcp, clusterv1.ReadyCondition)).To(BeTrue()) + g.Expect(hcp.Status.ObservedGeneration).To(Equal(hcp.Generation)) + }, + expectedError: "", + }, + { + name: "do not update HelmReleaseProxies when HelmChartProxy changes for InstallOnce strategy", + helmChartProxy: updatedInstallOnceProxy, + objects: []client.Object{hrpReady1, hrpReady2, cluster1, cluster2}, + expect: func(g *WithT, c client.Client, hcp *addonsv1alpha1.HelmChartProxy) { + g.Expect(hcp.Status.MatchingClusters).To(BeEquivalentTo([]corev1.ObjectReference{ + { + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: "test-cluster-1", + Namespace: "test-namespace", + }, + { + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: "test-cluster-2", + Namespace: "test-namespace", + }, + })) + + hrp1Result := &addonsv1alpha1.HelmReleaseProxy{} + g.Expect(c.Get(ctx, client.ObjectKey{Namespace: hrpReady1.Namespace, Name: hrpReady1.Name}, hrp1Result)).To(Succeed()) + g.Expect(cmp.Diff(hrp1Result, hrpReady1)).To(BeEmpty()) + + hrp2Result := &addonsv1alpha1.HelmReleaseProxy{} + g.Expect(c.Get(ctx, client.ObjectKey{Namespace: hrpReady2.Namespace, Name: hrpReady2.Name}, hrp2Result)).To(Succeed()) + g.Expect(cmp.Diff(hrp2Result, hrpReady2)).To(BeEmpty()) + + g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition)).To(BeTrue()) + g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition)).To(BeTrue()) + g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxiesReadyCondition)).To(BeTrue()) + g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxiesReadyCondition)).To(BeTrue()) + g.Expect(conditions.Has(hcp, clusterv1.ReadyCondition)).To(BeTrue()) + g.Expect(conditions.IsTrue(hcp, clusterv1.ReadyCondition)).To(BeTrue()) + g.Expect(hcp.Status.ObservedGeneration).To(Equal(hcp.Generation)) + }, + expectedError: "", + }, + { + name: "mark HelmChartProxy as ready once HelmReleaseProxies ready conditions are true ignoring paused clusters", + helmChartProxy: continuousProxy, + objects: []client.Object{cluster1, cluster2, clusterPaused, hrpReady1, hrpReady2}, + expect: func(g *WithT, c client.Client, hcp *addonsv1alpha1.HelmChartProxy) { + g.Expect(hcp.Status.MatchingClusters).To(BeEquivalentTo([]corev1.ObjectReference{ + { + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: "test-cluster-1", + Namespace: "test-namespace", + }, + { + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: "test-cluster-2", + Namespace: "test-namespace", + }, + { + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: "test-cluster-paused", + Namespace: "test-namespace", + }, + })) + g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition)).To(BeTrue()) + g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition)).To(BeTrue()) + g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxiesReadyCondition)).To(BeTrue()) + g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxiesReadyCondition)).To(BeTrue()) + g.Expect(conditions.Has(hcp, clusterv1.ReadyCondition)).To(BeTrue()) + g.Expect(conditions.IsTrue(hcp, clusterv1.ReadyCondition)).To(BeTrue()) + g.Expect(hcp.Status.ObservedGeneration).To(Equal(hcp.Generation)) + hrpList := &addonsv1alpha1.HelmReleaseProxyList{} + g.Expect(c.List(ctx, hrpList, &client.ListOptions{Namespace: hcp.Namespace})).To(Succeed()) + + // There should be 2 HelmReleaseProxies as the paused cluster should not have a HelmReleaseProxy. + g.Expect(hrpList.Items).To(HaveLen(2)) + }, + expectedError: "", + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + t.Parallel() + request := reconcile.Request{ + NamespacedName: util.ObjectKey(tc.helmChartProxy), + } + + tc.objects = append(tc.objects, tc.helmChartProxy) + r := &HelmChartProxyReconciler{ + Client: fake.NewClientBuilder(). + WithScheme(fakeScheme). + WithObjects(tc.objects...). + WithStatusSubresource(&addonsv1alpha1.HelmChartProxy{}). + WithStatusSubresource(&addonsv1alpha1.HelmReleaseProxy{}). + Build(), + } + result, err := r.Reconcile(ctx, request) + + if tc.expectedError != "" { + g.Expect(err).To(HaveOccurred()) + g.Expect(err).To(MatchError(tc.expectedError), err.Error()) + } else { + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(result).To(Equal(reconcile.Result{})) + + hcp := &addonsv1alpha1.HelmChartProxy{} + g.Expect(r.Client.Get(ctx, request.NamespacedName, hcp)).To(Succeed()) + + tc.expect(g, r.Client, hcp) + } + }) + } +} + +func TestRolloutReconcile(t *testing.T) { + t.Parallel() + + testcases := []struct { + name string + helmChartProxy *addonsv1alpha1.HelmChartProxy + objects []client.Object + expect func(g *WithT, c client.Client, hcp *addonsv1alpha1.HelmChartProxy) + expectedError string + reconcileResult reconcile.Result }{ { - name: "successfully select clusters and install HelmReleaseProxies for Continuous strategy", - helmChartProxy: continuousProxy, - objects: []client.Object{cluster1, cluster2, cluster3, cluster4}, + name: "when rollout is not used, exit early", + helmChartProxy: continuousProxy, + objects: []client.Object{}, + expectedError: "", + expect: func(g *WithT, c client.Client, hcp *addonsv1alpha1.HelmChartProxy) {}, + reconcileResult: reconcile.Result{}, + }, + { + name: "successfully set rollout completion condition to true when all hrp are rolled out", + + helmChartProxy: newRolloutProxy( + withRollout(&addonsv1alpha1.Rollout{Install: &addonsv1alpha1.RolloutOptions{StepInit: &intstr.IntOrString{Type: intstr.String, StrVal: "25%"}}}), + withRolloutStatus(&addonsv1alpha1.RolloutStatus{StepSize: ptr.To(1), Count: ptr.To(4)}), + ), + objects: []client.Object{cluster5, cluster6, cluster7, cluster8, hrpReady5, hrpReady6, hrpReady7, hrpReady8}, + expect: func(g *WithT, c client.Client, hcp *addonsv1alpha1.HelmChartProxy) { + g.Expect(hcp.Status.MatchingClusters).To(BeEquivalentTo([]corev1.ObjectReference{ + { + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: "test-cluster-5", + Namespace: "test-namespace", + }, + { + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: "test-cluster-6", + Namespace: "test-namespace", + }, + { + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: "test-cluster-7", + Namespace: "test-namespace", + }, + { + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: "test-cluster-8", + Namespace: "test-namespace", + }, + })) + g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxiesRolloutCompletedCondition)).To(BeTrue()) + g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxiesRolloutCompletedCondition)).To(BeTrue()) + g.Expect(hcp.Status.ObservedGeneration).To(Equal(hcp.Generation)) + }, + expectedError: "", + }, + { + name: "successfully set rollout completion condition to false when not all hrp are rolled out", + helmChartProxy: newRolloutProxy( + withRollout(&addonsv1alpha1.Rollout{Install: &addonsv1alpha1.RolloutOptions{StepInit: &intstr.IntOrString{Type: intstr.String, StrVal: "25%"}}}), + withRolloutStatus(&addonsv1alpha1.RolloutStatus{StepSize: ptr.To(1), Count: ptr.To(1)}), + ), + objects: []client.Object{cluster5, cluster6, cluster7, cluster8, hrpNotReady5}, expect: func(g *WithT, c client.Client, hcp *addonsv1alpha1.HelmChartProxy) { g.Expect(hcp.Status.MatchingClusters).To(BeEquivalentTo([]corev1.ObjectReference{ { APIVersion: clusterv1.GroupVersion.String(), Kind: "Cluster", - Name: "test-cluster-1", + Name: "test-cluster-5", Namespace: "test-namespace", }, { APIVersion: clusterv1.GroupVersion.String(), Kind: "Cluster", - Name: "test-cluster-2", + Name: "test-cluster-6", + Namespace: "test-namespace", + }, + { + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: "test-cluster-7", + Namespace: "test-namespace", + }, + { + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: "test-cluster-8", Namespace: "test-namespace", }, })) - g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition)).To(BeTrue()) - g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition)).To(BeTrue()) - // This is false as the HelmReleaseProxies won't be ready until the HelmReleaseProxy controller runs. - g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxiesReadyCondition)).To(BeFalse()) + g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxiesRolloutCompletedCondition)).To(BeTrue()) + g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxiesRolloutCompletedCondition)).To(BeFalse()) + g.Expect(hcp.Status.ObservedGeneration).To(Equal(hcp.Generation)) }, - expectedError: "", + expectedError: "", + reconcileResult: reconcile.Result{Requeue: true}, }, { - name: "successfully select clusters and install HelmReleaseProxies for unset strategy", - helmChartProxy: unsetProxy, - objects: []client.Object{cluster1, cluster2, cluster3, cluster4}, + name: "when HelmReleaseProxiesReadyCondition is unknown, requeue early if hrp and has non-zero hrps", + helmChartProxy: newRolloutProxy( + withRollout(&addonsv1alpha1.Rollout{Install: &addonsv1alpha1.RolloutOptions{StepInit: &intstr.IntOrString{Type: intstr.String, StrVal: "25%"}}}), + withRolloutStatus(&addonsv1alpha1.RolloutStatus{StepSize: ptr.To(1), Count: ptr.To(0)}), + ), + objects: []client.Object{cluster5, cluster6, cluster7, cluster8, hrpNotReady5}, expect: func(g *WithT, c client.Client, hcp *addonsv1alpha1.HelmChartProxy) { g.Expect(hcp.Status.MatchingClusters).To(BeEquivalentTo([]corev1.ObjectReference{ { APIVersion: clusterv1.GroupVersion.String(), Kind: "Cluster", - Name: "test-cluster-1", + Name: "test-cluster-5", Namespace: "test-namespace", }, { APIVersion: clusterv1.GroupVersion.String(), Kind: "Cluster", - Name: "test-cluster-2", + Name: "test-cluster-6", + Namespace: "test-namespace", + }, + { + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: "test-cluster-7", + Namespace: "test-namespace", + }, + { + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: "test-cluster-8", Namespace: "test-namespace", }, })) - g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition)).To(BeTrue()) - g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition)).To(BeTrue()) - // This is false as the HelmReleaseProxies won't be ready until the HelmReleaseProxy controller runs. - g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxiesReadyCondition)).To(BeFalse()) + g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxiesRolloutCompletedCondition)).To(BeTrue()) + g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxiesRolloutCompletedCondition)).To(BeFalse()) + g.Expect(hcp.Status.ObservedGeneration).To(Equal(hcp.Generation)) }, - expectedError: "", + expectedError: "", + reconcileResult: reconcile.Result{Requeue: true}, }, { - name: "successfully select clusters and install HelmReleaseProxies for InstallOnce strategy", - helmChartProxy: installOnceProxy, - objects: []client.Object{cluster1, cluster2, cluster3, cluster4}, + name: "when HelmReleaseProxiesReadyCondition is unknown, requeue with count of 1 and step size of 1 when hcp rollout step init is 25% and has zero hrps", + helmChartProxy: newRolloutProxy( + withRollout(&addonsv1alpha1.Rollout{Install: &addonsv1alpha1.RolloutOptions{StepInit: &intstr.IntOrString{Type: intstr.String, StrVal: "25%"}}}), + withRolloutStatus(&addonsv1alpha1.RolloutStatus{StepSize: ptr.To(0), Count: ptr.To(0)}), + ), + objects: []client.Object{cluster5, cluster6, cluster7, cluster8}, expect: func(g *WithT, c client.Client, hcp *addonsv1alpha1.HelmChartProxy) { g.Expect(hcp.Status.MatchingClusters).To(BeEquivalentTo([]corev1.ObjectReference{ { APIVersion: clusterv1.GroupVersion.String(), Kind: "Cluster", - Name: "test-cluster-1", + Name: "test-cluster-5", Namespace: "test-namespace", }, { APIVersion: clusterv1.GroupVersion.String(), Kind: "Cluster", - Name: "test-cluster-2", + Name: "test-cluster-6", + Namespace: "test-namespace", + }, + { + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: "test-cluster-7", + Namespace: "test-namespace", + }, + { + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: "test-cluster-8", Namespace: "test-namespace", }, })) - g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition)).To(BeTrue()) - g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition)).To(BeTrue()) - // This is false as the HelmReleaseProxies won't be ready until the HelmReleaseProxy controller runs. - g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxiesReadyCondition)).To(BeFalse()) + g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxiesRolloutCompletedCondition)).To(BeTrue()) + g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxiesRolloutCompletedCondition)).To(BeFalse()) + g.Expect((hcp.Status.Rollout.Count)).To(Equal(ptr.To(1))) + g.Expect((hcp.Status.Rollout.StepSize)).To(Equal(ptr.To(1))) + g.Expect(hcp.Status.ObservedGeneration).To(Equal(hcp.Generation)) }, - expectedError: "", + expectedError: "", + reconcileResult: reconcile.Result{Requeue: true}, }, { - name: "mark HelmChartProxy as ready once HelmReleaseProxies ready conditions are true for Continuous strategy", - helmChartProxy: continuousProxy, - objects: []client.Object{cluster1, cluster2, hrpReady1, hrpReady2}, + name: "when HelmReleaseProxiesReadyCondition is unknown, requeue with count of 2 and step size of 2 when hcp rollout step init is 50% and has zero hrps", + helmChartProxy: newRolloutProxy( + withRollout(&addonsv1alpha1.Rollout{Install: &addonsv1alpha1.RolloutOptions{StepInit: &intstr.IntOrString{Type: intstr.String, StrVal: "50%"}}}), + withRolloutStatus(&addonsv1alpha1.RolloutStatus{StepSize: ptr.To(0), Count: ptr.To(0)}), + ), + objects: []client.Object{cluster5, cluster6, cluster7, cluster8}, expect: func(g *WithT, c client.Client, hcp *addonsv1alpha1.HelmChartProxy) { g.Expect(hcp.Status.MatchingClusters).To(BeEquivalentTo([]corev1.ObjectReference{ { APIVersion: clusterv1.GroupVersion.String(), Kind: "Cluster", - Name: "test-cluster-1", + Name: "test-cluster-5", Namespace: "test-namespace", }, { APIVersion: clusterv1.GroupVersion.String(), Kind: "Cluster", - Name: "test-cluster-2", + Name: "test-cluster-6", + Namespace: "test-namespace", + }, + { + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: "test-cluster-7", + Namespace: "test-namespace", + }, + { + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: "test-cluster-8", Namespace: "test-namespace", }, })) - g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition)).To(BeTrue()) - g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition)).To(BeTrue()) - g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxiesReadyCondition)).To(BeTrue()) - g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxiesReadyCondition)).To(BeTrue()) - g.Expect(conditions.Has(hcp, clusterv1.ReadyCondition)).To(BeTrue()) - g.Expect(conditions.IsTrue(hcp, clusterv1.ReadyCondition)).To(BeTrue()) + g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxiesRolloutCompletedCondition)).To(BeTrue()) + g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxiesRolloutCompletedCondition)).To(BeFalse()) + g.Expect((hcp.Status.Rollout.Count)).To(Equal(ptr.To(2))) + g.Expect((hcp.Status.Rollout.StepSize)).To(Equal(ptr.To(2))) g.Expect(hcp.Status.ObservedGeneration).To(Equal(hcp.Generation)) }, - expectedError: "", + expectedError: "", + reconcileResult: reconcile.Result{Requeue: true}, }, { - name: "mark HelmChartProxy as ready once HelmReleaseProxies ready conditions are true for unset strategy", - helmChartProxy: unsetProxy, - objects: []client.Object{cluster1, cluster2, hrpReady1, hrpReady2}, + name: "when HelmReleaseProxiesReadyCondition is false, reconciles existing hrps and keeps count to 1 and step size to 1", + helmChartProxy: newRolloutProxy( + withRollout(&addonsv1alpha1.Rollout{Install: &addonsv1alpha1.RolloutOptions{StepInit: &intstr.IntOrString{Type: intstr.String, StrVal: "25%"}}}), + withRolloutStatus(&addonsv1alpha1.RolloutStatus{StepSize: ptr.To(1), Count: ptr.To(1)}), + withConditions( + []clusterv1.Condition{ + { + Type: addonsv1alpha1.HelmReleaseProxiesReadyCondition, + Status: corev1.ConditionFalse, + Severity: clusterv1.ConditionSeverityInfo, + }, + }, + ), + ), + objects: []client.Object{cluster5, cluster6, cluster7, cluster8, hrpNotReady5}, expect: func(g *WithT, c client.Client, hcp *addonsv1alpha1.HelmChartProxy) { g.Expect(hcp.Status.MatchingClusters).To(BeEquivalentTo([]corev1.ObjectReference{ { APIVersion: clusterv1.GroupVersion.String(), Kind: "Cluster", - Name: "test-cluster-1", + Name: "test-cluster-5", Namespace: "test-namespace", }, { APIVersion: clusterv1.GroupVersion.String(), Kind: "Cluster", - Name: "test-cluster-2", + Name: "test-cluster-6", + Namespace: "test-namespace", + }, + { + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: "test-cluster-7", + Namespace: "test-namespace", + }, + { + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: "test-cluster-8", Namespace: "test-namespace", }, })) - g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition)).To(BeTrue()) - g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition)).To(BeTrue()) - g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxiesReadyCondition)).To(BeTrue()) - g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxiesReadyCondition)).To(BeTrue()) - g.Expect(conditions.Has(hcp, clusterv1.ReadyCondition)).To(BeTrue()) - g.Expect(conditions.IsTrue(hcp, clusterv1.ReadyCondition)).To(BeTrue()) + g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxiesRolloutCompletedCondition)).To(BeTrue()) + g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxiesRolloutCompletedCondition)).To(BeFalse()) + g.Expect((hcp.Status.Rollout.Count)).To(Equal(ptr.To(1))) + g.Expect((hcp.Status.Rollout.StepSize)).To(Equal(ptr.To(1))) g.Expect(hcp.Status.ObservedGeneration).To(Equal(hcp.Generation)) }, - expectedError: "", + expectedError: "", + reconcileResult: reconcile.Result{Requeue: true}, }, { - // TODO: how to make sure HelmReleaseProxySpecsUpToDateCondition stays true even after they get deleted? - name: "mark HelmChartProxy as ready once HelmReleaseProxies ready conditions are true for InstallOnce strategy", - helmChartProxy: installOnceProxy, - objects: []client.Object{cluster1, cluster2, hrpReady1, hrpReady2}, + name: "when HelmReleaseProxiesReadyCondition is true and 1 out of 4 hrp is rolled out and ready, it rolls out 2 more, sets count to 3 and step size to 2", + helmChartProxy: newRolloutProxy( + withRollout(&addonsv1alpha1.Rollout{Install: &addonsv1alpha1.RolloutOptions{ + StepInit: &intstr.IntOrString{Type: intstr.Int, IntVal: 1}, + StepIncrement: &intstr.IntOrString{Type: intstr.Int, IntVal: 1}, + StepLimit: &intstr.IntOrString{Type: intstr.Int, IntVal: 2}, + }}), + withRolloutStatus(&addonsv1alpha1.RolloutStatus{StepSize: ptr.To(1), Count: ptr.To(1)}), + withConditions( + []clusterv1.Condition{ + { + Type: addonsv1alpha1.HelmReleaseProxiesReadyCondition, + Status: corev1.ConditionTrue, + }, + }, + ), + ), + objects: []client.Object{cluster5, cluster6, cluster7, cluster8, hrpReady5}, expect: func(g *WithT, c client.Client, hcp *addonsv1alpha1.HelmChartProxy) { g.Expect(hcp.Status.MatchingClusters).To(BeEquivalentTo([]corev1.ObjectReference{ { APIVersion: clusterv1.GroupVersion.String(), Kind: "Cluster", - Name: "test-cluster-1", + Name: "test-cluster-5", Namespace: "test-namespace", }, { APIVersion: clusterv1.GroupVersion.String(), Kind: "Cluster", - Name: "test-cluster-2", + Name: "test-cluster-6", + Namespace: "test-namespace", + }, + { + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: "test-cluster-7", + Namespace: "test-namespace", + }, + { + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: "test-cluster-8", Namespace: "test-namespace", }, })) - g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition)).To(BeTrue()) - g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition)).To(BeTrue()) - g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxiesReadyCondition)).To(BeTrue()) - g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxiesReadyCondition)).To(BeTrue()) - g.Expect(conditions.Has(hcp, clusterv1.ReadyCondition)).To(BeTrue()) - g.Expect(conditions.IsTrue(hcp, clusterv1.ReadyCondition)).To(BeTrue()) - g.Expect(hcp.Status.ObservedGeneration).To(Equal(hcp.Generation)) - }, - expectedError: "", - }, - { - name: "successfully delete orphaned HelmReleaseProxies for Continuous strategy", - helmChartProxy: continuousProxy, - objects: []client.Object{hrpReady1, hrpReady2}, - expect: func(g *WithT, c client.Client, hcp *addonsv1alpha1.HelmChartProxy) { - g.Expect(hcp.Status.MatchingClusters).To(BeEmpty()) - g.Expect(c.Get(ctx, client.ObjectKey{Namespace: hrpReady1.Namespace, Name: hrpReady1.Name}, &addonsv1alpha1.HelmReleaseProxy{})).ToNot(Succeed()) - g.Expect(c.Get(ctx, client.ObjectKey{Namespace: hrpReady2.Namespace, Name: hrpReady2.Name}, &addonsv1alpha1.HelmReleaseProxy{})).ToNot(Succeed()) - - // Vacuously true as there are no HRPs - g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition)).To(BeTrue()) - g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition)).To(BeTrue()) - g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxiesReadyCondition)).To(BeTrue()) - g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxiesReadyCondition)).To(BeTrue()) - g.Expect(conditions.Has(hcp, clusterv1.ReadyCondition)).To(BeTrue()) - g.Expect(conditions.IsTrue(hcp, clusterv1.ReadyCondition)).To(BeTrue()) - g.Expect(hcp.Status.ObservedGeneration).To(Equal(hcp.Generation)) - }, - expectedError: "", - }, - { - name: "successfully delete orphaned HelmReleaseProxies for unset strategy", - helmChartProxy: unsetProxy, - objects: []client.Object{hrpReady1, hrpReady2}, - expect: func(g *WithT, c client.Client, hcp *addonsv1alpha1.HelmChartProxy) { - g.Expect(hcp.Status.MatchingClusters).To(BeEmpty()) - g.Expect(c.Get(ctx, client.ObjectKey{Namespace: hrpReady1.Namespace, Name: hrpReady1.Name}, &addonsv1alpha1.HelmReleaseProxy{})).ToNot(Succeed()) - g.Expect(c.Get(ctx, client.ObjectKey{Namespace: hrpReady2.Namespace, Name: hrpReady2.Name}, &addonsv1alpha1.HelmReleaseProxy{})).ToNot(Succeed()) - - // Vacuously true as there are no HRPs - g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition)).To(BeTrue()) - g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition)).To(BeTrue()) - g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxiesReadyCondition)).To(BeTrue()) - g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxiesReadyCondition)).To(BeTrue()) - g.Expect(conditions.Has(hcp, clusterv1.ReadyCondition)).To(BeTrue()) - g.Expect(conditions.IsTrue(hcp, clusterv1.ReadyCondition)).To(BeTrue()) + g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxiesRolloutCompletedCondition)).To(BeTrue()) + g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxiesRolloutCompletedCondition)).To(BeFalse()) + g.Expect((hcp.Status.Rollout.Count)).To(Equal(ptr.To(3))) + g.Expect((hcp.Status.Rollout.StepSize)).To(Equal(ptr.To(2))) g.Expect(hcp.Status.ObservedGeneration).To(Equal(hcp.Generation)) }, - expectedError: "", + expectedError: "", + reconcileResult: reconcile.Result{Requeue: true}, }, { - name: "do not delete orphaned HelmReleaseProxies for InstallOnce strategy", - helmChartProxy: installOnceProxy, - objects: []client.Object{hrpReady1, hrpReady2}, + name: "when HelmReleaseProxiesReadyCondition is true and 3 hrps are rolled out and ready, rolls out final hrp and sets count to 4 and step size to 2", + helmChartProxy: newRolloutProxy( + withRollout(&addonsv1alpha1.Rollout{Install: &addonsv1alpha1.RolloutOptions{ + StepInit: &intstr.IntOrString{Type: intstr.Int, IntVal: 1}, + StepIncrement: &intstr.IntOrString{Type: intstr.Int, IntVal: 1}, + StepLimit: &intstr.IntOrString{Type: intstr.Int, IntVal: 2}, + }}), + withRolloutStatus(&addonsv1alpha1.RolloutStatus{StepSize: ptr.To(2), Count: ptr.To(3)}), + withConditions( + []clusterv1.Condition{ + { + Type: addonsv1alpha1.HelmReleaseProxiesReadyCondition, + Status: corev1.ConditionTrue, + }, + }, + ), + ), + objects: []client.Object{cluster5, cluster6, cluster7, cluster8, hrpReady5, hrpReady6, hrpReady7}, expect: func(g *WithT, c client.Client, hcp *addonsv1alpha1.HelmChartProxy) { - g.Expect(hcp.Status.MatchingClusters).To(BeEmpty()) - g.Expect(c.Get(ctx, client.ObjectKey{Namespace: hrpReady1.Namespace, Name: hrpReady1.Name}, &addonsv1alpha1.HelmReleaseProxy{})).To(Succeed()) - g.Expect(c.Get(ctx, client.ObjectKey{Namespace: hrpReady2.Namespace, Name: hrpReady2.Name}, &addonsv1alpha1.HelmReleaseProxy{})).To(Succeed()) - - g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition)).To(BeTrue()) - g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition)).To(BeTrue()) - g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxiesReadyCondition)).To(BeTrue()) - g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxiesReadyCondition)).To(BeTrue()) - g.Expect(conditions.Has(hcp, clusterv1.ReadyCondition)).To(BeTrue()) - g.Expect(conditions.IsTrue(hcp, clusterv1.ReadyCondition)).To(BeTrue()) + g.Expect(hcp.Status.MatchingClusters).To(BeEquivalentTo([]corev1.ObjectReference{ + { + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: "test-cluster-5", + Namespace: "test-namespace", + }, + { + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: "test-cluster-6", + Namespace: "test-namespace", + }, + { + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: "test-cluster-7", + Namespace: "test-namespace", + }, + { + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: "test-cluster-8", + Namespace: "test-namespace", + }, + })) + g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxiesRolloutCompletedCondition)).To(BeTrue()) + g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxiesRolloutCompletedCondition)).To(BeFalse()) + g.Expect((hcp.Status.Rollout.Count)).To(Equal(ptr.To(4))) + g.Expect((hcp.Status.Rollout.StepSize)).To(Equal(ptr.To(2))) g.Expect(hcp.Status.ObservedGeneration).To(Equal(hcp.Generation)) }, - expectedError: "", + expectedError: "", + reconcileResult: reconcile.Result{Requeue: true}, }, { - name: "do not update HelmReleaseProxies when HelmChartProxy changes for InstallOnce strategy", - helmChartProxy: updatedInstallOnceProxy, - objects: []client.Object{hrpReady1, hrpReady2, cluster1, cluster2}, + name: "when HelmReleaseProxiesReadyCondition is true and 4 hrps are rolled out and ready, sets Rollout Completed condition to True and marks hcp as ready", + helmChartProxy: newRolloutProxy( + withRollout(&addonsv1alpha1.Rollout{Install: &addonsv1alpha1.RolloutOptions{ + StepInit: &intstr.IntOrString{Type: intstr.Int, IntVal: 1}, + StepIncrement: &intstr.IntOrString{Type: intstr.Int, IntVal: 1}, + StepLimit: &intstr.IntOrString{Type: intstr.Int, IntVal: 2}, + }}), + withRolloutStatus(&addonsv1alpha1.RolloutStatus{StepSize: ptr.To(2), Count: ptr.To(4)}), + withConditions( + []clusterv1.Condition{ + { + Type: addonsv1alpha1.HelmReleaseProxiesReadyCondition, + Status: corev1.ConditionTrue, + }, + }, + ), + ), + objects: []client.Object{cluster5, cluster6, cluster7, cluster8, hrpReady5, hrpReady6, hrpReady7, hrpReady8}, expect: func(g *WithT, c client.Client, hcp *addonsv1alpha1.HelmChartProxy) { g.Expect(hcp.Status.MatchingClusters).To(BeEquivalentTo([]corev1.ObjectReference{ { APIVersion: clusterv1.GroupVersion.String(), Kind: "Cluster", - Name: "test-cluster-1", + Name: "test-cluster-5", Namespace: "test-namespace", }, { APIVersion: clusterv1.GroupVersion.String(), Kind: "Cluster", - Name: "test-cluster-2", + Name: "test-cluster-6", + Namespace: "test-namespace", + }, + { + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: "test-cluster-7", + Namespace: "test-namespace", + }, + { + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: "test-cluster-8", Namespace: "test-namespace", }, })) - - hrp1Result := &addonsv1alpha1.HelmReleaseProxy{} - g.Expect(c.Get(ctx, client.ObjectKey{Namespace: hrpReady1.Namespace, Name: hrpReady1.Name}, hrp1Result)).To(Succeed()) - g.Expect(cmp.Diff(hrp1Result, hrpReady1)).To(BeEmpty()) - - hrp2Result := &addonsv1alpha1.HelmReleaseProxy{} - g.Expect(c.Get(ctx, client.ObjectKey{Namespace: hrpReady2.Namespace, Name: hrpReady2.Name}, hrp2Result)).To(Succeed()) - g.Expect(cmp.Diff(hrp2Result, hrpReady2)).To(BeEmpty()) - - g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition)).To(BeTrue()) - g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition)).To(BeTrue()) + g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxiesRolloutCompletedCondition)).To(BeTrue()) + g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxiesRolloutCompletedCondition)).To(BeTrue()) g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxiesReadyCondition)).To(BeTrue()) g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxiesReadyCondition)).To(BeTrue()) g.Expect(conditions.Has(hcp, clusterv1.ReadyCondition)).To(BeTrue()) g.Expect(conditions.IsTrue(hcp, clusterv1.ReadyCondition)).To(BeTrue()) + g.Expect((hcp.Status.Rollout.Count)).To(Equal(ptr.To(4))) + g.Expect((hcp.Status.Rollout.StepSize)).To(Equal(ptr.To(2))) g.Expect(hcp.Status.ObservedGeneration).To(Equal(hcp.Generation)) }, expectedError: "", }, { - name: "mark HelmChartProxy as ready once HelmReleaseProxies ready conditions are true ignoring paused clusters", - helmChartProxy: continuousProxy, - objects: []client.Object{cluster1, cluster2, clusterPaused, hrpReady1, hrpReady2}, + name: "during upgrade, when HelmReleaseProxiesReadyCondition is true and 4 hrps are rolled out and ready, sets Rollout Completed condition to True and marks hcp as ready", + helmChartProxy: newRolloutProxy( + withRollout(&addonsv1alpha1.Rollout{ + Upgrade: &addonsv1alpha1.RolloutOptions{ + StepInit: &intstr.IntOrString{Type: intstr.Int, IntVal: 1}, + StepIncrement: &intstr.IntOrString{Type: intstr.Int, IntVal: 1}, + StepLimit: &intstr.IntOrString{Type: intstr.Int, IntVal: 2}, + }, + }, + ), + withRolloutStatus(&addonsv1alpha1.RolloutStatus{StepSize: ptr.To(2), Count: ptr.To(4)}), + withConditions( + []clusterv1.Condition{ + { + Type: addonsv1alpha1.HelmReleaseProxiesReadyCondition, + Status: corev1.ConditionTrue, + }, + }, + ), + withGeneration(2), + ), + objects: []client.Object{cluster5, cluster6, cluster7, cluster8, hrpReady5, hrpReady6, hrpReady7, hrpReady8}, expect: func(g *WithT, c client.Client, hcp *addonsv1alpha1.HelmChartProxy) { g.Expect(hcp.Status.MatchingClusters).To(BeEquivalentTo([]corev1.ObjectReference{ { APIVersion: clusterv1.GroupVersion.String(), Kind: "Cluster", - Name: "test-cluster-1", + Name: "test-cluster-5", Namespace: "test-namespace", }, { APIVersion: clusterv1.GroupVersion.String(), Kind: "Cluster", - Name: "test-cluster-2", + Name: "test-cluster-6", Namespace: "test-namespace", }, { APIVersion: clusterv1.GroupVersion.String(), Kind: "Cluster", - Name: "test-cluster-paused", + Name: "test-cluster-7", + Namespace: "test-namespace", + }, + { + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: "test-cluster-8", Namespace: "test-namespace", }, })) - g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition)).To(BeTrue()) - g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition)).To(BeTrue()) + g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxiesRolloutCompletedCondition)).To(BeTrue()) + g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxiesRolloutCompletedCondition)).To(BeTrue()) g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxiesReadyCondition)).To(BeTrue()) g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxiesReadyCondition)).To(BeTrue()) g.Expect(conditions.Has(hcp, clusterv1.ReadyCondition)).To(BeTrue()) g.Expect(conditions.IsTrue(hcp, clusterv1.ReadyCondition)).To(BeTrue()) + g.Expect((hcp.Status.Rollout.Count)).To(Equal(ptr.To(4))) + g.Expect((hcp.Status.Rollout.StepSize)).To(Equal(ptr.To(2))) g.Expect(hcp.Status.ObservedGeneration).To(Equal(hcp.Generation)) - hrpList := &addonsv1alpha1.HelmReleaseProxyList{} - g.Expect(c.List(ctx, hrpList, &client.ListOptions{Namespace: hcp.Namespace})).To(Succeed()) - - // There should be 2 HelmReleaseProxies as the paused cluster should not have a HelmReleaseProxy. - g.Expect(hrpList.Items).To(HaveLen(2)) }, expectedError: "", }, @@ -675,7 +1607,7 @@ func TestReconcileNormal(t *testing.T) { g.Expect(err).To(MatchError(tc.expectedError), err.Error()) } else { g.Expect(err).NotTo(HaveOccurred()) - g.Expect(result).To(Equal(reconcile.Result{})) + g.Expect(result).To(Equal(tc.reconcileResult)) hcp := &addonsv1alpha1.HelmChartProxy{} g.Expect(r.Client.Get(ctx, request.NamespacedName, hcp)).To(Succeed()) diff --git a/main.go b/main.go index 05568d20..c109558b 100644 --- a/main.go +++ b/main.go @@ -105,7 +105,7 @@ func InitFlags(fs *pflag.FlagSet) { fs.StringVar(&profilerAddress, "profiler-address", "", "Bind address to expose the pprof profiler (e.g. localhost:6060)") - fs.IntVar(&helmChartProxyConcurrency, "helm-chart-proxy-concurrency", 10, + fs.IntVar(&helmChartProxyConcurrency, "helm-chart-proxy-concurrency", 1, "Number of HelmChartProxies to process concurrently.") fs.IntVar(&helmReleaseProxyConcurrency, "helm-release-proxy-concurrency", 10,