diff --git a/api/v1/clusterobjectdeployment_types.go b/api/v1/clusterobjectdeployment_types.go
index a2a28dcd0b..fac421ffdf 100644
--- a/api/v1/clusterobjectdeployment_types.go
+++ b/api/v1/clusterobjectdeployment_types.go
@@ -26,10 +26,16 @@ const (
// Condition types.
ClusterObjectDeploymentTypeProgressing = "Progressing"
+ ClusterObjectDeploymentTypeAvailable = "Available"
- // Condition reasons.
+ // Progressing condition reasons.
ClusterObjectDeploymentReasonNewClusterObjectSetProgressed = "NewClusterObjectSetProgressed"
ClusterObjectDeploymentReasonNewClusterObjectSetProgressing = "NewClusterObjectSetProgressing"
+
+ // Available condition reasons.
+ ClusterObjectDeploymentReasonAvailable = "Available"
+ ClusterObjectDeploymentReasonUnavailable = "Unavailable"
+ ClusterObjectDeploymentReasonNoActiveRevision = "NoActiveRevision"
)
// ClusterObjectDeploymentSpec defines the mutable desired state of a deployment.
@@ -77,11 +83,18 @@ type ClusterObjectDeploymentTemplateSpec struct {
// ClusterObjectDeploymentStatus reports rollout progress.
type ClusterObjectDeploymentStatus struct {
- // conditions describes rollout progress for the observed generation.
+ // conditions describes rollout progress and availability for the observed generation.
+ //
// Progressing is True with reason NewClusterObjectSetProgressing while the desired
// rollout is incomplete, or False with reason NewClusterObjectSetProgressed after
// completion. Completion remains recorded even if the completed revision later
// becomes unhealthy.
+ //
+ // Available is True with reason Available when at least one active (non-archived)
+ // ClusterObjectSet revision is Ready, so the deployment remains available across
+ // rollouts while a previous revision is still Ready. It is False with reason
+ // Unavailable when active revisions exist but none are Ready yet, or with reason
+ // NoActiveRevision when no active revision exists.
// +optional
// +listType=map
// +listMapKey=type
@@ -95,6 +108,7 @@ type ClusterObjectDeploymentStatus struct {
// +kubebuilder:object:root=true
// +kubebuilder:resource:scope=Cluster,shortName=cod
// +kubebuilder:subresource:status
+// +kubebuilder:printcolumn:name=Available,type=string,JSONPath=`.status.conditions[?(@.type=='Available')].status`
// +kubebuilder:printcolumn:name=Progressing,type=string,JSONPath=`.status.conditions[?(@.type=='Progressing')].status`
// +kubebuilder:printcolumn:name=Age,type=date,JSONPath=`.metadata.creationTimestamp`
// +kubebuilder:validation:XValidation:rule="size(self.metadata.name) <= 52 && self.metadata.name.matches('^[a-z]([a-z0-9-]*[a-z0-9])?$')",message="name must be 1 to 52 lowercase alphanumeric or hyphen characters, starting with a letter and ending with an alphanumeric character"
diff --git a/applyconfigurations/api/v1/clusterobjectdeploymentstatus.go b/applyconfigurations/api/v1/clusterobjectdeploymentstatus.go
index 183dda5203..faf43e4b35 100644
--- a/applyconfigurations/api/v1/clusterobjectdeploymentstatus.go
+++ b/applyconfigurations/api/v1/clusterobjectdeploymentstatus.go
@@ -27,11 +27,18 @@ import (
//
// ClusterObjectDeploymentStatus reports rollout progress.
type ClusterObjectDeploymentStatusApplyConfiguration struct {
- // conditions describes rollout progress for the observed generation.
+ // conditions describes rollout progress and availability for the observed generation.
+ //
// Progressing is True with reason NewClusterObjectSetProgressing while the desired
// rollout is incomplete, or False with reason NewClusterObjectSetProgressed after
// completion. Completion remains recorded even if the completed revision later
// becomes unhealthy.
+ //
+ // Available is True with reason Available when at least one active (non-archived)
+ // ClusterObjectSet revision is Ready, so the deployment remains available across
+ // rollouts while a previous revision is still Ready. It is False with reason
+ // Unavailable when active revisions exist but none are Ready yet, or with reason
+ // NoActiveRevision when no active revision exists.
Conditions []metav1.ConditionApplyConfiguration `json:"conditions,omitempty"`
}
diff --git a/docs/api-reference/olmv1-api-reference.md b/docs/api-reference/olmv1-api-reference.md
index 8d2df41777..553bd54f71 100644
--- a/docs/api-reference/olmv1-api-reference.md
+++ b/docs/api-reference/olmv1-api-reference.md
@@ -458,7 +458,7 @@ _Appears in:_
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
-| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#condition-v1-meta) array_ | conditions describes rollout progress for the observed generation.
Progressing is True with reason NewClusterObjectSetProgressing while the desired
rollout is incomplete, or False with reason NewClusterObjectSetProgressed after
completion. Completion remains recorded even if the completed revision later
becomes unhealthy. | | Optional: \{\}
|
+| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#condition-v1-meta) array_ | conditions describes rollout progress and availability for the observed generation.
Progressing is True with reason NewClusterObjectSetProgressing while the desired
rollout is incomplete, or False with reason NewClusterObjectSetProgressed after
completion. Completion remains recorded even if the completed revision later
becomes unhealthy.
Available is True with reason Available when at least one active (non-archived)
ClusterObjectSet revision is Ready, so the deployment remains available across
rollouts while a previous revision is still Ready. It is False with reason
Unavailable when active revisions exist but none are Ready yet, or with reason
NoActiveRevision when no active revision exists. | | Optional: \{\}
|
#### ClusterObjectDeploymentTemplate
diff --git a/helm/olmv1/base/object-controller/crd/experimental/olm.operatorframework.io_clusterobjectdeployments.yaml b/helm/olmv1/base/object-controller/crd/experimental/olm.operatorframework.io_clusterobjectdeployments.yaml
index 68f1e2ad42..93bc43e1c3 100644
--- a/helm/olmv1/base/object-controller/crd/experimental/olm.operatorframework.io_clusterobjectdeployments.yaml
+++ b/helm/olmv1/base/object-controller/crd/experimental/olm.operatorframework.io_clusterobjectdeployments.yaml
@@ -18,6 +18,9 @@ spec:
scope: Cluster
versions:
- additionalPrinterColumns:
+ - jsonPath: .status.conditions[?(@.type=='Available')].status
+ name: Available
+ type: string
- jsonPath: .status.conditions[?(@.type=='Progressing')].status
name: Progressing
type: string
@@ -482,11 +485,18 @@ spec:
properties:
conditions:
description: |-
- conditions describes rollout progress for the observed generation.
+ conditions describes rollout progress and availability for the observed generation.
+
Progressing is True with reason NewClusterObjectSetProgressing while the desired
rollout is incomplete, or False with reason NewClusterObjectSetProgressed after
completion. Completion remains recorded even if the completed revision later
becomes unhealthy.
+
+ Available is True with reason Available when at least one active (non-archived)
+ ClusterObjectSet revision is Ready, so the deployment remains available across
+ rollouts while a previous revision is still Ready. It is False with reason
+ Unavailable when active revisions exist but none are Ready yet, or with reason
+ NoActiveRevision when no active revision exists.
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
diff --git a/internal/object-controller/controllers/clusterobjectdeployment_controller.go b/internal/object-controller/controllers/clusterobjectdeployment_controller.go
index ab68c273f3..be41b36c2c 100644
--- a/internal/object-controller/controllers/clusterobjectdeployment_controller.go
+++ b/internal/object-controller/controllers/clusterobjectdeployment_controller.go
@@ -80,6 +80,8 @@ func (c *ClusterObjectDeploymentReconciler) reconcile(ctx context.Context, cod *
hash := templateHash(cod.Spec.Template)
+ setAvailable(cod, revisions)
+
latest := latestActiveRevision(revisions)
if latest == nil || latest.Labels[labels.TemplateHashKey] != hash {
if err := c.createRevision(ctx, cod, revisions, hash); err != nil {
@@ -127,10 +129,41 @@ func (c *ClusterObjectDeploymentReconciler) createRevision(ctx context.Context,
return nil
}
+// setAvailable sets the deployment's Available condition. It is True when at least
+// one active (non-archived, non-deleting) revision is Ready, so the deployment
+// stays available across rollouts while a prior revision remains Ready.
+func setAvailable(cod *ocv1.ClusterObjectDeployment, revisions []ocv1.ClusterObjectSet) {
+ activeCount := 0
+ for i := range revisions {
+ rev := &revisions[i]
+ if !rev.DeletionTimestamp.IsZero() || rev.Spec.LifecycleState == ocv1.ClusterObjectSetLifecycleStateArchived {
+ continue
+ }
+ activeCount++
+ if apimeta.IsStatusConditionTrue(rev.Status.Conditions, ocv1.ClusterObjectSetTypeReady) {
+ setCondition(cod, ocv1.ClusterObjectDeploymentTypeAvailable, metav1.ConditionTrue,
+ ocv1.ClusterObjectDeploymentReasonAvailable, "an active revision is ready")
+ return
+ }
+ }
+ if activeCount == 0 {
+ setCondition(cod, ocv1.ClusterObjectDeploymentTypeAvailable, metav1.ConditionFalse,
+ ocv1.ClusterObjectDeploymentReasonNoActiveRevision, "no active revision exists")
+ return
+ }
+ setCondition(cod, ocv1.ClusterObjectDeploymentTypeAvailable, metav1.ConditionFalse,
+ ocv1.ClusterObjectDeploymentReasonUnavailable, "no active revision is ready")
+}
+
// setProgressing sets the deployment's Progressing condition.
func setProgressing(cod *ocv1.ClusterObjectDeployment, status metav1.ConditionStatus, reason, message string) {
+ setCondition(cod, ocv1.ClusterObjectDeploymentTypeProgressing, status, reason, message)
+}
+
+// setCondition sets a condition on the deployment status with the observed generation.
+func setCondition(cod *ocv1.ClusterObjectDeployment, condType string, status metav1.ConditionStatus, reason, message string) {
apimeta.SetStatusCondition(&cod.Status.Conditions, metav1.Condition{
- Type: ocv1.ClusterObjectDeploymentTypeProgressing,
+ Type: condType,
Status: status,
Reason: reason,
Message: message,
diff --git a/internal/object-controller/controllers/clusterobjectdeployment_controller_test.go b/internal/object-controller/controllers/clusterobjectdeployment_controller_test.go
index 39e0d02461..f8c110be5f 100644
--- a/internal/object-controller/controllers/clusterobjectdeployment_controller_test.go
+++ b/internal/object-controller/controllers/clusterobjectdeployment_controller_test.go
@@ -80,6 +80,93 @@ func progressing(t *testing.T, c client.Client) *metav1.Condition {
return apimeta.FindStatusCondition(cod.Status.Conditions, ocv1.ClusterObjectDeploymentTypeProgressing)
}
+func available(t *testing.T, c client.Client) *metav1.Condition {
+ t.Helper()
+ cod := &ocv1.ClusterObjectDeployment{}
+ require.NoError(t, c.Get(t.Context(), types.NamespacedName{Name: clusterObjectDeploymentName}, cod))
+ return apimeta.FindStatusCondition(cod.Status.Conditions, ocv1.ClusterObjectDeploymentTypeAvailable)
+}
+
+func markCOSReady(t *testing.T, c client.Client, name string, ready bool) {
+ t.Helper()
+ cos, ok := getCOS(t, c, name)
+ require.True(t, ok)
+ status, reason := metav1.ConditionFalse, ocv1.ClusterObjectSetReasonObjectsNotReady
+ if ready {
+ status, reason = metav1.ConditionTrue, ocv1.ClusterObjectSetReasonAllObjectsReady
+ }
+ apimeta.SetStatusCondition(&cos.Status.Conditions, metav1.Condition{
+ Type: ocv1.ClusterObjectSetTypeReady,
+ Status: status,
+ Reason: reason,
+ Message: "test",
+ })
+ require.NoError(t, c.Status().Update(t.Context(), cos))
+}
+
+func Test_ClusterObjectDeploymentReconciler_Available(t *testing.T) {
+ t.Run("no active revision before the first revision exists", func(t *testing.T) {
+ c := newCODClient(t, codFixture())
+ _, err := reconcileCOD(t, c)
+ require.NoError(t, err)
+
+ cond := available(t, c)
+ require.NotNil(t, cond)
+ require.Equal(t, metav1.ConditionFalse, cond.Status)
+ require.Equal(t, ocv1.ClusterObjectDeploymentReasonNoActiveRevision, cond.Reason)
+ })
+
+ t.Run("unavailable when the active revision is not Ready", func(t *testing.T) {
+ c := newCODClient(t, codFixture())
+ _, err := reconcileCOD(t, c) // creates revision 1
+ require.NoError(t, err)
+ _, err = reconcileCOD(t, c) // revision 1 is now listed but not Ready
+ require.NoError(t, err)
+
+ cond := available(t, c)
+ require.NotNil(t, cond)
+ require.Equal(t, metav1.ConditionFalse, cond.Status)
+ require.Equal(t, ocv1.ClusterObjectDeploymentReasonUnavailable, cond.Reason)
+ })
+
+ t.Run("available when an active revision is Ready", func(t *testing.T) {
+ c := newCODClient(t, codFixture())
+ _, err := reconcileCOD(t, c) // creates revision 1
+ require.NoError(t, err)
+ markCOSReady(t, c, clusterObjectDeploymentName+"-1", true)
+ _, err = reconcileCOD(t, c)
+ require.NoError(t, err)
+
+ cond := available(t, c)
+ require.NotNil(t, cond)
+ require.Equal(t, metav1.ConditionTrue, cond.Status)
+ require.Equal(t, ocv1.ClusterObjectDeploymentReasonAvailable, cond.Reason)
+ })
+
+ t.Run("stays available while a new revision rolls out", func(t *testing.T) {
+ c := newCODClient(t, codFixture())
+ _, err := reconcileCOD(t, c) // creates revision 1
+ require.NoError(t, err)
+ markCOSReady(t, c, clusterObjectDeploymentName+"-1", true)
+ _, err = reconcileCOD(t, c)
+ require.NoError(t, err)
+ require.Equal(t, metav1.ConditionTrue, available(t, c).Status)
+
+ // Change the template so a new revision is created while revision 1 stays Ready.
+ cod := &ocv1.ClusterObjectDeployment{}
+ require.NoError(t, c.Get(t.Context(), types.NamespacedName{Name: clusterObjectDeploymentName}, cod))
+ cod.Spec.Template.Spec.Phases[0].Objects[0].Object.Object["data"] = map[string]interface{}{"k": "v"}
+ require.NoError(t, c.Update(t.Context(), cod))
+
+ _, err = reconcileCOD(t, c) // creates revision 2; revision 1 is still active and Ready
+ require.NoError(t, err)
+
+ cond := available(t, c)
+ require.NotNil(t, cond)
+ require.Equal(t, metav1.ConditionTrue, cond.Status, "deployment must stay available while a prior revision is still Ready")
+ })
+}
+
func Test_ClusterObjectDeploymentReconciler_CreatesFirstRevision(t *testing.T) {
c := newCODClient(t, codFixture())
diff --git a/manifests/experimental-e2e.yaml b/manifests/experimental-e2e.yaml
index d79111109d..3272b32401 100644
--- a/manifests/experimental-e2e.yaml
+++ b/manifests/experimental-e2e.yaml
@@ -1429,6 +1429,9 @@ spec:
scope: Cluster
versions:
- additionalPrinterColumns:
+ - jsonPath: .status.conditions[?(@.type=='Available')].status
+ name: Available
+ type: string
- jsonPath: .status.conditions[?(@.type=='Progressing')].status
name: Progressing
type: string
@@ -1893,11 +1896,18 @@ spec:
properties:
conditions:
description: |-
- conditions describes rollout progress for the observed generation.
+ conditions describes rollout progress and availability for the observed generation.
+
Progressing is True with reason NewClusterObjectSetProgressing while the desired
rollout is incomplete, or False with reason NewClusterObjectSetProgressed after
completion. Completion remains recorded even if the completed revision later
becomes unhealthy.
+
+ Available is True with reason Available when at least one active (non-archived)
+ ClusterObjectSet revision is Ready, so the deployment remains available across
+ rollouts while a previous revision is still Ready. It is False with reason
+ Unavailable when active revisions exist but none are Ready yet, or with reason
+ NoActiveRevision when no active revision exists.
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
diff --git a/manifests/experimental.yaml b/manifests/experimental.yaml
index 31f9c4232e..5d36438341 100644
--- a/manifests/experimental.yaml
+++ b/manifests/experimental.yaml
@@ -1390,6 +1390,9 @@ spec:
scope: Cluster
versions:
- additionalPrinterColumns:
+ - jsonPath: .status.conditions[?(@.type=='Available')].status
+ name: Available
+ type: string
- jsonPath: .status.conditions[?(@.type=='Progressing')].status
name: Progressing
type: string
@@ -1854,11 +1857,18 @@ spec:
properties:
conditions:
description: |-
- conditions describes rollout progress for the observed generation.
+ conditions describes rollout progress and availability for the observed generation.
+
Progressing is True with reason NewClusterObjectSetProgressing while the desired
rollout is incomplete, or False with reason NewClusterObjectSetProgressed after
completion. Completion remains recorded even if the completed revision later
becomes unhealthy.
+
+ Available is True with reason Available when at least one active (non-archived)
+ ClusterObjectSet revision is Ready, so the deployment remains available across
+ rollouts while a previous revision is still Ready. It is False with reason
+ Unavailable when active revisions exist but none are Ready yet, or with reason
+ NoActiveRevision when no active revision exists.
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
diff --git a/test/e2e/features/cod_rollout.feature b/test/e2e/features/cod_rollout.feature
index fb3dad53ed..24757a0468 100644
--- a/test/e2e/features/cod_rollout.feature
+++ b/test/e2e/features/cod_rollout.feature
@@ -3,8 +3,8 @@ Feature: ClusterObjectDeployment revision rollout
As an OLM user applying a ClusterObjectDeployment, I want the controller to roll
out my template as an immutable ClusterObjectSet revision, create a new revision
- when the template changes, and report rollout progress on the Progressing
- condition.
+ when the template changes, report rollout progress on the Progressing condition,
+ and report availability on the Available condition.
Background:
Given OLM is available
@@ -36,6 +36,7 @@ Feature: ClusterObjectDeployment revision rollout
Then ClusterObjectSet "${NAME}-1" has group "${NAME}"
And ClusterObjectSet "${NAME}-1" reports Ready as True with Reason AllObjectsReady
And ClusterObjectDeployment "${NAME}" reports Progressing as False with Reason NewClusterObjectSetProgressed
+ And ClusterObjectDeployment "${NAME}" reports Available as True with Reason Available
Scenario: Updating the template creates a new revision
Given namespace "${TEST_NAMESPACE}" is available
@@ -95,3 +96,4 @@ Feature: ClusterObjectDeployment revision rollout
Then ClusterObjectSet "${NAME}-2" has group "${NAME}"
And ClusterObjectSet "${NAME}-2" reports Ready as True with Reason AllObjectsReady
And ClusterObjectDeployment "${NAME}" reports Progressing as False with Reason NewClusterObjectSetProgressed
+ And ClusterObjectDeployment "${NAME}" reports Available as True with Reason Available