Skip to content

Commit 5ed4b38

Browse files
committed
Support ClusterExtension progress deadline detection
Adds optional `.spec.progressDeadlineMinutes` field to `ClusterExtension` and `ClusterExtensionRevision` that defines the maximum time an extension version can take to roll out before being marked as failed. When configured, if a `ClusterExtensionRevision` fails to roll out within the specified duration, the `Progressing` condition is set to `False` with reason `ProgressDeadlineExceeded`. This signals that manual intervention is required and stops automatic retry attempts. Added unit and e2e test asserting the added behavior.
1 parent dc20dfb commit 5ed4b38

19 files changed

Lines changed: 480 additions & 4 deletions

File tree

api/v1/clusterextension_types.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,17 @@ type ClusterExtensionSpec struct {
107107
//
108108
// +optional
109109
Config *ClusterExtensionConfig `json:"config,omitempty"`
110+
111+
// progressDeadlineMinutes is an optional field that defines the maximum period
112+
// of time in minutes after which an installation should be considered failed and
113+
// require manual intervention. This functionality is disabled when no value
114+
// is provided. The minimum period is 10 minutes, and the maximum is 720 minutes (12 hours).
115+
//
116+
// +kubebuilder:validation:Minimum:=10
117+
// +kubebuilder:validation:Maximum:=720
118+
// +optional
119+
// <opcon:experimental>
120+
ProgressDeadlineMinutes int32 `json:"progressDeadlineMinutes,omitempty"`
110121
}
111122

112123
const SourceTypeCatalog = "Catalog"

api/v1/clusterextensionrevision_types.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,17 @@ type ClusterExtensionRevisionSpec struct {
8787
// +listMapKey=name
8888
// +optional
8989
Phases []ClusterExtensionRevisionPhase `json:"phases,omitempty"`
90+
91+
// progressDeadlineMinutes is an optional field that defines the maximum period
92+
// of time in minutes after which an installation should be considered failed and
93+
// require manual intervention. This functionality is disabled when no value
94+
// is provided. The minimum period is 10 minutes, and the maximum is 720 minutes (12 hours).
95+
//
96+
// +kubebuilder:validation:Minimum:=10
97+
// +kubebuilder:validation:Maximum:=720
98+
// +optional
99+
// <opcon:experimental>
100+
ProgressDeadlineMinutes int32 `json:"progressDeadlineMinutes,omitempty"`
90101
}
91102

92103
// ClusterExtensionRevisionLifecycleState specifies the lifecycle state of the ClusterExtensionRevision.

api/v1/clusterextensionrevision_types_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,56 @@ func TestClusterExtensionRevisionValidity(t *testing.T) {
140140
})
141141
}
142142
}
143+
144+
func TestClusterExtensionProgressDeadlineValidity(t *testing.T) {
145+
c := newClient(t)
146+
ctx := context.Background()
147+
i := 0
148+
for name, tc := range map[string]struct {
149+
spec ClusterExtensionRevisionSpec
150+
valid bool
151+
}{
152+
"invalid progress deadline < 10": {
153+
spec: ClusterExtensionRevisionSpec{
154+
ProgressDeadlineMinutes: 9,
155+
},
156+
valid: false,
157+
},
158+
"valid progress deadline = 10": {
159+
spec: ClusterExtensionRevisionSpec{
160+
ProgressDeadlineMinutes: 10,
161+
},
162+
valid: true,
163+
},
164+
"valid progress deadline <= 720": {
165+
spec: ClusterExtensionRevisionSpec{
166+
ProgressDeadlineMinutes: 720,
167+
},
168+
valid: true,
169+
},
170+
"invalid progress deadline > 720": {
171+
spec: ClusterExtensionRevisionSpec{
172+
ProgressDeadlineMinutes: 721,
173+
},
174+
valid: false,
175+
},
176+
} {
177+
t.Run(name, func(t *testing.T) {
178+
cer := &ClusterExtensionRevision{
179+
ObjectMeta: metav1.ObjectMeta{
180+
Name: fmt.Sprintf("bla%d", i),
181+
},
182+
Spec: tc.spec,
183+
}
184+
cer.Spec.Revision = 1
185+
i = i + 1
186+
err := c.Create(ctx, cer)
187+
if tc.valid && err != nil {
188+
t.Fatal("expected create to succeed, but got:", err)
189+
}
190+
if !tc.valid && !errors.IsInvalid(err) {
191+
t.Fatal("expected create to fail due to invalid payload, but got:", err)
192+
}
193+
})
194+
}
195+
}

