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..9f3e5231 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,15 @@ type HelmChartProxySpec struct { // +optional ReconcileStrategy string `json:"reconcileStrategy,omitempty"` + // RolloutStepSize is an opt-in feature that defines a step size during the + // initial rollout of HelmReleaseProxies on matching clusters. Once all + // rolled out HelmReleaseProxy resources are ready=true, the next batch of + // HelmReleaseProxy resources are reconciled. If undefined, will default to + // creating HelmReleaseProxy resources for all matching clusters. + // e.g. an int (5) or percentage of count of total matching clusters (25%) + // +optional + RolloutStepSize *intstr.IntOrString `json:"rolloutStepSize,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 diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 19fb7024..c2e03f16 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.RolloutStepSize != nil { + in, out := &in.RolloutStepSize, &out.RolloutStepSize + *out = new(intstr.IntOrString) + **out = **in + } in.Options.DeepCopyInto(&out.Options) if in.Credentials != nil { in, out := &in.Credentials, &out.Credentials 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..76abb096 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,18 @@ 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 + rolloutStepSize: + anyOf: + - type: integer + - type: string + description: |- + RolloutStepSize is an opt-in feature that defines a step size during the + initial rollout of HelmReleaseProxies on matching clusters. Once all + rolled out HelmReleaseProxy resources are ready=true, the next batch of + HelmReleaseProxy resources are reconciled. If undefined, will default to + creating HelmReleaseProxy resources for all matching clusters. + e.g. an int (5) or percentage of count of total matching clusters (25%) + x-kubernetes-int-or-string: true tlsConfig: description: TLSConfig contains the TLS configuration for a HelmChartProxy. properties: diff --git a/controllers/helmchartproxy/helmchartproxy_controller.go b/controllers/helmchartproxy/helmchartproxy_controller.go index fc970451..105a8eaf 100644 --- a/controllers/helmchartproxy/helmchartproxy_controller.go +++ b/controllers/helmchartproxy/helmchartproxy_controller.go @@ -18,12 +18,15 @@ package helmchartproxy import ( "context" + "slices" "github.com/pkg/errors" 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" 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 +48,18 @@ 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 +} + // 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) @@ -197,12 +212,161 @@ func (r *HelmChartProxyReconciler) reconcileNormal(ctx context.Context, helmChar } } - for _, cluster := range clusters { - // Don't reconcile if the Cluster is being deleted - if !cluster.DeletionTimestamp.IsZero() { - continue + // RolloutStepSize is defined; check if count of HelmReleaseProxies matches + // the count of matching clusters. + if helmChartProxy.Spec.RolloutStepSize != nil { + if len(clusters) != len(helmReleaseProxies) { + // 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)-len(helmReleaseProxies), + ) + + // 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 + } + + 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) { + count := 0 + stepSize, err := intstr.GetScaledValueFromIntOrPercent(helmChartProxy.Spec.RolloutStepSize, len(clusters), true) + if err != nil { + return err + } + + // If HelmReleaseProxiesReadyCondition is Unknown and the first batch of HelmReleaseProxies have + // been created, then exit early. + if stepSize == len(helmReleaseProxies) { + return nil + } + + for _, meta := range rolloutMetaSorted { + // The first batch of helmReleaseProxies have been reconciled. + if count >= stepSize { + return nil + } + + err := r.reconcileForCluster(ctx, helmChartProxy, meta.cluster) + if err != nil { + return err + } + count++ + } + // In cases where the count of remaining HelmReleaseProxies to be rolled + // out is less than rollout step size. + return nil + } + + // If HelmReleaseProxiesReadyCondition is false, reconcile existing + // HelmReleaseProxies and exit. + if conditions.IsFalse(helmChartProxy, addonsv1alpha1.HelmReleaseProxiesReadyCondition) { + for _, meta := range rolloutMetaSorted { + if meta.hrpExists { + err := r.reconcileForCluster(ctx, helmChartProxy, meta.cluster) + if err != nil { + return err + } + } + } + + return nil + } + + // HelmReleaseProxyReadyCondition is True; continue with reconciling the + // next batch of HelmReleaseProxies. + count := 0 + stepSize, err := intstr.GetScaledValueFromIntOrPercent(helmChartProxy.Spec.RolloutStepSize, len(clusters), true) + if err != nil { + return err + } + + for _, meta := range rolloutMetaSorted { + // Exit if HelmReleaseProxyReadyCondition has not caught up to existing + // HelmReleaseProxies status. + if meta.hrpExists && !meta.hrpReady { + return nil + } + + // The next batch of helmReleaseProxies have been reconciled. + if count >= stepSize { + return nil + } + + // Skip reconciling the cluster if its HelmReleaseProxy already exists. + if meta.hrpExists { + continue + } + err := r.reconcileForCluster(ctx, helmChartProxy, meta.cluster) + if err != nil { + return err + } + count++ + } + // In cases where the count of remaining HelmReleaseProxies to be rolled + // out is less than rollout step size. + return nil + } else { + // RolloutStepSize is defined and all HelmReleaseProxies have been rolled out. + conditions.MarkTrue(helmChartProxy, addonsv1alpha1.HelmReleaseProxiesRolloutCompletedCondition) } + } else { + // RolloutStepSize is undefined. Set HelmReleaseProxiesRolloutCompletedCondition to True with reason. + conditions.MarkTrueWithNegativePolarity( + helmChartProxy, + addonsv1alpha1.HelmReleaseProxiesRolloutCompletedCondition, + addonsv1alpha1.HelmReleaseProxiesRolloutUndefinedReason, + clusterv1.ConditionSeverityInfo, + "HelmChartProxy does not use rollout step", + ) + } + // Continue with reconciling for all clusters after initial rollout. + for _, cluster := range clusters { err := r.reconcileForCluster(ctx, helmChartProxy, cluster) if err != nil { return err @@ -321,6 +485,7 @@ func patchHelmChartProxy(ctx context.Context, patchHelper *patch.Helper, helmCha conditions.WithConditions( addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition, addonsv1alpha1.HelmReleaseProxiesReadyCondition, + addonsv1alpha1.HelmReleaseProxiesRolloutCompletedCondition, ), ) @@ -332,6 +497,7 @@ func patchHelmChartProxy(ctx context.Context, patchHelper *patch.Helper, helmCha clusterv1.ReadyCondition, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition, addonsv1alpha1.HelmReleaseProxiesReadyCondition, + addonsv1alpha1.HelmReleaseProxiesRolloutCompletedCondition, }}, patch.WithStatusObservedGeneration{}, ) @@ -407,3 +573,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..8bb543b1 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" @@ -68,6 +69,69 @@ var ( }, } + rolloutStepSizeProxy = &addonsv1alpha1.HelmChartProxy{ + TypeMeta: metav1.TypeMeta{ + APIVersion: addonsv1alpha1.GroupVersion.String(), + Kind: "HelmChartProxy", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-hcp", + Namespace: "test-namespace", + }, + Spec: addonsv1alpha1.HelmChartProxySpec{ + ClusterSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "test-label": "test-value", + }, + }, + RolloutStepSize: &intstr.IntOrString{Type: intstr.String, StrVal: "25%"}, + 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{}, + }, + } + + rolloutStepSizeProxyWithReleaseProxyReadyFalse = &addonsv1alpha1.HelmChartProxy{ + TypeMeta: metav1.TypeMeta{ + APIVersion: addonsv1alpha1.GroupVersion.String(), + Kind: "HelmChartProxy", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-hcp", + Namespace: "test-namespace", + }, + Spec: addonsv1alpha1.HelmChartProxySpec{ + ClusterSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "test-label": "test-value", + }, + }, + RolloutStepSize: &intstr.IntOrString{Type: intstr.String, StrVal: "25%"}, + 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{}, + }, + Status: addonsv1alpha1.HelmChartProxyStatus{ + Conditions: []clusterv1.Condition{ + { + Type: addonsv1alpha1.HelmReleaseProxiesReadyCondition, + Status: corev1.ConditionFalse, + Severity: clusterv1.ConditionSeverityInfo, + }, + }, + }, + } + unsetProxy = &addonsv1alpha1.HelmChartProxy{ TypeMeta: metav1.TypeMeta{ APIVersion: addonsv1alpha1.GroupVersion.String(), @@ -289,6 +353,53 @@ var ( }, } + hrpNotReady1 = &addonsv1alpha1.HelmReleaseProxy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-hrp-1", + 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-1", + 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-1", + 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: clusterv1.ReadyCondition, + Status: corev1.ConditionFalse, + Severity: clusterv1.ConditionSeverityInfo, + }, + }, + }, + } + hrpReady2 = &addonsv1alpha1.HelmReleaseProxy{ ObjectMeta: metav1.ObjectMeta{ Name: "test-hrp-2", @@ -334,6 +445,53 @@ var ( }, }, } + + hrpNotReady2 = &addonsv1alpha1.HelmReleaseProxy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-hrp-2", + 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-2", + 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-2", + 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: 5678", + Options: addonsv1alpha1.HelmOptions{}, + }, + Status: addonsv1alpha1.HelmReleaseProxyStatus{ + Conditions: []clusterv1.Condition{ + { + Type: clusterv1.ReadyCondition, + Status: corev1.ConditionFalse, + Severity: clusterv1.ConditionSeverityInfo, + }, + }, + }, + } ) func TestReconcileNormal(t *testing.T) { @@ -372,6 +530,34 @@ func TestReconcileNormal(t *testing.T) { }, expectedError: "", }, + { + name: "successfully select clusters and set Rollout Step Ready Condition as False", + helmChartProxy: rolloutStepSizeProxy, + 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()) + g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxiesRolloutCompletedCondition)).To(BeTrue()) + g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxiesRolloutCompletedCondition)).To(BeFalse()) + // 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, @@ -447,6 +633,8 @@ func TestReconcileNormal(t *testing.T) { 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.Has(hcp, clusterv1.ReadyCondition)).To(BeTrue()) g.Expect(conditions.IsTrue(hcp, clusterv1.ReadyCondition)).To(BeTrue()) g.Expect(hcp.Status.ObservedGeneration).To(Equal(hcp.Generation)) @@ -476,6 +664,8 @@ func TestReconcileNormal(t *testing.T) { 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.Has(hcp, clusterv1.ReadyCondition)).To(BeTrue()) g.Expect(conditions.IsTrue(hcp, clusterv1.ReadyCondition)).To(BeTrue()) g.Expect(hcp.Status.ObservedGeneration).To(Equal(hcp.Generation)) @@ -506,12 +696,107 @@ func TestReconcileNormal(t *testing.T) { 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.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 are rolled out and ready", + helmChartProxy: rolloutStepSizeProxy, + 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.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 not-ready when not all helm release proxies are rolled out", + helmChartProxy: rolloutStepSizeProxyWithReleaseProxyReadyFalse, + objects: []client.Object{cluster1, cluster2, hrpNotReady1}, + 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(BeFalse()) + g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxiesRolloutCompletedCondition)).To(BeTrue()) + g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxiesRolloutCompletedCondition)).To(BeFalse()) + g.Expect(conditions.Has(hcp, clusterv1.ReadyCondition)).To(BeTrue()) + g.Expect(conditions.IsTrue(hcp, clusterv1.ReadyCondition)).To(BeFalse()) + g.Expect(hcp.Status.ObservedGeneration).To(Equal(hcp.Generation)) + }, + expectedError: "", + }, + { + name: "mark HelmChartProxy as not-ready when all helm release proxies are rolled out and not ready", + helmChartProxy: rolloutStepSizeProxyWithReleaseProxyReadyFalse, + objects: []client.Object{cluster1, cluster2, hrpNotReady1, hrpNotReady2}, + 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(BeFalse()) + g.Expect(conditions.Has(hcp, addonsv1alpha1.HelmReleaseProxiesRolloutCompletedCondition)).To(BeTrue()) + g.Expect(conditions.IsTrue(hcp, addonsv1alpha1.HelmReleaseProxiesRolloutCompletedCondition)).To(BeTrue()) + g.Expect(conditions.Has(hcp, clusterv1.ReadyCondition)).To(BeTrue()) + g.Expect(conditions.IsTrue(hcp, clusterv1.ReadyCondition)).To(BeFalse()) + g.Expect(hcp.Status.ObservedGeneration).To(Equal(hcp.Generation)) + }, + expectedError: "", + }, { name: "successfully delete orphaned HelmReleaseProxies for Continuous strategy", helmChartProxy: continuousProxy,