Skip to content

Commit 94ff7df

Browse files
Per G. da Silvaclaude
andcommitted
⚠️ Remove Succeeded condition type from ClusterObjectSet
Replace the ClusterObjectSet Succeeded condition type with the status.completedAt field as the signal that a revision has rolled out. The ClusterExtension status mapping and the progress deadline check now key off completedAt instead of the Succeeded condition, and the Helm-to-boxcutter migrator records completedAt on migrated revisions. Since ClusterObjectSet is experimental and does not guarantee upgrade safety, this is a clean cut with no backward-compatibility fallback. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Per G. da Silva <pegoncal@redhat.com>
1 parent 2774587 commit 94ff7df

13 files changed

Lines changed: 116 additions & 111 deletions

File tree

‎api/v1/clusterobjectset_types.go‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ const (
2828
// Condition Types
2929
ClusterObjectSetTypeAvailable = "Available"
3030
ClusterObjectSetTypeProgressing = "Progressing"
31-
ClusterObjectSetTypeSucceeded = "Succeeded"
3231

3332
// Condition Reasons
3433
ClusterObjectSetReasonArchived = "Archived"
@@ -510,9 +509,6 @@ type ClusterObjectSetStatus struct {
510509
// - When status is Unknown and reason is Archived, the ClusterObjectSet has been archived and its objects have been torn down.
511510
// - When status is Unknown and reason is Migrated, the ClusterObjectSet was migrated from an existing release and object status probe results have not yet been observed.
512511
//
513-
// The Succeeded condition represents whether the revision has successfully completed its rollout:
514-
// - When status is True and reason is Succeeded, the ClusterObjectSet has successfully completed its rollout. This condition is set once and persists even if the revision later becomes unavailable.
515-
//
516512
// +listType=map
517513
// +listMapKey=type
518514
// +optional

‎applyconfigurations/api/v1/clusterobjectsetstatus.go‎

Lines changed: 0 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -570,9 +570,6 @@ spec:
570570
- When status is Unknown and reason is Reconciling, the ClusterObjectSet has encountered an error that prevented it from observing the probes.
571571
- When status is Unknown and reason is Archived, the ClusterObjectSet has been archived and its objects have been torn down.
572572
- When status is Unknown and reason is Migrated, the ClusterObjectSet was migrated from an existing release and object status probe results have not yet been observed.
573-
574-
The Succeeded condition represents whether the revision has successfully completed its rollout:
575-
- When status is True and reason is Succeeded, the ClusterObjectSet has successfully completed its rollout. This condition is set once and persists even if the revision later becomes unavailable.
576573
items:
577574
description: Condition contains details for one aspect of the current
578575
state of this API Resource.

‎internal/object-controller/controllers/clusterobjectset_controller.go‎

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -252,20 +252,11 @@ func (c *ClusterObjectSetReconciler) reconcile(ctx context.Context, cos *ocv1.Cl
252252

253253
// Record the timestamp of the first time the revision was observed to be
254254
// ready. This is set once and never changes for subsequent reconciliations.
255+
// It also serves as the signal that the revision has completed its rollout,
256+
// which the ClusterExtension controller uses to determine the installed revision.
255257
if cos.Status.CompletedAt.IsZero() {
256258
cos.Status.CompletedAt = metav1.NewTime(c.Clock.Now())
257259
}
258-
259-
// We'll probably only want to remove this once we are done updating the ClusterExtension conditions
260-
// as its one of the interfaces between the revision and the extension. If we still have the Succeeded for now
261-
// that's fine.
262-
meta.SetStatusCondition(&cos.Status.Conditions, metav1.Condition{
263-
Type: ocv1.ClusterObjectSetTypeSucceeded,
264-
Status: metav1.ConditionTrue,
265-
Reason: ocv1.ReasonSucceeded,
266-
Message: "Revision succeeded rolling out.",
267-
ObservedGeneration: cos.Generation,
268-
})
269260
} else {
270261
var probeFailureMsgs []string
271262
for _, pres := range rres.GetPhases() {

‎internal/object-controller/controllers/clusterobjectset_controller_test.go‎

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ func Test_ClusterObjectSetReconciler_Reconcile_RevisionReconciliation(t *testing
398398
},
399399
},
400400
{
401-
name: "set Available:True:ProbesSucceeded and Succeeded:True:Succeeded conditions on successful revision rollout",
401+
name: "set Available:True:ProbesSucceeded condition and completedAt on successful revision rollout",
402402
revisionResult: newMockRevisionResult(mockCtrl, revisionResultConfig{
403403
isComplete: true,
404404
}),
@@ -428,12 +428,7 @@ func Test_ClusterObjectSetReconciler_Reconcile_RevisionReconciliation(t *testing
428428
require.Equal(t, "Revision 1.0.0 has rolled out.", cond.Message)
429429
require.Equal(t, int64(1), cond.ObservedGeneration)
430430

431-
cond = meta.FindStatusCondition(rev.Status.Conditions, ocv1.ClusterObjectSetTypeSucceeded)
432-
require.NotNil(t, cond)
433-
require.Equal(t, metav1.ConditionTrue, cond.Status)
434-
require.Equal(t, ocv1.ReasonSucceeded, cond.Reason)
435-
require.Equal(t, "Revision succeeded rolling out.", cond.Message)
436-
require.Equal(t, int64(1), cond.ObservedGeneration)
431+
require.False(t, rev.Status.CompletedAt.IsZero(), "completedAt should be set on successful rollout")
437432
},
438433
},
439434
{
@@ -1137,12 +1132,7 @@ func Test_ClusterObjectSetReconciler_Reconcile_ProgressDeadline(t *testing.T) {
11371132
Reason: ocv1.ReasonSucceeded,
11381133
ObservedGeneration: rev1.Generation,
11391134
})
1140-
meta.SetStatusCondition(&rev1.Status.Conditions, metav1.Condition{
1141-
Type: ocv1.ClusterObjectSetTypeSucceeded,
1142-
Status: metav1.ConditionTrue,
1143-
Reason: ocv1.ReasonSucceeded,
1144-
ObservedGeneration: rev1.Generation,
1145-
})
1135+
rev1.Status.CompletedAt = metav1.Now()
11461136
return []client.Object{rev1, ext}
11471137
},
11481138
revisionResult: newMockRevisionResult(mockCtrl, revisionResultConfig{

‎internal/object-controller/controllers/progress_deadline.go‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"sync"
88
"time"
99

10-
"k8s.io/apimachinery/pkg/api/meta"
1110
"k8s.io/client-go/util/workqueue"
1211
"k8s.io/utils/clock"
1312
ctrl "sigs.k8s.io/controller-runtime"
@@ -78,25 +77,26 @@ func (r *deadlineAwareRateLimiter) NumRequeues(item ctrl.Request) int {
7877
// expires. A negative duration means the deadline has already passed.
7978
//
8079
// It derives the deadline from spec and metadata only, with one exception:
81-
// it checks the Succeeded status condition so that a revision recovering
82-
// from drift is not penalised by the original deadline.
80+
// it checks status.completedAt so that a revision recovering from drift is not
81+
// penalised by the original deadline.
8382
//
84-
// Succeeded is a latch: there is no way to deduce from current cluster state
85-
// alone that a COS succeeded in the past. If Succeeded is removed or set to
86-
// False, this function will return a deadline and the reconciler will set
87-
// ProgressDeadlineExceeded even though the revision previously succeeded.
83+
// completedAt is a latch: there is no way to deduce from current cluster state
84+
// alone that a COS became ready in the past. It is set once and never cleared,
85+
// so once observed ready a revision is never subject to the deadline again.
8886
//
8987
// Returns (0, false) when there is no active deadline:
9088
// - progressDeadlineMinutes is 0
91-
// - the revision has already succeeded
89+
// - the revision has already been observed ready (completedAt set)
9290
// - the revision is archived (deadline is irrelevant)
9391
// - the revision is being deleted
9492
func durationUntilDeadline(clk clock.Clock, cos *ocv1.ClusterObjectSet) (time.Duration, bool) {
9593
pd := cos.Spec.ProgressDeadlineMinutes
9694
if pd <= 0 {
9795
return 0, false
9896
}
99-
if meta.IsStatusConditionTrue(cos.Status.Conditions, ocv1.ClusterObjectSetTypeSucceeded) {
97+
// Once the revision has been observed ready (completedAt set), the deadline
98+
// no longer applies.
99+
if !cos.Status.CompletedAt.IsZero() {
100100
return 0, false
101101
}
102102
if cos.Spec.LifecycleState == ocv1.ClusterObjectSetLifecycleStateArchived {

‎internal/object-controller/controllers/progress_deadline_test.go‎

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,12 @@ func TestDurationUntilDeadline(t *testing.T) {
3838
expectHasDeadline: false,
3939
},
4040
{
41-
name: "Succeeded is true — no deadline",
41+
name: "completedAt is set — no deadline",
4242
cos: ocv1.ClusterObjectSet{
4343
ObjectMeta: metav1.ObjectMeta{CreationTimestamp: metav1.NewTime(creation)},
4444
Spec: ocv1.ClusterObjectSetSpec{ProgressDeadlineMinutes: 1, LifecycleState: ocv1.ClusterObjectSetLifecycleStateActive},
4545
Status: ocv1.ClusterObjectSetStatus{
46-
Conditions: []metav1.Condition{{
47-
Type: ocv1.ClusterObjectSetTypeSucceeded,
48-
Status: metav1.ConditionTrue,
49-
}},
46+
CompletedAt: metav1.NewTime(creation),
5047
},
5148
},
5249
expectDuration: 0,

‎internal/operator-controller/applier/boxcutter.go‎

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import (
1717
corev1 "k8s.io/api/core/v1"
1818
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
1919
apierrors "k8s.io/apimachinery/pkg/api/errors"
20-
"k8s.io/apimachinery/pkg/api/meta"
2120
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2221
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2322
"k8s.io/apimachinery/pkg/runtime"
@@ -376,8 +375,8 @@ func (m *BoxcutterStorageMigrator) ensureMigratedRevisionStatus(ctx context.Cont
376375
if revisions[i].Spec.Revision != 1 {
377376
continue
378377
}
379-
// Skip if already succeeded - status is already set correctly.
380-
if meta.IsStatusConditionTrue(revisions[i].Status.Conditions, ocv1.ClusterObjectSetTypeSucceeded) {
378+
// Skip if already completed - status is already set correctly.
379+
if !revisions[i].Status.CompletedAt.IsZero() {
381380
return nil
382381
}
383382
// Ensure revision 1 status is set correctly, including for previously migrated
@@ -416,8 +415,8 @@ func (m *BoxcutterStorageMigrator) findLatestDeployedRelease(ac helmclient.Actio
416415
return latestDeployed, nil
417416
}
418417

419-
// ensureRevisionStatus ensures the revision has the Succeeded status condition set.
420-
// Returns nil if the status is already set or after successfully setting it.
418+
// ensureRevisionStatus ensures the revision has completedAt set, marking it as
419+
// installed. Returns nil if the status is already set or after successfully setting it.
421420
// Only sets status on revisions that were actually migrated from Helm (marked with MigratedFromHelmKey label).
422421
func (m *BoxcutterStorageMigrator) ensureRevisionStatus(ctx context.Context, name string) error {
423422
rev := &ocv1.ClusterObjectSet{}
@@ -426,25 +425,22 @@ func (m *BoxcutterStorageMigrator) ensureRevisionStatus(ctx context.Context, nam
426425
}
427426

428427
// Only set status if this revision was actually migrated from Helm.
429-
// This prevents us from incorrectly marking normal Boxcutter revision 1 as succeeded
428+
// This prevents us from incorrectly marking normal Boxcutter revision 1 as completed
430429
// when it's still in progress.
431430
if rev.Labels[labels.MigratedFromHelmKey] != "true" {
432431
return nil
433432
}
434433

435-
// Check if status is already set to Succeeded=True
436-
if meta.IsStatusConditionTrue(rev.Status.Conditions, ocv1.ClusterObjectSetTypeSucceeded) {
434+
// Check if completedAt is already set.
435+
if !rev.Status.CompletedAt.IsZero() {
437436
return nil
438437
}
439438

440-
// Set the Succeeded status condition
441-
meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{
442-
Type: ocv1.ClusterObjectSetTypeSucceeded,
443-
Status: metav1.ConditionTrue,
444-
Reason: ocv1.ReasonSucceeded,
445-
Message: "Revision succeeded - migrated from Helm release",
446-
ObservedGeneration: rev.GetGeneration(),
447-
})
439+
// Since we're migrating from a successfully deployed Helm release, the revision
440+
// represents a working installation. Record completedAt so the ClusterExtension
441+
// controller treats it as installed. The original ready time is not recoverable,
442+
// so we use the migration time.
443+
rev.Status.CompletedAt = metav1.Now()
448444

449445
if err := m.Client.Status().Update(ctx, rev); err != nil {
450446
return fmt.Errorf("updating migrated revision status: %w", err)

‎internal/operator-controller/applier/boxcutter_test.go‎

Lines changed: 12 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import (
1717
appsv1 "k8s.io/api/apps/v1"
1818
corev1 "k8s.io/api/core/v1"
1919
apierrors "k8s.io/apimachinery/pkg/api/errors"
20-
apimeta "k8s.io/apimachinery/pkg/api/meta"
2120
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2221
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2322
"k8s.io/apimachinery/pkg/runtime"
@@ -1196,18 +1195,13 @@ func TestBoxcutterStorageMigrator(t *testing.T) {
11961195
err := sm.Migrate(t.Context(), ext, map[string]string{"my-label": "my-value"})
11971196
require.NoError(t, err)
11981197

1199-
// Verify the migrated revision has Succeeded=True status with Succeeded reason and a migration message
1198+
// Verify the migrated revision has completedAt set, marking it as installed
12001199
require.NotNil(t, updatedObj, "Updated object should not be nil")
12011200

12021201
rev, ok := updatedObj.(*ocv1.ClusterObjectSet)
12031202
require.True(t, ok, "Updated object should be a ClusterObjectSet")
12041203

1205-
succeededCond := apimeta.FindStatusCondition(rev.Status.Conditions, ocv1.ClusterObjectSetTypeSucceeded)
1206-
require.NotNil(t, succeededCond, "Succeeded condition should be set")
1207-
assert.Equal(t, metav1.ConditionTrue, succeededCond.Status, "Succeeded condition should be True")
1208-
assert.Equal(t, ocv1.ReasonSucceeded, succeededCond.Reason, "Reason should be Succeeded")
1209-
assert.Equal(t, "Revision succeeded - migrated from Helm release", succeededCond.Message, "Message should indicate Helm migration")
1210-
assert.Equal(t, int64(1), succeededCond.ObservedGeneration, "ObservedGeneration should match revision generation")
1204+
assert.False(t, rev.Status.CompletedAt.IsZero(), "completedAt should be set on migrated revision")
12111205
})
12121206

12131207
t.Run("does not create revision when revisions exist", func(t *testing.T) {
@@ -1245,13 +1239,7 @@ func TestBoxcutterStorageMigrator(t *testing.T) {
12451239
Revision: 1, // Migration creates revision 1
12461240
},
12471241
Status: ocv1.ClusterObjectSetStatus{
1248-
Conditions: []metav1.Condition{
1249-
{
1250-
Type: ocv1.ClusterObjectSetTypeSucceeded,
1251-
Status: metav1.ConditionTrue,
1252-
Reason: ocv1.ReasonSucceeded,
1253-
},
1254-
},
1242+
CompletedAt: metav1.Now(),
12551243
},
12561244
}
12571245

@@ -1334,13 +1322,10 @@ func TestBoxcutterStorageMigrator(t *testing.T) {
13341322
rev, ok := updatedObj.(*ocv1.ClusterObjectSet)
13351323
require.True(t, ok, "Updated object should be a ClusterObjectSet")
13361324

1337-
succeededCond := apimeta.FindStatusCondition(rev.Status.Conditions, ocv1.ClusterObjectSetTypeSucceeded)
1338-
require.NotNil(t, succeededCond, "Succeeded condition should be set")
1339-
assert.Equal(t, metav1.ConditionTrue, succeededCond.Status, "Succeeded condition should be True")
1340-
assert.Equal(t, ocv1.ReasonSucceeded, succeededCond.Reason, "Reason should be Succeeded")
1325+
assert.False(t, rev.Status.CompletedAt.IsZero(), "completedAt should be set")
13411326
})
13421327

1343-
t.Run("updates status from False to True for migrated revision", func(t *testing.T) {
1328+
t.Run("sets completedAt for migrated revision that has not completed", func(t *testing.T) {
13441329
testScheme := runtime.NewScheme()
13451330
require.NoError(t, ocv1.AddToScheme(testScheme))
13461331

@@ -1364,8 +1349,8 @@ func TestBoxcutterStorageMigrator(t *testing.T) {
13641349
FieldOwner: "test-owner",
13651350
}
13661351

1367-
// Migrated revision with Succeeded=False (e.g., from a previous failed status update attempt)
1368-
// This simulates a revision whose Succeeded condition should be corrected from False to True during migration.
1352+
// Migrated revision without completedAt (e.g., from a previous failed status update attempt).
1353+
// This simulates a revision whose completedAt should be set during migration.
13691354
existingRev := ocv1.ClusterObjectSet{
13701355
ObjectMeta: metav1.ObjectMeta{
13711356
Name: "test-revision",
@@ -1377,15 +1362,7 @@ func TestBoxcutterStorageMigrator(t *testing.T) {
13771362
Spec: ocv1.ClusterObjectSetSpec{
13781363
Revision: 1,
13791364
},
1380-
Status: ocv1.ClusterObjectSetStatus{
1381-
Conditions: []metav1.Condition{
1382-
{
1383-
Type: ocv1.ClusterObjectSetTypeSucceeded,
1384-
Status: metav1.ConditionFalse, // Important: False, not missing
1385-
Reason: "InProgress",
1386-
},
1387-
},
1388-
},
1365+
// completedAt is not set - simulating a revision that was migrated but never marked completed.
13891366
}
13901367

13911368
mockClient.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any()).
@@ -1412,16 +1389,13 @@ func TestBoxcutterStorageMigrator(t *testing.T) {
14121389
err := sm.Migrate(t.Context(), ext, map[string]string{"my-label": "my-value"})
14131390
require.NoError(t, err)
14141391

1415-
// Verify the status was updated from False to True
1392+
// Verify completedAt was set
14161393
require.NotNil(t, updatedObj, "Updated object should not be nil")
14171394

14181395
rev, ok := updatedObj.(*ocv1.ClusterObjectSet)
14191396
require.True(t, ok, "Updated object should be a ClusterObjectSet")
14201397

1421-
succeededCond := apimeta.FindStatusCondition(rev.Status.Conditions, ocv1.ClusterObjectSetTypeSucceeded)
1422-
require.NotNil(t, succeededCond, "Succeeded condition should be set")
1423-
assert.Equal(t, metav1.ConditionTrue, succeededCond.Status, "Succeeded condition should be updated to True")
1424-
assert.Equal(t, ocv1.ReasonSucceeded, succeededCond.Reason, "Reason should be Succeeded")
1398+
assert.False(t, rev.Status.CompletedAt.IsZero(), "completedAt should be set during migration")
14251399
})
14261400

14271401
t.Run("does not set status on non-migrated revision 1", func(t *testing.T) {
@@ -1569,15 +1543,13 @@ func TestBoxcutterStorageMigrator(t *testing.T) {
15691543
err := sm.Migrate(t.Context(), ext, map[string]string{"my-label": "my-value"})
15701544
require.NoError(t, err)
15711545

1572-
// Verify the migrated revision has Succeeded=True status
1546+
// Verify the migrated revision has completedAt set
15731547
require.NotNil(t, updatedObj, "Updated object should not be nil")
15741548

15751549
rev, ok := updatedObj.(*ocv1.ClusterObjectSet)
15761550
require.True(t, ok, "Updated object should be a ClusterObjectSet")
15771551

1578-
succeededCond := apimeta.FindStatusCondition(rev.Status.Conditions, ocv1.ClusterObjectSetTypeSucceeded)
1579-
require.NotNil(t, succeededCond, "Succeeded condition should be set")
1580-
assert.Equal(t, metav1.ConditionTrue, succeededCond.Status, "Succeeded condition should be True")
1552+
assert.False(t, rev.Status.CompletedAt.IsZero(), "completedAt should be set on migrated revision")
15811553
})
15821554

15831555
t.Run("does not create revision when helm release is not deployed and no deployed history", func(t *testing.T) {

‎internal/operator-controller/controllers/boxcutter_reconcile_steps.go‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,9 @@ func (d *BoxcutterRevisionStatesGetter) GetRevisionStates(ctx context.Context, e
7676
rm.Release = &releaseValue
7777
}
7878

79-
if apimeta.IsStatusConditionTrue(rev.Status.Conditions, ocv1.ClusterObjectSetTypeSucceeded) {
79+
// A revision is considered installed once it has been observed ready at
80+
// least once, recorded by status.completedAt.
81+
if !rev.Status.CompletedAt.IsZero() {
8082
rs.Installed = rm
8183
} else {
8284
rs.RollingOut = append(rs.RollingOut, rm)

0 commit comments

Comments
 (0)