api/v1/common_types.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const (
3232
ReasonDeprecated = "Deprecated"
3333

3434
// Common reasons
35-
ReasonSucceeded = "Succeeded"
36-
ReasonFailed = "Failed"
35+
ReasonSucceeded = "Succeeded"
36+
ReasonFailed = "Failed"
37+
ReasonProgressDeadlineExceeded = "ProgressDeadlineExceeded"
3738
)

docs/api-reference/olmv1-api-reference.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,7 @@ _Appears in:_
344344
| `source` _[SourceConfig](#sourceconfig)_ | source is required and selects the installation source of content for this ClusterExtension.<br />Set the sourceType field to perform the selection.<br />Catalog is currently the only implemented sourceType.<br />Setting sourceType to "Catalog" requires the catalog field to also be defined.<br />Below is a minimal example of a source definition (in yaml):<br />source:<br /> sourceType: Catalog<br /> catalog:<br /> packageName: example-package | | Required: \{\} <br /> |
345345
| `install` _[ClusterExtensionInstallConfig](#clusterextensioninstallconfig)_ | install is optional and configures installation options for the ClusterExtension,<br />such as the pre-flight check configuration. | | |
346346
| `config` _[ClusterExtensionConfig](#clusterextensionconfig)_ | config is optional and specifies bundle-specific configuration.<br />Configuration is bundle-specific and a bundle may provide a configuration schema.<br />When not specified, the default configuration of the resolved bundle is used.<br />config is validated against a configuration schema provided by the resolved bundle. If the bundle does not provide<br />a configuration schema the bundle is deemed to not be configurable. More information on how<br />to configure bundles can be found in the OLM documentation associated with your current OLM version. | | |
347+
| `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 /><opcon:experimental> | | Maximum: 720 <br />Minimum: 10 <br /> |
347348

348349

349350
#### ClusterExtensionStatus

helm/olmv1/base/operator-controller/crd/experimental/olm.operatorframework.io_clusterextensionrevisions.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,16 @@ spec:
166166
x-kubernetes-validations:
167167
- message: phases is immutable
168168
rule: self == oldSelf || oldSelf.size() == 0
169+
progressDeadlineMinutes:
170+
description: |-
171+
progressDeadlineMinutes is an optional field that defines the maximum period
172+
of time in minutes after which an installation should be considered failed and
173+
require manual intervention. This functionality is disabled when no value
174+
is provided. The minimum period is 10 minutes, and the maximum is 720 minutes (12 hours).
175+
format: int32
176+
maximum: 720
177+
minimum: 10
178+
type: integer
169179
revision:
170180
description: |-
171181
revision is a required, immutable sequence number representing a specific revision

helm/olmv1/base/operator-controller/crd/experimental/olm.operatorframework.io_clusterextensions.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,16 @@ spec:
165165
rule: self == oldSelf
166166
- message: namespace must be a valid DNS1123 label
167167
rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?$")
168+
progressDeadlineMinutes:
169+
description: |-
170+
progressDeadlineMinutes is an optional field that defines the maximum period
171+
of time in minutes after which an installation should be considered failed and
172+
require manual intervention. This functionality is disabled when no value
173+
is provided. The minimum period is 10 minutes, and the maximum is 720 minutes (12 hours).
174+
format: int32
175+
maximum: 720
176+
minimum: 10
177+
type: integer
168178
serviceAccount:
169179
description: |-
170180
serviceAccount specifies a ServiceAccount used to perform all interactions with the cluster

internal/operator-controller/applier/boxcutter.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ func (r *SimpleRevisionGenerator) buildClusterExtensionRevision(
191191
annotations[labels.ServiceAccountNameKey] = ext.Spec.ServiceAccount.Name
192192
annotations[labels.ServiceAccountNamespaceKey] = ext.Spec.Namespace
193193

194-
return &ocv1.ClusterExtensionRevision{
194+
cer := &ocv1.ClusterExtensionRevision{
195195
ObjectMeta: metav1.ObjectMeta{
196196
Annotations: annotations,
197197
Labels: map[string]string{
@@ -206,6 +206,10 @@ func (r *SimpleRevisionGenerator) buildClusterExtensionRevision(
206206
Phases: PhaseSort(objects),
207207
},
208208
}
209+
if p := ext.Spec.ProgressDeadlineMinutes; p > 0 {
210+
cer.Spec.ProgressDeadlineMinutes = p
211+
}
212+
return cer
209213
}
210214

211215
// BoxcutterStorageMigrator migrates ClusterExtensions from Helm-based storage to

internal/operator-controller/applier/boxcutter_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"k8s.io/apimachinery/pkg/runtime"
2323
"k8s.io/apimachinery/pkg/util/validation/field"
2424
k8scheme "k8s.io/client-go/kubernetes/scheme"
25+
"k8s.io/utils/ptr"
2526
"sigs.k8s.io/controller-runtime/pkg/client"
2627
"sigs.k8s.io/controller-runtime/pkg/client/fake"
2728
"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
@@ -327,6 +328,65 @@ func Test_SimpleRevisionGenerator_AppliesObjectLabelsAndRevisionAnnotations(t *t
327328
require.Equal(t, revAnnotations, rev.Annotations)
328329
}
329330

331+
func Test_SimpleRevisionGenerator_PropagatesProgressDeadlineMinutes(t *testing.T) {
332+
r := &FakeManifestProvider{
333+
GetFn: func(b fs.FS, e *ocv1.ClusterExtension) ([]client.Object, error) {
334+
return []client.Object{}, nil
335+
},
336+
}
337+
338+
b := applier.SimpleRevisionGenerator{
339+
Scheme: k8scheme.Scheme,
340+
ManifestProvider: r,
341+
}
342+
343+
type args struct {
344+
progressDeadlineMinutes *int32
345+
}
346+
type want struct {
347+
progressDeadlineMinutes int32
348+
}
349+
type testCase struct {
350+
args args
351+
want want
352+
}
353+
for name, tc := range map[string]testCase{
354+
"propagates when set": {
355+
args: args{
356+
progressDeadlineMinutes: ptr.To(int32(10)),
357+
},
358+
want: want{
359+
progressDeadlineMinutes: 10,
360+
},
361+
},
362+
"do not propagate when unset": {
363+
want: want{
364+
progressDeadlineMinutes: 0,
365+
},
366+
},
367+
} {
368+
ext := &ocv1.ClusterExtension{
369+
ObjectMeta: metav1.ObjectMeta{
370+
Name: "test-extension",
371+
},
372+
Spec: ocv1.ClusterExtensionSpec{
373+
Namespace: "test-namespace",
374+
ServiceAccount: ocv1.ServiceAccountReference{Name: "test-sa"},
375+
},
376+
}
377+
empty := map[string]string{}
378+
t.Run(name, func(t *testing.T) {
379+
if pd := tc.args.progressDeadlineMinutes; pd != nil {
380+
ext.Spec.ProgressDeadlineMinutes = *pd
381+
}
382+
383+
rev, err := b.GenerateRevision(t.Context(), fstest.MapFS{}, ext, empty, empty)
384+
require.NoError(t, err)
385+
require.Equal(t, tc.want.progressDeadlineMinutes, rev.Spec.ProgressDeadlineMinutes)
386+
})
387+
}
388+
}
389+
330390
func Test_SimpleRevisionGenerator_Failure(t *testing.T) {
331391
r := &FakeManifestProvider{
332392
GetFn: func(b fs.FS, e *ocv1.ClusterExtension) ([]client.Object, error) {

internal/operator-controller/conditionsets/conditionsets.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,5 @@ var ConditionReasons = []string{
4141
ocv1.ReasonRetrying,
4242
ocv1.ReasonAbsent,
4343
ocv1.ReasonRollingOut,
44+
ocv1.ReasonProgressDeadlineExceeded,
4445
}

0 commit comments

Comments
 (0)