Skip to content
Merged
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
16 changes: 15 additions & 1 deletion api/v1/clusterobjectdeployment_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const (
// Progressing condition reasons.
ClusterObjectDeploymentReasonNewClusterObjectSetProgressed = "NewClusterObjectSetProgressed"
ClusterObjectDeploymentReasonNewClusterObjectSetProgressing = "NewClusterObjectSetProgressing"
ClusterObjectDeploymentReasonProgressDeadlineExceeded = ReasonProgressDeadlineExceeded

// Available condition reasons.
ClusterObjectDeploymentReasonAvailable = "Available"
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
18 changes: 16 additions & 2 deletions api/v1/clusterobjectdeployment_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
64 changes: 64 additions & 0 deletions api/v1/validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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)
}
Expand Down
3 changes: 2 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.

15 changes: 15 additions & 0 deletions applyconfigurations/api/v1/clusterobjectdeploymenttemplatespec.go

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

3 changes: 3 additions & 0 deletions applyconfigurations/internal/internal.go

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

3 changes: 2 additions & 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 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 /> |
| `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. Progressing is False with reason ProgressDeadlineExceeded when<br />the latest revision does not roll out within its spec.progressDeadlineMinutes.<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 Expand Up @@ -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<br />at most 50 objects using the existing inline object or immutable Secret reference<br />representation. Phase, object, and probe order participate in template identity. | | MaxItems: 20 <br />MinItems: 1 <br />Required: \{\} <br /> |
| `progressDeadlineMinutes` _integer_ | progressDeadlineMinutes is an optional field that defines the maximum period<br />of time in minutes after which an installation should be considered failed and<br />require manual intervention. This functionality is disabled when no value<br />is provided. The minimum period is 10 minutes, and the maximum is 720 minutes (12 hours).<br />The value is passed through to each ClusterObjectSet revision created from this template. | | Maximum: 720 <br />Minimum: 10 <br />Optional: \{\} <br /> |
| `progressionProbes` _[ProgressionProbe](#progressionprobe) array_ | progressionProbes supplies custom readiness probes for objects in every phase.<br />The existing ClusterObjectSet selector and assertion semantics apply. | | MaxItems: 20 <br />MinItems: 1 <br />Optional: \{\} <br /> |
| `collisionProtection` _[CollisionProtection](#collisionprotection)_ | collisionProtection is the required default collision protection strategy for objects in the revision.<br />Objects and phases may override it. | | Enum: [Prevent IfNoController None] <br />Required: \{\} <br /> |

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
},
}
}
Loading
Loading