Skip to content

Commit 9eae3e6

Browse files
authored
fix: ResourceGroup resource status bugs (#1576)
- Fixed a bug that was causing the status field to remain Current after the object had successfully reconciled, even if the object later became InProgress, Terminating, or Failed. This will cause nomos status and the Cloud Console UI to correctly show the current status after initial reconciliation, rather than just the status based on the last time the Applier ran. - Fixed a bug that was causing the status field to become Unknown when the object is actually Terminating when the actuation is empty/unknown, pending, skipped, or failed. Now, the Terminating and NotFound statuses should be correctly exposed for previously applied objects, even if the object spec has not been updated to match the source of truth yet. - Refactored ActuationStatusToLegacy to be easier to read, with extra comments to explain the reasoning. - Adjusted the test expectations for the new behavior
1 parent 88b384a commit 9eae3e6

File tree

3 files changed

+103
-40
lines changed

3 files changed

+103
-40
lines changed

pkg/resourcegroup/controllers/resourcegroup/resourcegroup_controller.go

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -327,16 +327,16 @@ func (r *reconciler) computeStatus(
327327
hasErr = true
328328
}
329329

330-
// Update the legacy status field based on the actuation, strategy and reconcile
331-
// statuses set by cli-utils. If the actuation is not successful, update the legacy
332-
// status field to be of unknown status.
330+
// Keep the existing object statuses written by cli-utils
333331
aStatus, exists := actuationStatuses[resStatus.ObjMetadata]
334332
if exists {
335333
resStatus.Actuation = aStatus.Actuation
336334
resStatus.Strategy = aStatus.Strategy
337335
resStatus.Reconcile = aStatus.Reconcile
338336

339-
resStatus.Status = ActuationStatusToLegacy(resStatus)
337+
// Update the status field to reflect source -> spec -> status,
338+
// not just spec -> status.
339+
resStatus.Status = UpdateStatusToReflectActuation(resStatus)
340340
}
341341

342342
// add the resource status into resgroup
@@ -350,23 +350,45 @@ func (r *reconciler) computeStatus(
350350
return statuses
351351
}
352352

353-
// ActuationStatusToLegacy contains the logic/rules to convert from the actuation statuses
354-
// to the legacy status field. If conversion is not needed, the original status field is returned
355-
// instead.
356-
func ActuationStatusToLegacy(s v1alpha1.ResourceStatus) v1alpha1.Status {
357-
if s.Status == v1alpha1.NotFound {
358-
return v1alpha1.NotFound
353+
// UpdateStatusToReflectActuation updates the latest computed kstatus to reflect
354+
// the desired state in the source of truth, as much as possible, not just the
355+
// desired state that has been persisted to the Kubernetes API.
356+
//
357+
// This is necessary, because kstatus only reflects whether the object status
358+
// reflects the object spec. But in Config Sync, the desired state is actually
359+
// in the source or truth. So if the latest desired state has not yet been
360+
// successfully actuated (applied or deleted), then the current status is
361+
// Unknown, because neither Kubernetes nor this ResourceGroup controller knows
362+
// what the desired state is.
363+
func UpdateStatusToReflectActuation(s v1alpha1.ResourceStatus) v1alpha1.Status {
364+
// When actuation is unknown, return the latest computed status.
365+
// This should only ever happen when upgrading from an old version.
366+
// It means the applier was last run with code that predates the addition
367+
// of the strategy, actuation, and reconcile fields.
368+
if s.Actuation == "" {
369+
return s.Status
359370
}
360371

361-
if s.Actuation != "" &&
362-
s.Actuation != v1alpha1.ActuationSucceeded {
363-
return v1alpha1.Unknown
372+
// When actuation has succeeded, we know the object spec is up to date with
373+
// the latest source, so the latest computed status should be correct.
374+
if s.Actuation == v1alpha1.ActuationSucceeded {
375+
return s.Status
364376
}
365377

366-
if s.Actuation == v1alpha1.ActuationSucceeded && s.Reconcile == v1alpha1.ReconcileSucceeded {
367-
return v1alpha1.Current
378+
// When the latest computed status is Terminating or NotFound, keep it,
379+
// even if actuation is Pending, Skipped, or Failed, because Terminating and
380+
// NotFound are independent of the desired state in the source of truth,
381+
// while InProgress, Current, and Failed depend on actuation having
382+
// successfully updated the spec on the cluster.
383+
if s.Status == v1alpha1.NotFound || s.Status == v1alpha1.Terminating {
384+
return s.Status
368385
}
369-
return s.Status
386+
387+
// When actuation is not successful (Pending, Skipped, Failed), that means
388+
// the object spec has not been updated to the desired spec from source yet,
389+
// so the computed status can be ignored and the real status is Unknown,
390+
// unless the latest computed status is NotFound or Terminating.
391+
return v1alpha1.Unknown
370392
}
371393

372394
// setResStatus updates a resource status struct using values within the cached status struct.

pkg/resourcegroup/controllers/resourcegroup/resourcegroup_controller_test.go

Lines changed: 62 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -474,65 +474,106 @@ func TestReconcileTimeout(t *testing.T) {
474474
}
475475
}
476476

477-
func TestActuationStatusToLegacy(t *testing.T) {
477+
func TestUpdateStatusToReflectActuation(t *testing.T) {
478478
tests := []struct {
479479
name string
480480
resStatus v1alpha1.ResourceStatus
481481
want v1alpha1.Status
482482
}{
483483
{
484-
"Status should equal current status when actuation is status is successful",
485-
v1alpha1.ResourceStatus{
484+
name: "Status should equal current status when actuation is status is successful",
485+
resStatus: v1alpha1.ResourceStatus{
486486
Status: v1alpha1.Current,
487487
Actuation: v1alpha1.ActuationSucceeded,
488488
},
489-
v1alpha1.Current,
489+
want: v1alpha1.Current,
490490
},
491491
{
492-
"Return status field when actuation is status is empty",
493-
v1alpha1.ResourceStatus{
492+
name: "Return status field when actuation is status is empty",
493+
resStatus: v1alpha1.ResourceStatus{
494494
Status: v1alpha1.InProgress,
495495
},
496-
v1alpha1.InProgress,
496+
want: v1alpha1.InProgress,
497497
},
498498
{
499-
"Return unknown when actuation is not successful",
500-
v1alpha1.ResourceStatus{
499+
name: "Return unknown when actuation is not successful",
500+
resStatus: v1alpha1.ResourceStatus{
501501
Actuation: v1alpha1.ActuationPending,
502502
},
503-
v1alpha1.Unknown,
503+
want: v1alpha1.Unknown,
504504
},
505505
{
506-
"Return not found when status is not found already",
507-
v1alpha1.ResourceStatus{
506+
name: "Return not found when status is not found already",
507+
resStatus: v1alpha1.ResourceStatus{
508508
Status: v1alpha1.NotFound,
509509
Actuation: v1alpha1.ActuationPending,
510510
},
511-
v1alpha1.NotFound,
511+
want: v1alpha1.NotFound,
512512
},
513513
{
514-
"Return not found when status is not found already - disregard actuation success",
515-
v1alpha1.ResourceStatus{
514+
name: "Return not found when status is not found already - disregard actuation success",
515+
resStatus: v1alpha1.ResourceStatus{
516516
Status: v1alpha1.NotFound,
517517
Actuation: v1alpha1.ActuationSucceeded,
518518
},
519-
v1alpha1.NotFound,
519+
want: v1alpha1.NotFound,
520520
},
521521
{
522-
"Return Current if both Actuation and Reconcile succeeded",
523-
v1alpha1.ResourceStatus{
524-
Status: v1alpha1.Unknown,
522+
name: "Return Terminating when Terminating even if apply and reconcile previously succeeded",
523+
resStatus: v1alpha1.ResourceStatus{
524+
Status: v1alpha1.Terminating,
525+
Strategy: v1alpha1.Apply,
525526
Actuation: v1alpha1.ActuationSucceeded,
526527
Reconcile: v1alpha1.ReconcileSucceeded,
527528
},
528-
v1alpha1.Current,
529+
want: v1alpha1.Terminating,
530+
},
531+
{
532+
name: "Return Terminating when Terminating even if delete and reconcile previously succeeded",
533+
resStatus: v1alpha1.ResourceStatus{
534+
Status: v1alpha1.Terminating,
535+
Strategy: v1alpha1.Delete,
536+
Actuation: v1alpha1.ActuationSucceeded,
537+
Reconcile: v1alpha1.ReconcileSucceeded,
538+
},
539+
want: v1alpha1.Terminating,
540+
},
541+
{
542+
name: "Return NotFound when NotFound even if apply and reconcile previously succeeded",
543+
resStatus: v1alpha1.ResourceStatus{
544+
Status: v1alpha1.NotFound,
545+
Strategy: v1alpha1.Apply,
546+
Actuation: v1alpha1.ActuationSucceeded,
547+
Reconcile: v1alpha1.ReconcileSucceeded,
548+
},
549+
want: v1alpha1.NotFound,
550+
},
551+
{
552+
name: "Return NotFound when NotFound when strategy is delete",
553+
resStatus: v1alpha1.ResourceStatus{
554+
Status: v1alpha1.NotFound,
555+
Strategy: v1alpha1.Delete,
556+
Actuation: v1alpha1.ActuationSucceeded,
557+
Reconcile: v1alpha1.ReconcileSucceeded,
558+
},
559+
want: v1alpha1.NotFound,
560+
},
561+
{
562+
name: "Return Terminating if status is Terminating and strategy is delete, even if reconcile previously succeeded",
563+
resStatus: v1alpha1.ResourceStatus{
564+
Status: v1alpha1.Terminating,
565+
Strategy: v1alpha1.Delete,
566+
Actuation: v1alpha1.ActuationSucceeded,
567+
Reconcile: v1alpha1.ReconcileSucceeded,
568+
},
569+
want: v1alpha1.Terminating,
529570
},
530571
}
531572
for _, tc := range tests {
532573
tc := tc
533574
t.Run(tc.name, func(t *testing.T) {
534575
t.Parallel()
535-
if got := ActuationStatusToLegacy(tc.resStatus); got != tc.want {
576+
if got := UpdateStatusToReflectActuation(tc.resStatus); got != tc.want {
536577
t.Errorf("ActuationStatusToLegacy() = %v, want %v", got, tc.want)
537578
}
538579
})

pkg/resourcegroup/controllers/root/root_controller.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -267,11 +267,11 @@ func (ResourceGroupPredicate) Update(e event.UpdateEvent) bool {
267267
return statusNeedsUpdate(rgNew.Status.ResourceStatuses)
268268
}
269269

270-
// statusNeedsUpdate checks each resource status to ensure the legacy status field
271-
// aligns with the new actuation/reconcile status fields.
270+
// statusNeedsUpdate checks each resource status to ensure the status field
271+
// reflects the actuation status.
272272
func statusNeedsUpdate(statuses []v1alpha1.ResourceStatus) bool {
273273
for _, s := range statuses {
274-
if resourcegroup.ActuationStatusToLegacy(s) != s.Status {
274+
if resourcegroup.UpdateStatusToReflectActuation(s) != s.Status {
275275
return true
276276
}
277277
}

0 commit comments

Comments
 (0)