Skip to content

Commit b2da731

Browse files
fix: Allow spec changes after ProgressDeadlineExceeded
Once ProgressDeadlineExceeded is set, the controller predicate was blocking ALL updates including spec changes like lifecycleState=Archived. This prevented timed-out ClusterObjectSets from being archived. Now allows generation/spec changes while still suppressing status-only churn.
1 parent c304741 commit b2da731

File tree

2 files changed

+220
-14
lines changed

2 files changed

+220
-14
lines changed

internal/operator-controller/controllers/clusterobjectset_controller.go

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -343,22 +343,44 @@ type Sourcoser interface {
343343
Source(handler handler.EventHandler, predicates ...predicate.Predicate) source.Source
344344
}
345345

346+
// skipProgressDeadlineExceededUpdateFunc is the update predicate function that
347+
// prevents reconciliation churn for ClusterObjectSets that have reached ProgressDeadlineExceeded.
348+
// It allows deletions and spec/generation changes (including lifecycleState transitions)
349+
// but suppresses status-only updates.
350+
func skipProgressDeadlineExceededUpdateFunc(e event.UpdateEvent) bool {
351+
newRev, ok := e.ObjectNew.(*ocv1.ClusterObjectSet)
352+
if !ok {
353+
return true
354+
}
355+
oldRev, ok := e.ObjectOld.(*ocv1.ClusterObjectSet)
356+
if !ok {
357+
return true
358+
}
359+
360+
// Allow deletions to happen
361+
if !newRev.DeletionTimestamp.IsZero() {
362+
return true
363+
}
364+
365+
// Check if the revision has ProgressDeadlineExceeded condition
366+
cnd := meta.FindStatusCondition(newRev.Status.Conditions, ocv1.ClusterObjectSetTypeProgressing)
367+
if cnd == nil || cnd.Status != metav1.ConditionFalse || cnd.Reason != ocv1.ReasonProgressDeadlineExceeded {
368+
return true
369+
}
370+
371+
// Progress deadline exceeded - allow generation/spec changes but suppress status-only churn
372+
// Allow if generation changed (indicates spec change, including lifecycleState transitions)
373+
if oldRev.Generation != newRev.Generation {
374+
return true
375+
}
376+
377+
// Suppress status-only updates
378+
return false
379+
}
380+
346381
func (c *ClusterObjectSetReconciler) SetupWithManager(mgr ctrl.Manager) error {
347382
skipProgressDeadlineExceededPredicate := predicate.Funcs{
348-
UpdateFunc: func(e event.UpdateEvent) bool {
349-
rev, ok := e.ObjectNew.(*ocv1.ClusterObjectSet)
350-
if !ok {
351-
return true
352-
}
353-
// allow deletions to happen
354-
if !rev.DeletionTimestamp.IsZero() {
355-
return true
356-
}
357-
if cnd := meta.FindStatusCondition(rev.Status.Conditions, ocv1.ClusterObjectSetTypeProgressing); cnd != nil && cnd.Status == metav1.ConditionFalse && cnd.Reason == ocv1.ReasonProgressDeadlineExceeded {
358-
return false
359-
}
360-
return true
361-
},
383+
UpdateFunc: skipProgressDeadlineExceededUpdateFunc,
362384
}
363385
c.Clock = clock.RealClock{}
364386
return ctrl.NewControllerManagedBy(mgr).

internal/operator-controller/controllers/clusterobjectset_controller_internal_test.go

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"sigs.k8s.io/controller-runtime/pkg/client"
1515
"sigs.k8s.io/controller-runtime/pkg/client/fake"
1616
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
17+
"sigs.k8s.io/controller-runtime/pkg/event"
1718
"sigs.k8s.io/controller-runtime/pkg/handler"
1819
"sigs.k8s.io/controller-runtime/pkg/predicate"
1920
"sigs.k8s.io/controller-runtime/pkg/source"
@@ -235,3 +236,186 @@ func (m *mockTrackingCacheInternal) Watch(ctx context.Context, user client.Objec
235236
func (m *mockTrackingCacheInternal) Source(h handler.EventHandler, predicates ...predicate.Predicate) source.Source {
236237
return nil
237238
}
239+
240+
func Test_skipProgressDeadlineExceededUpdateFunc(t *testing.T) {
241+
for _, tc := range []struct {
242+
name string
243+
oldRev *ocv1.ClusterObjectSet
244+
newRev *ocv1.ClusterObjectSet
245+
expected bool
246+
}{
247+
{
248+
name: "allow deletion",
249+
oldRev: &ocv1.ClusterObjectSet{
250+
ObjectMeta: metav1.ObjectMeta{Generation: 1},
251+
Status: ocv1.ClusterObjectSetStatus{
252+
Conditions: []metav1.Condition{
253+
{
254+
Type: ocv1.ClusterObjectSetTypeProgressing,
255+
Status: metav1.ConditionFalse,
256+
Reason: ocv1.ReasonProgressDeadlineExceeded,
257+
},
258+
},
259+
},
260+
},
261+
newRev: &ocv1.ClusterObjectSet{
262+
ObjectMeta: metav1.ObjectMeta{
263+
Generation: 1,
264+
DeletionTimestamp: &metav1.Time{Time: time.Now()},
265+
},
266+
Status: ocv1.ClusterObjectSetStatus{
267+
Conditions: []metav1.Condition{
268+
{
269+
Type: ocv1.ClusterObjectSetTypeProgressing,
270+
Status: metav1.ConditionFalse,
271+
Reason: ocv1.ReasonProgressDeadlineExceeded,
272+
},
273+
},
274+
},
275+
},
276+
expected: true,
277+
},
278+
{
279+
name: "allow when ProgressDeadlineExceeded condition not present",
280+
oldRev: &ocv1.ClusterObjectSet{
281+
ObjectMeta: metav1.ObjectMeta{Generation: 1},
282+
},
283+
newRev: &ocv1.ClusterObjectSet{
284+
ObjectMeta: metav1.ObjectMeta{Generation: 1},
285+
},
286+
expected: true,
287+
},
288+
{
289+
name: "allow when Progressing condition is true",
290+
oldRev: &ocv1.ClusterObjectSet{
291+
ObjectMeta: metav1.ObjectMeta{Generation: 1},
292+
Status: ocv1.ClusterObjectSetStatus{
293+
Conditions: []metav1.Condition{
294+
{
295+
Type: ocv1.ClusterObjectSetTypeProgressing,
296+
Status: metav1.ConditionTrue,
297+
Reason: ocv1.ReasonRollingOut,
298+
},
299+
},
300+
},
301+
},
302+
newRev: &ocv1.ClusterObjectSet{
303+
ObjectMeta: metav1.ObjectMeta{Generation: 1},
304+
Status: ocv1.ClusterObjectSetStatus{
305+
Conditions: []metav1.Condition{
306+
{
307+
Type: ocv1.ClusterObjectSetTypeProgressing,
308+
Status: metav1.ConditionTrue,
309+
Reason: ocv1.ReasonRollingOut,
310+
},
311+
},
312+
},
313+
},
314+
expected: true,
315+
},
316+
{
317+
name: "allow generation change when ProgressDeadlineExceeded",
318+
oldRev: &ocv1.ClusterObjectSet{
319+
ObjectMeta: metav1.ObjectMeta{Generation: 1},
320+
Status: ocv1.ClusterObjectSetStatus{
321+
Conditions: []metav1.Condition{
322+
{
323+
Type: ocv1.ClusterObjectSetTypeProgressing,
324+
Status: metav1.ConditionFalse,
325+
Reason: ocv1.ReasonProgressDeadlineExceeded,
326+
},
327+
},
328+
},
329+
},
330+
newRev: &ocv1.ClusterObjectSet{
331+
ObjectMeta: metav1.ObjectMeta{Generation: 2}, // Generation changed
332+
Status: ocv1.ClusterObjectSetStatus{
333+
Conditions: []metav1.Condition{
334+
{
335+
Type: ocv1.ClusterObjectSetTypeProgressing,
336+
Status: metav1.ConditionFalse,
337+
Reason: ocv1.ReasonProgressDeadlineExceeded,
338+
},
339+
},
340+
},
341+
},
342+
expected: true,
343+
},
344+
{
345+
name: "suppress status-only update when ProgressDeadlineExceeded",
346+
oldRev: &ocv1.ClusterObjectSet{
347+
ObjectMeta: metav1.ObjectMeta{Generation: 1},
348+
Status: ocv1.ClusterObjectSetStatus{
349+
Conditions: []metav1.Condition{
350+
{
351+
Type: ocv1.ClusterObjectSetTypeProgressing,
352+
Status: metav1.ConditionFalse,
353+
Reason: ocv1.ReasonProgressDeadlineExceeded,
354+
},
355+
},
356+
},
357+
},
358+
newRev: &ocv1.ClusterObjectSet{
359+
ObjectMeta: metav1.ObjectMeta{Generation: 1}, // Same generation
360+
Status: ocv1.ClusterObjectSetStatus{
361+
Conditions: []metav1.Condition{
362+
{
363+
Type: ocv1.ClusterObjectSetTypeProgressing,
364+
Status: metav1.ConditionFalse,
365+
Reason: ocv1.ReasonProgressDeadlineExceeded,
366+
},
367+
{
368+
Type: ocv1.ClusterObjectSetTypeAvailable,
369+
Status: metav1.ConditionFalse,
370+
Reason: "SomeReason",
371+
},
372+
},
373+
},
374+
},
375+
expected: false,
376+
},
377+
{
378+
name: "allow spec change via generation bump when ProgressDeadlineExceeded (lifecycleState transition)",
379+
oldRev: &ocv1.ClusterObjectSet{
380+
ObjectMeta: metav1.ObjectMeta{Generation: 1},
381+
Spec: ocv1.ClusterObjectSetSpec{
382+
LifecycleState: ocv1.ClusterObjectSetLifecycleStateActive,
383+
},
384+
Status: ocv1.ClusterObjectSetStatus{
385+
Conditions: []metav1.Condition{
386+
{
387+
Type: ocv1.ClusterObjectSetTypeProgressing,
388+
Status: metav1.ConditionFalse,
389+
Reason: ocv1.ReasonProgressDeadlineExceeded,
390+
},
391+
},
392+
},
393+
},
394+
newRev: &ocv1.ClusterObjectSet{
395+
ObjectMeta: metav1.ObjectMeta{Generation: 2}, // Generation bumped due to spec change
396+
Spec: ocv1.ClusterObjectSetSpec{
397+
LifecycleState: ocv1.ClusterObjectSetLifecycleStateArchived,
398+
},
399+
Status: ocv1.ClusterObjectSetStatus{
400+
Conditions: []metav1.Condition{
401+
{
402+
Type: ocv1.ClusterObjectSetTypeProgressing,
403+
Status: metav1.ConditionFalse,
404+
Reason: ocv1.ReasonProgressDeadlineExceeded,
405+
},
406+
},
407+
},
408+
},
409+
expected: true,
410+
},
411+
} {
412+
t.Run(tc.name, func(t *testing.T) {
413+
e := event.UpdateEvent{
414+
ObjectOld: tc.oldRev,
415+
ObjectNew: tc.newRev,
416+
}
417+
result := skipProgressDeadlineExceededUpdateFunc(e)
418+
require.Equal(t, tc.expected, result)
419+
})
420+
}
421+
}

0 commit comments

Comments
 (0)