diff --git a/api/v1/clusterobjectdeployment_types.go b/api/v1/clusterobjectdeployment_types.go
index fac421ffdf..b7956ce46f 100644
--- a/api/v1/clusterobjectdeployment_types.go
+++ b/api/v1/clusterobjectdeployment_types.go
@@ -31,6 +31,7 @@ const (
// Progressing condition reasons.
ClusterObjectDeploymentReasonNewClusterObjectSetProgressed = "NewClusterObjectSetProgressed"
ClusterObjectDeploymentReasonNewClusterObjectSetProgressing = "NewClusterObjectSetProgressing"
+ ClusterObjectDeploymentReasonProgressDeadlineExceeded = ReasonProgressDeadlineExceeded
// Available condition reasons.
ClusterObjectDeploymentReasonAvailable = "Available"
@@ -66,6 +67,18 @@ type ClusterObjectDeploymentTemplateSpec struct {
// +listMapKey=name
Phases []ClusterObjectSetPhase `json:"phases"` //nolint:kubeapilinter // These are ordered groups of objects, not an enum describing lifecycle state.
+ // progressDeadlineMinutes is an optional field that defines the maximum period
+ // of time in minutes after which an installation should be considered failed and
+ // require manual intervention. This functionality is disabled when no value
+ // is provided. The minimum period is 10 minutes, and the maximum is 720 minutes (12 hours).
+ //
+ // The value is passed through to each ClusterObjectSet revision created from this template.
+ //
+ // +kubebuilder:validation:Minimum:=10
+ // +kubebuilder:validation:Maximum:=720
+ // +optional
+ ProgressDeadlineMinutes int32 `json:"progressDeadlineMinutes,omitempty"`
+
// progressionProbes supplies custom readiness probes for objects in every phase.
// The existing ClusterObjectSet selector and assertion semantics apply.
// +optional
@@ -88,7 +101,8 @@ type ClusterObjectDeploymentStatus struct {
// 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.
+ // becomes unhealthy. Progressing is False with reason ProgressDeadlineExceeded when
+ // the latest revision does not roll out within its spec.progressDeadlineMinutes.
//
// Available is True with reason Available when at least one active (non-archived)
// ClusterObjectSet revision is Ready, so the deployment remains available across
diff --git a/api/v1/clusterobjectdeployment_types_test.go b/api/v1/clusterobjectdeployment_types_test.go
index 1340f1116f..e08a6ecc2d 100644
--- a/api/v1/clusterobjectdeployment_types_test.go
+++ b/api/v1/clusterobjectdeployment_types_test.go
@@ -238,19 +238,33 @@ func TestClusterObjectDeploymentTemplateSchema(t *testing.T) {
raw, err := runtime.DefaultUnstructuredConverter.ToUnstructured(cod)
require.NoError(t, err)
for field, value := range map[string]interface{}{
- "group": "example", "revision": int64(42), "lifecycleState": "Active", "progressDeadlineMinutes": int64(10),
+ "group": "example", "revision": int64(42), "lifecycleState": "Active",
} {
require.NoError(t, unstructured.SetNestedField(raw, value, "spec", "template", "spec", field))
}
obj := &unstructured.Unstructured{Object: raw}
require.NoError(t, c.Create(t.Context(), obj))
require.NoError(t, c.Get(t.Context(), client.ObjectKeyFromObject(obj), obj))
- for _, field := range []string{"group", "revision", "lifecycleState", "progressDeadlineMinutes"} {
+ for _, field := range []string{"group", "revision", "lifecycleState"} {
_, found, err := unstructured.NestedFieldNoCopy(obj.Object, "spec", "template", "spec", field)
require.NoError(t, err)
require.False(t, found, "template must not expose %s", field)
}
})
+
+ t.Run("progressDeadlineMinutes is retained and passed through", func(t *testing.T) {
+ cod := newTestClusterObjectDeployment("cod-template-progress-deadline")
+ raw, err := runtime.DefaultUnstructuredConverter.ToUnstructured(cod)
+ require.NoError(t, err)
+ require.NoError(t, unstructured.SetNestedField(raw, int64(10), "spec", "template", "spec", "progressDeadlineMinutes"))
+ obj := &unstructured.Unstructured{Object: raw}
+ require.NoError(t, c.Create(t.Context(), obj))
+ require.NoError(t, c.Get(t.Context(), client.ObjectKeyFromObject(obj), obj))
+ value, found, err := unstructured.NestedInt64(obj.Object, "spec", "template", "spec", "progressDeadlineMinutes")
+ require.NoError(t, err)
+ require.True(t, found, "template must expose progressDeadlineMinutes")
+ require.Equal(t, int64(10), value)
+ })
}
func TestClusterObjectDeploymentStatusValidation(t *testing.T) {
diff --git a/api/v1/validation_test.go b/api/v1/validation_test.go
index 1e7ce6e034..eb12cc549e 100644
--- a/api/v1/validation_test.go
+++ b/api/v1/validation_test.go
@@ -39,6 +39,13 @@ func TestValidate(t *testing.T) {
s.CollisionProtection = CollisionProtectionPrevent
return s
}
+ defaultDeploymentSpec := func(s *ClusterObjectDeploymentSpec) *ClusterObjectDeploymentSpec {
+ s.Template.Spec.CollisionProtection = CollisionProtectionPrevent
+ s.Template.Spec.Phases = []ClusterObjectSetPhase{
+ {Name: "deploy", Objects: []ClusterObjectSetObject{{Object: configMap()}}},
+ }
+ return s
+ }
c := newClient(t)
i := 0
@@ -142,6 +149,52 @@ func TestValidate(t *testing.T) {
},
want: want{valid: true},
},
+ "ClusterObjectDeployment: invalid progress deadline < 10": {
+ args: args{
+ object: ClusterObjectDeploymentSpec{
+ Template: ClusterObjectDeploymentTemplate{
+ Spec: ClusterObjectDeploymentTemplateSpec{ProgressDeadlineMinutes: 9},
+ },
+ },
+ },
+ want: want{valid: false},
+ },
+ "ClusterObjectDeployment: valid progress deadline = 10": {
+ args: args{
+ object: ClusterObjectDeploymentSpec{
+ Template: ClusterObjectDeploymentTemplate{
+ Spec: ClusterObjectDeploymentTemplateSpec{ProgressDeadlineMinutes: 10},
+ },
+ },
+ },
+ want: want{valid: true},
+ },
+ "ClusterObjectDeployment: valid progress deadline = 720": {
+ args: args{
+ object: ClusterObjectDeploymentSpec{
+ Template: ClusterObjectDeploymentTemplate{
+ Spec: ClusterObjectDeploymentTemplateSpec{ProgressDeadlineMinutes: 720},
+ },
+ },
+ },
+ want: want{valid: true},
+ },
+ "ClusterObjectDeployment: invalid progress deadline > 720": {
+ args: args{
+ object: ClusterObjectDeploymentSpec{
+ Template: ClusterObjectDeploymentTemplate{
+ Spec: ClusterObjectDeploymentTemplateSpec{ProgressDeadlineMinutes: 721},
+ },
+ },
+ },
+ want: want{valid: false},
+ },
+ "ClusterObjectDeployment: no progress deadline set": {
+ args: args{
+ object: ClusterObjectDeploymentSpec{},
+ },
+ want: want{valid: true},
+ },
} {
t.Run(name, func(t *testing.T) {
var obj client.Object
@@ -168,6 +221,17 @@ func TestValidate(t *testing.T) {
defaultRevisionSpec(&cos.Spec)
}
obj = cos
+ case ClusterObjectDeploymentSpec:
+ cod := &ClusterObjectDeployment{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: fmt.Sprintf("cod-%d", i),
+ },
+ Spec: s,
+ }
+ if !tc.args.skipDefaulting {
+ defaultDeploymentSpec(&cod.Spec)
+ }
+ obj = cod
default:
t.Fatalf("unknown type %T", s)
}
diff --git a/applyconfigurations/api/v1/clusterobjectdeploymentstatus.go b/applyconfigurations/api/v1/clusterobjectdeploymentstatus.go
index faf43e4b35..f1f70d8da7 100644
--- a/applyconfigurations/api/v1/clusterobjectdeploymentstatus.go
+++ b/applyconfigurations/api/v1/clusterobjectdeploymentstatus.go
@@ -32,7 +32,8 @@ type ClusterObjectDeploymentStatusApplyConfiguration struct {
// 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.
+ // becomes unhealthy. Progressing is False with reason ProgressDeadlineExceeded when
+ // the latest revision does not roll out within its spec.progressDeadlineMinutes.
//
// Available is True with reason Available when at least one active (non-archived)
// ClusterObjectSet revision is Ready, so the deployment remains available across
diff --git a/applyconfigurations/api/v1/clusterobjectdeploymenttemplatespec.go b/applyconfigurations/api/v1/clusterobjectdeploymenttemplatespec.go
index 8639fa7656..4b09cb2103 100644
--- a/applyconfigurations/api/v1/clusterobjectdeploymenttemplatespec.go
+++ b/applyconfigurations/api/v1/clusterobjectdeploymenttemplatespec.go
@@ -31,6 +31,13 @@ type ClusterObjectDeploymentTemplateSpecApplyConfiguration struct {
// at most 50 objects using the existing inline object or immutable Secret reference
// representation. Phase, object, and probe order participate in template identity.
Phases []ClusterObjectSetPhaseApplyConfiguration `json:"phases,omitempty"`
+ // progressDeadlineMinutes is an optional field that defines the maximum period
+ // of time in minutes after which an installation should be considered failed and
+ // require manual intervention. This functionality is disabled when no value
+ // is provided. The minimum period is 10 minutes, and the maximum is 720 minutes (12 hours).
+ //
+ // The value is passed through to each ClusterObjectSet revision created from this template.
+ ProgressDeadlineMinutes *int32 `json:"progressDeadlineMinutes,omitempty"`
// progressionProbes supplies custom readiness probes for objects in every phase.
// The existing ClusterObjectSet selector and assertion semantics apply.
ProgressionProbes []ProgressionProbeApplyConfiguration `json:"progressionProbes,omitempty"`
@@ -58,6 +65,14 @@ func (b *ClusterObjectDeploymentTemplateSpecApplyConfiguration) WithPhases(value
return b
}
+// WithProgressDeadlineMinutes sets the ProgressDeadlineMinutes field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the ProgressDeadlineMinutes field is set to the value of the last call.
+func (b *ClusterObjectDeploymentTemplateSpecApplyConfiguration) WithProgressDeadlineMinutes(value int32) *ClusterObjectDeploymentTemplateSpecApplyConfiguration {
+ b.ProgressDeadlineMinutes = &value
+ return b
+}
+
// WithProgressionProbes adds the given value to the ProgressionProbes field in the declarative configuration
// and returns the receiver, so that objects can be build by chaining "With" function invocations.
// If called multiple times, values provided by each call will be appended to the ProgressionProbes field.
diff --git a/applyconfigurations/internal/internal.go b/applyconfigurations/internal/internal.go
index 0c03ea2e92..e08d65d452 100644
--- a/applyconfigurations/internal/internal.go
+++ b/applyconfigurations/internal/internal.go
@@ -304,6 +304,9 @@ var schemaYAML = typed.YAMLObject(`types:
elementRelationship: associative
keys:
- name
+ - name: progressDeadlineMinutes
+ type:
+ scalar: numeric
- name: progressionProbes
type:
list:
diff --git a/docs/api-reference/olmv1-api-reference.md b/docs/api-reference/olmv1-api-reference.md
index 553bd54f71..8f8993b3e1 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 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: \{\}
|
+| `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. Progressing is False with reason ProgressDeadlineExceeded when
the latest revision does not roll out within its spec.progressDeadlineMinutes.
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
@@ -491,6 +491,7 @@ _Appears in:_
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `phases` _[ClusterObjectSetPhase](#clusterobjectsetphase) array_ | phases is an ordered list of 1 to 20 uniquely named phases. Each phase contains
at most 50 objects using the existing inline object or immutable Secret reference
representation. Phase, object, and probe order participate in template identity. | | MaxItems: 20
MinItems: 1
Required: \{\}
|
+| `progressDeadlineMinutes` _integer_ | progressDeadlineMinutes is an optional field that defines the maximum period
of time in minutes after which an installation should be considered failed and
require manual intervention. This functionality is disabled when no value
is provided. The minimum period is 10 minutes, and the maximum is 720 minutes (12 hours).
The value is passed through to each ClusterObjectSet revision created from this template. | | Maximum: 720
Minimum: 10
Optional: \{\}
|
| `progressionProbes` _[ProgressionProbe](#progressionprobe) array_ | progressionProbes supplies custom readiness probes for objects in every phase.
The existing ClusterObjectSet selector and assertion semantics apply. | | MaxItems: 20
MinItems: 1
Optional: \{\}
|
| `collisionProtection` _[CollisionProtection](#collisionprotection)_ | collisionProtection is the required default collision protection strategy for objects in the revision.
Objects and phases may override it. | | Enum: [Prevent IfNoController None]
Required: \{\}
|
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 93bc43e1c3..dc1a2be859 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
@@ -213,6 +213,18 @@ spec:
x-kubernetes-list-map-keys:
- name
x-kubernetes-list-type: map
+ progressDeadlineMinutes:
+ description: |-
+ progressDeadlineMinutes is an optional field that defines the maximum period
+ of time in minutes after which an installation should be considered failed and
+ require manual intervention. This functionality is disabled when no value
+ is provided. The minimum period is 10 minutes, and the maximum is 720 minutes (12 hours).
+
+ The value is passed through to each ClusterObjectSet revision created from this template.
+ format: int32
+ maximum: 720
+ minimum: 10
+ type: integer
progressionProbes:
description: |-
progressionProbes supplies custom readiness probes for objects in every phase.
@@ -490,7 +502,8 @@ spec:
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.
+ becomes unhealthy. Progressing is False with reason ProgressDeadlineExceeded when
+ the latest revision does not roll out within its spec.progressDeadlineMinutes.
Available is True with reason Available when at least one active (non-archived)
ClusterObjectSet revision is Ready, so the deployment remains available across
diff --git a/internal/object-controller/controllers/clusterobjectdeployment_controller.go b/internal/object-controller/controllers/clusterobjectdeployment_controller.go
index be41b36c2c..545ddb5b84 100644
--- a/internal/object-controller/controllers/clusterobjectdeployment_controller.go
+++ b/internal/object-controller/controllers/clusterobjectdeployment_controller.go
@@ -92,6 +92,16 @@ func (c *ClusterObjectDeploymentReconciler) reconcile(ctx context.Context, cod *
return nil
}
+ // A revision that has exceeded its progress deadline reports Ready=False with
+ // reason ProgressDeadlineExceeded and never records completedAt. Surface that
+ // terminal rollout state on the deployment's Progressing condition so it is
+ // actionable, rather than reporting an indefinitely-progressing rollout.
+ if ready := apimeta.FindStatusCondition(latest.Status.Conditions, ocv1.ClusterObjectSetTypeReady); ready != nil &&
+ ready.Status == metav1.ConditionFalse && ready.Reason == ocv1.ReasonProgressDeadlineExceeded {
+ setProgressing(cod, metav1.ConditionFalse, ocv1.ClusterObjectDeploymentReasonProgressDeadlineExceeded, "latest revision exceeded its progress deadline")
+ return nil
+ }
+
if latest.Status.CompletedAt.IsZero() {
setProgressing(cod, metav1.ConditionTrue, ocv1.ClusterObjectDeploymentReasonNewClusterObjectSetProgressing, "latest revision is progressing")
} else {
@@ -259,12 +269,13 @@ func buildClusterObjectSet(cod *ocv1.ClusterObjectDeployment, revision int64, ha
},
},
Spec: ocv1.ClusterObjectSetSpec{
- Group: cod.Name,
- Revision: revision,
- LifecycleState: ocv1.ClusterObjectSetLifecycleStateActive,
- Phases: cod.Spec.Template.Spec.Phases,
- ProgressionProbes: cod.Spec.Template.Spec.ProgressionProbes,
- CollisionProtection: cod.Spec.Template.Spec.CollisionProtection,
+ Group: cod.Name,
+ Revision: revision,
+ LifecycleState: ocv1.ClusterObjectSetLifecycleStateActive,
+ Phases: cod.Spec.Template.Spec.Phases,
+ ProgressDeadlineMinutes: cod.Spec.Template.Spec.ProgressDeadlineMinutes,
+ ProgressionProbes: cod.Spec.Template.Spec.ProgressionProbes,
+ CollisionProtection: cod.Spec.Template.Spec.CollisionProtection,
},
}
}
diff --git a/internal/object-controller/controllers/clusterobjectdeployment_controller_test.go b/internal/object-controller/controllers/clusterobjectdeployment_controller_test.go
index f8c110be5f..9cd92605ff 100644
--- a/internal/object-controller/controllers/clusterobjectdeployment_controller_test.go
+++ b/internal/object-controller/controllers/clusterobjectdeployment_controller_test.go
@@ -31,7 +31,8 @@ func codFixture() *ocv1.ClusterObjectDeployment {
Spec: ocv1.ClusterObjectDeploymentSpec{
Template: ocv1.ClusterObjectDeploymentTemplate{
Spec: ocv1.ClusterObjectDeploymentTemplateSpec{
- CollisionProtection: ocv1.CollisionProtectionPrevent,
+ CollisionProtection: ocv1.CollisionProtectionPrevent,
+ ProgressDeadlineMinutes: 15,
Phases: []ocv1.ClusterObjectSetPhase{{
Name: "resources",
Objects: []ocv1.ClusterObjectSetObject{{
@@ -104,6 +105,22 @@ func markCOSReady(t *testing.T, c client.Client, name string, ready bool) {
require.NoError(t, c.Status().Update(t.Context(), cos))
}
+// markCOSProgressDeadlineExceeded sets the named revision's Ready condition to
+// False with reason ProgressDeadlineExceeded, mirroring what the ClusterObjectSet
+// controller records when a revision does not roll out within its deadline.
+func markCOSProgressDeadlineExceeded(t *testing.T, c client.Client, name string) {
+ t.Helper()
+ cos, ok := getCOS(t, c, name)
+ require.True(t, ok)
+ apimeta.SetStatusCondition(&cos.Status.Conditions, metav1.Condition{
+ Type: ocv1.ClusterObjectSetTypeReady,
+ Status: metav1.ConditionFalse,
+ Reason: ocv1.ReasonProgressDeadlineExceeded,
+ 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())
@@ -165,6 +182,39 @@ func Test_ClusterObjectDeploymentReconciler_Available(t *testing.T) {
require.NotNil(t, cond)
require.Equal(t, metav1.ConditionTrue, cond.Status, "deployment must stay available while a prior revision is still Ready")
})
+
+ t.Run("stays available when a new revision exceeds its progress deadline but a prior 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)
+ 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
+ require.NoError(t, err)
+
+ // Revision 2 blows its deadline while revision 1 is still Active and Ready.
+ markCOSProgressDeadlineExceeded(t, c, clusterObjectDeploymentName+"-2")
+ _, err = reconcileCOD(t, c)
+ require.NoError(t, err)
+
+ require.Equal(t, metav1.ConditionTrue, available(t, c).Status,
+ "deployment must stay available while a prior revision is still Ready, even if the new revision exceeds its deadline")
+
+ prog := progressing(t, c)
+ require.NotNil(t, prog)
+ require.Equal(t, metav1.ConditionFalse, prog.Status)
+ require.Equal(t, ocv1.ClusterObjectDeploymentReasonProgressDeadlineExceeded, prog.Reason,
+ "Progressing must surface the deadline breach of the latest revision")
+ })
}
func Test_ClusterObjectDeploymentReconciler_CreatesFirstRevision(t *testing.T) {
@@ -178,6 +228,8 @@ func Test_ClusterObjectDeploymentReconciler_CreatesFirstRevision(t *testing.T) {
require.Equal(t, clusterObjectDeploymentName, cos.Spec.Group)
require.Equal(t, int64(1), cos.Spec.Revision)
require.Equal(t, ocv1.ClusterObjectSetLifecycleStateActive, cos.Spec.LifecycleState)
+ require.Equal(t, ocv1.CollisionProtectionPrevent, cos.Spec.CollisionProtection)
+ require.Equal(t, int32(15), cos.Spec.ProgressDeadlineMinutes, "progressDeadlineMinutes should pass through to the revision")
require.NotEmpty(t, cos.Labels[labels.TemplateHashKey])
ref := metav1.GetControllerOf(cos)
@@ -194,22 +246,33 @@ func Test_ClusterObjectDeploymentReconciler_CreatesFirstRevision(t *testing.T) {
func Test_ClusterObjectDeploymentReconciler_NoNewRevisionWhenTemplateUnchanged(t *testing.T) {
for _, tc := range []struct {
name string
- completed bool
+ prep func(t *testing.T, c client.Client, cosName string)
wantStatus metav1.ConditionStatus
wantReason string
}{
{
- name: "completed latest revision reports Progressed",
- completed: true,
+ name: "completed latest revision reports Progressed",
+ prep: func(t *testing.T, c client.Client, cosName string) {
+ cos, ok := getCOS(t, c, cosName)
+ require.True(t, ok)
+ cos.Status.CompletedAt = metav1.Now()
+ require.NoError(t, c.Status().Update(t.Context(), cos))
+ },
wantStatus: metav1.ConditionFalse,
wantReason: ocv1.ClusterObjectDeploymentReasonNewClusterObjectSetProgressed,
},
{
name: "not-yet-completed latest revision reports Progressing",
- completed: false,
+ prep: nil,
wantStatus: metav1.ConditionTrue,
wantReason: ocv1.ClusterObjectDeploymentReasonNewClusterObjectSetProgressing,
},
+ {
+ name: "deadline-exceeded latest revision reports ProgressDeadlineExceeded",
+ prep: func(t *testing.T, c client.Client, cosName string) { markCOSProgressDeadlineExceeded(t, c, cosName) },
+ wantStatus: metav1.ConditionFalse,
+ wantReason: ocv1.ClusterObjectDeploymentReasonProgressDeadlineExceeded,
+ },
} {
t.Run(tc.name, func(t *testing.T) {
c := newCODClient(t, codFixture())
@@ -218,11 +281,8 @@ func Test_ClusterObjectDeploymentReconciler_NoNewRevisionWhenTemplateUnchanged(t
_, err := reconcileCOD(t, c)
require.NoError(t, err)
- if tc.completed {
- cos, ok := getCOS(t, c, clusterObjectDeploymentName+"-1")
- require.True(t, ok)
- cos.Status.CompletedAt = metav1.Now()
- require.NoError(t, c.Status().Update(t.Context(), cos))
+ if tc.prep != nil {
+ tc.prep(t, c, clusterObjectDeploymentName+"-1")
}
// Second reconcile must not create a new revision.
diff --git a/manifests/experimental-e2e.yaml b/manifests/experimental-e2e.yaml
index 3272b32401..f002c5fe43 100644
--- a/manifests/experimental-e2e.yaml
+++ b/manifests/experimental-e2e.yaml
@@ -1624,6 +1624,18 @@ spec:
x-kubernetes-list-map-keys:
- name
x-kubernetes-list-type: map
+ progressDeadlineMinutes:
+ description: |-
+ progressDeadlineMinutes is an optional field that defines the maximum period
+ of time in minutes after which an installation should be considered failed and
+ require manual intervention. This functionality is disabled when no value
+ is provided. The minimum period is 10 minutes, and the maximum is 720 minutes (12 hours).
+
+ The value is passed through to each ClusterObjectSet revision created from this template.
+ format: int32
+ maximum: 720
+ minimum: 10
+ type: integer
progressionProbes:
description: |-
progressionProbes supplies custom readiness probes for objects in every phase.
@@ -1901,7 +1913,8 @@ spec:
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.
+ becomes unhealthy. Progressing is False with reason ProgressDeadlineExceeded when
+ the latest revision does not roll out within its spec.progressDeadlineMinutes.
Available is True with reason Available when at least one active (non-archived)
ClusterObjectSet revision is Ready, so the deployment remains available across
diff --git a/manifests/experimental.yaml b/manifests/experimental.yaml
index 5d36438341..dc0ff96b9e 100644
--- a/manifests/experimental.yaml
+++ b/manifests/experimental.yaml
@@ -1585,6 +1585,18 @@ spec:
x-kubernetes-list-map-keys:
- name
x-kubernetes-list-type: map
+ progressDeadlineMinutes:
+ description: |-
+ progressDeadlineMinutes is an optional field that defines the maximum period
+ of time in minutes after which an installation should be considered failed and
+ require manual intervention. This functionality is disabled when no value
+ is provided. The minimum period is 10 minutes, and the maximum is 720 minutes (12 hours).
+
+ The value is passed through to each ClusterObjectSet revision created from this template.
+ format: int32
+ maximum: 720
+ minimum: 10
+ type: integer
progressionProbes:
description: |-
progressionProbes supplies custom readiness probes for objects in every phase.
@@ -1862,7 +1874,8 @@ spec:
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.
+ becomes unhealthy. Progressing is False with reason ProgressDeadlineExceeded when
+ the latest revision does not roll out within its spec.progressDeadlineMinutes.
Available is True with reason Available when at least one active (non-archived)
ClusterObjectSet revision is Ready, so the deployment remains available across
diff --git a/test/e2e/features/cod_progression_timeout.feature b/test/e2e/features/cod_progression_timeout.feature
new file mode 100644
index 0000000000..cdb58906a9
--- /dev/null
+++ b/test/e2e/features/cod_progression_timeout.feature
@@ -0,0 +1,64 @@
+@BoxcutterRuntime
+@ProgressDeadline
+Feature: ClusterObjectDeployment progression timeout
+
+ As an OLM user applying a ClusterObjectDeployment, I want spec.template.spec.progressDeadlineMinutes
+ to be passed through to the ClusterObjectSet revisions it creates, so that a revision that does not
+ roll out within the deadline is reported as failed and the deployment is reported unavailable.
+
+ Background:
+ Given OLM is available
+
+ Scenario: ClusterObjectDeployment passes progressDeadlineMinutes through to its revision and times out
+ Given namespace "${TEST_NAMESPACE}" is available
+ And min value for ClusterObjectDeployment .spec.template.spec.progressDeadlineMinutes is set to 1
+ And min value for ClusterObjectSet .spec.progressDeadlineMinutes is set to 1
+ When resource is applied
+ """
+ apiVersion: olm.operatorframework.io/v1
+ kind: ClusterObjectDeployment
+ metadata:
+ name: ${NAME}
+ spec:
+ template:
+ spec:
+ collisionProtection: Prevent
+ progressDeadlineMinutes: 1
+ progressionProbes:
+ - selector:
+ type: GroupKind
+ groupKind:
+ group: apps
+ kind: Deployment
+ assertions:
+ - type: ConditionEqual
+ conditionEqual:
+ type: Available
+ status: "True"
+ phases:
+ - name: resources
+ objects:
+ - object:
+ apiVersion: apps/v1
+ kind: Deployment
+ metadata:
+ name: never-ready
+ namespace: ${TEST_NAMESPACE}
+ spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: never-ready
+ template:
+ metadata:
+ labels:
+ app: never-ready
+ spec:
+ containers:
+ - name: never-ready
+ image: does-not-exist:latest
+ """
+ Then ClusterObjectSet "${NAME}-1" has group "${NAME}"
+ And ClusterObjectSet "${NAME}-1" reports Ready as False with Reason ProgressDeadlineExceeded
+ And ClusterObjectDeployment "${NAME}" reports Progressing as False with Reason ProgressDeadlineExceeded
+ And ClusterObjectDeployment "${NAME}" reports Available as False with Reason Unavailable
diff --git a/test/e2e/steps/steps.go b/test/e2e/steps/steps.go
index e3b23f5571..c06fb504c7 100644
--- a/test/e2e/steps/steps.go
+++ b/test/e2e/steps/steps.go
@@ -204,7 +204,7 @@ func RegisterSteps(sc *godog.ScenarioContext) {
sc.Step(`^(?i)operator "([^"]+)" target namespace is "([^"]+)"$`, OperatorTargetNamespace)
sc.Step(`^(?i)Prometheus metrics are returned in the response$`, PrometheusMetricsAreReturned)
- sc.Step(`^(?i)min value for (ClusterExtension|ClusterObjectSet) ((?:\.[a-zA-Z]+)+) is set to (\d+)$`, SetCRDFieldMinValue)
+ sc.Step(`^(?i)min value for (ClusterExtension|ClusterObjectSet|ClusterObjectDeployment) ((?:\.[a-zA-Z]+)+) is set to (\d+)$`, SetCRDFieldMinValue)
sc.Step(`^(?i)ClusterExtension "([^"]+)" owns (\d+) ClusterObjectSets?$`, ClusterExtensionOwnsClusterObjectSets)
sc.Step(`^(?i)ClusterExtension "([^"]+)" reports ([[:alnum:]]+) as ([[:alnum:]]+)$`, NamedClusterExtensionReportsCondition)
@@ -2190,6 +2190,8 @@ func SetCRDFieldMinValue(ctx context.Context, resourceType, jsonPath string, min
crdName = "clusterextensions.olm.operatorframework.io"
case "ClusterObjectSet":
crdName = "clusterobjectsets.olm.operatorframework.io"
+ case "ClusterObjectDeployment":
+ crdName = "clusterobjectdeployments.olm.operatorframework.io"
default:
return fmt.Errorf("unsupported resource type: %s", resourceType)
}