Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions api/v1/clusterobjectdeployment_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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"
Expand Down
9 changes: 8 additions & 1 deletion applyconfigurations/api/v1/clusterobjectdeploymentstatus.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion docs/api-reference/olmv1-api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<br />Progressing is True with reason NewClusterObjectSetProgressing while the desired<br />rollout is incomplete, or False with reason NewClusterObjectSetProgressed after<br />completion. Completion remains recorded even if the completed revision later<br />becomes unhealthy. | | Optional: \{\} <br /> |
| `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.<br />Progressing is True with reason NewClusterObjectSetProgressing while the desired<br />rollout is incomplete, or False with reason NewClusterObjectSetProgressed after<br />completion. Completion remains recorded even if the completed revision later<br />becomes unhealthy.<br />Available is True with reason Available when at least one active (non-archived)<br />ClusterObjectSet revision is Ready, so the deployment remains available across<br />rollouts while a previous revision is still Ready. It is False with reason<br />Unavailable when active revisions exist but none are Ready yet, or with reason<br />NoActiveRevision when no active revision exists. | | Optional: \{\} <br /> |


#### ClusterObjectDeploymentTemplate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())

Expand Down
12 changes: 11 additions & 1 deletion manifests/experimental-e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
12 changes: 11 additions & 1 deletion manifests/experimental.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
6 changes: 4 additions & 2 deletions test/e2e/features/cod_rollout.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Loading