diff --git a/api/v1alpha1/clusterdeployment_types.go b/api/v1alpha1/clusterdeployment_types.go index 10d665d9b..e32ce8dea 100644 --- a/api/v1alpha1/clusterdeployment_types.go +++ b/api/v1alpha1/clusterdeployment_types.go @@ -46,8 +46,6 @@ const ( HelmChartReadyCondition = "HelmChartReady" // HelmReleaseReadyCondition indicates the corresponding HelmRelease is ready and fully reconciled. HelmReleaseReadyCondition = "HelmReleaseReady" - // ReadyCondition indicates the ClusterDeployment is ready and fully reconciled. - ReadyCondition string = "Ready" ) // ClusterDeploymentSpec defines the desired state of ClusterDeployment diff --git a/api/v1alpha1/common.go b/api/v1alpha1/common.go index 1929e19ee..22ac65bba 100644 --- a/api/v1alpha1/common.go +++ b/api/v1alpha1/common.go @@ -28,6 +28,9 @@ const ( ProgressingReason string = "Progressing" ) +// ReadyCondition indicates a resource is ready and fully reconciled. +const ReadyCondition string = "Ready" + type ( // Holds different types of CAPI providers. Providers []string diff --git a/api/v1alpha1/management_types.go b/api/v1alpha1/management_types.go index f045d53ad..64870e373 100644 --- a/api/v1alpha1/management_types.go +++ b/api/v1alpha1/management_types.go @@ -45,6 +45,13 @@ type ManagementSpec struct { Providers []Provider `json:"providers,omitempty"` } +const ( + // AllComponentsHealthyReason surfaces overall readiness of Management's components. + AllComponentsHealthyReason = "AllComponentsHealthy" + // NotAllComponentsHealthyReason documents a condition not in Status=True because one or more components are failing. + NotAllComponentsHealthyReason = "NotAllComponentsHealthy" +) + // Core represents a structure describing core Management components. type Core struct { // KCM represents the core KCM component and references the KCM template. @@ -111,6 +118,11 @@ type ManagementStatus struct { CAPIContracts map[string]CompatibilityContracts `json:"capiContracts,omitempty"` // Components indicates the status of installed KCM components and CAPI providers. Components map[string]ComponentStatus `json:"components,omitempty"` + // Conditions represents the observations of a Management's current state. + // +listType=map + // +listMapKey=type + // +kubebuilder:validation:MaxItems=32 + Conditions []metav1.Condition `json:"conditions,omitempty"` // BackupName is a name of the management cluster scheduled backup. BackupName string `json:"backupName,omitempty"` // Release indicates the current Release object. @@ -132,8 +144,11 @@ type ComponentStatus struct { } // +kubebuilder:object:root=true -// +kubebuilder:subresource:status // +kubebuilder:resource:shortName=kcm-mgmt;mgmt,scope=Cluster +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status",description="Overall readiness of the Management resource" +// +kubebuilder:printcolumn:name="Release",type="string",JSONPath=".status.release",description="Current release version" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Time duration since creation of Management" // Management is the Schema for the managements API type Management struct { diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index b94934b7c..de04aecb0 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -896,6 +896,13 @@ func (in *ManagementStatus) DeepCopyInto(out *ManagementStatus) { (*out)[key] = val } } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } if in.AvailableProviders != nil { in, out := &in.AvailableProviders, &out.AvailableProviders *out = make(Providers, len(*in)) diff --git a/internal/controller/management_controller.go b/internal/controller/management_controller.go index 91e615063..6c866dca0 100644 --- a/internal/controller/management_controller.go +++ b/internal/controller/management_controller.go @@ -31,6 +31,7 @@ import ( appsv1 "k8s.io/api/apps/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" @@ -222,6 +223,8 @@ func (r *ManagementReconciler) Update(ctx context.Context, management *kcm.Manag requeue = true } + setReadyCondition(management) + if err := r.Client.Status().Update(ctx, management); err != nil { errs = errors.Join(errs, fmt.Errorf("failed to update status for Management %s: %w", management.Name, err)) } @@ -772,6 +775,34 @@ func updateComponentsStatus( } } +// setReadyCondition updates the Management resource's "Ready" condition based on whether +// all components are healthy. +func setReadyCondition(management *kcm.Management) { + var failing []string + for name, comp := range management.Status.Components { + if !comp.Success { + failing = append(failing, name) + } + } + + readyCond := metav1.Condition{ + Type: kcm.ReadyCondition, + ObservedGeneration: management.Generation, + } + + if len(failing) == 0 { + readyCond.Status = metav1.ConditionTrue + readyCond.Reason = kcm.AllComponentsHealthyReason + readyCond.Message = "All components are successfully installed." + } else { + readyCond.Status = metav1.ConditionFalse + readyCond.Reason = kcm.NotAllComponentsHealthyReason + readyCond.Message = fmt.Sprintf("Components not ready: %v", failing) + } + + meta.SetStatusCondition(&management.Status.Conditions, readyCond) +} + // SetupWithManager sets up the controller with the Manager. func (r *ManagementReconciler) SetupWithManager(mgr ctrl.Manager) error { dc, err := dynamic.NewForConfig(mgr.GetConfig()) diff --git a/internal/controller/management_controller_test.go b/internal/controller/management_controller_test.go index 098363b4e..d3e9f89fc 100644 --- a/internal/controller/management_controller_test.go +++ b/internal/controller/management_controller_test.go @@ -26,6 +26,7 @@ import ( . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" capioperator "sigs.k8s.io/cluster-api-operator/api/v1alpha2" @@ -355,6 +356,12 @@ var _ = Describe("Management Controller", func() { }, })) + By("Expecting condition Ready=False Management status") + cond := meta.FindStatusCondition(mgmt.Status.Conditions, kcmv1.ReadyCondition) + Expect(cond).NotTo(BeNil(), "Expected Ready condition to exist after reconcile") + Expect(cond.Status).To(Equal(metav1.ConditionFalse), "Expected Ready to be False") + Expect(cond.Reason).To(Equal(kcmv1.NotAllComponentsHealthyReason)) + By("Updating capi HelmRelease with Ready condition") helmRelease = &helmcontrollerv2.HelmRelease{} Expect(k8sClient.Get(ctx, types.NamespacedName{ @@ -422,6 +429,12 @@ var _ = Describe("Management Controller", func() { kcmv1.CoreCAPIName: {Success: true, Template: providerTemplateRequiredComponent}, })) + By("Expecting condition Ready=True Management status") + cond = meta.FindStatusCondition(mgmt.Status.Conditions, kcmv1.ReadyCondition) + Expect(cond).NotTo(BeNil(), "Expected Ready condition to exist") + Expect(cond.Status).To(Equal(metav1.ConditionTrue), "Expected Ready to be True") + Expect(cond.Reason).To(Equal(kcmv1.AllComponentsHealthyReason)) + By("Removing the leftover objects") mgmt.Finalizers = nil Expect(k8sClient.Update(ctx, mgmt)).To(Succeed()) diff --git a/templates/provider/kcm/templates/crds/k0rdent.mirantis.com_managements.yaml b/templates/provider/kcm/templates/crds/k0rdent.mirantis.com_managements.yaml index 67ce933ef..a83c92b76 100644 --- a/templates/provider/kcm/templates/crds/k0rdent.mirantis.com_managements.yaml +++ b/templates/provider/kcm/templates/crds/k0rdent.mirantis.com_managements.yaml @@ -17,7 +17,20 @@ spec: singular: management scope: Cluster versions: - - name: v1alpha1 + - additionalPrinterColumns: + - description: Overall readiness of the Management resource + jsonPath: .status.conditions[?(@.type=='Ready')].status + name: Ready + type: string + - description: Current release version + jsonPath: .status.release + name: Release + type: string + - description: Time duration since creation of Management + jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 schema: openAPIV3Schema: description: Management is the Schema for the managements API @@ -163,6 +176,68 @@ spec: description: Components indicates the status of installed KCM components and CAPI providers. type: object + conditions: + description: Conditions represents the observations of a Management's + current state. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 32 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map observedGeneration: description: ObservedGeneration is the last observed generation. format: int64