diff --git a/pkg/kube/statuswait.go b/pkg/kube/statuswait.go index 5a865201b..6097521c1 100644 --- a/pkg/kube/statuswait.go +++ b/pkg/kube/statuswait.go @@ -221,7 +221,7 @@ func (w *statusWaiter) wait(ctx context.Context, resourceList ResourceList, sw w errs := []error{} for _, id := range resources { rs := statusCollector.ResourceStatuses[id] - if rs.Status == status.CurrentStatus { + if resourceStatusSatisfied(rs, status.CurrentStatus) { continue } errs = append(errs, fmt.Errorf("resource %s/%s/%s not ready. status: %s, message: %s", @@ -367,10 +367,16 @@ func statusObserver(cancel context.CancelFunc, desired status.Status, logger *sl if rs.Status == status.FailedStatus && desired == status.CurrentStatus { continue } - rss = append(rss, rs) - if rs.Status != desired { - nonDesiredResources = append(nonDesiredResources, rs) + // A resource whose status could not be computed (Unknown with an attached + // error) is treated as having reached the desired state. Helm 3 considered + // all kinds it could not evaluate (e.g. CRDs such as Argo Rollout whose + // status fields deviate from Kubernetes conventions) as ready, so don't + // block the wait on them. + if resourceStatusSatisfied(rs, desired) { + continue } + rss = append(rss, rs) + nonDesiredResources = append(nonDesiredResources, rs) } if aggregator.AggregateStatus(rss, desired) == desired { @@ -390,6 +396,18 @@ func statusObserver(cancel context.CancelFunc, desired status.Status, logger *sl } } +// resourceStatusSatisfied reports whether the given resource status satisfies the +// desired status. A resource whose status could not be computed (Unknown with an +// attached error, e.g. a CRD such as an Argo Rollout whose status fields do not +// follow Kubernetes conventions) is treated as satisfied for the Current status, +// matching Helm 3's behavior of considering such kinds ready. +func resourceStatusSatisfied(rs *event.ResourceStatus, desired status.Status) bool { + if rs.Status == desired { + return true + } + return desired == status.CurrentStatus && rs.Status == status.UnknownStatus && rs.Error != nil +} + type hookOnlyWaiter struct { sw *statusWaiter } diff --git a/pkg/kube/statuswait_crd_test.go b/pkg/kube/statuswait_crd_test.go index 3a8525128..3732a2b9e 100644 --- a/pkg/kube/statuswait_crd_test.go +++ b/pkg/kube/statuswait_crd_test.go @@ -39,10 +39,11 @@ metadata: namespace: default generation: 1 status: - observedGeneration: 1 + observedGeneration: "1" conditions: - type: Promoted status: "True" + lastTransitionTime: "2026-01-01T00:00:00Z" ` var rolloutGVR = schema.GroupVersionResource{ @@ -165,3 +166,30 @@ func TestStatusWaitCustomResource(t *testing.T) { }) } } + +// TestStatusWaitCustomResourceUncomputableStatus ensures that waiting on a custom +// resource whose status cannot be computed by the kstatus library (such as an +// Argo Rollout, which stores status.observedGeneration as a string) does not +// hang. Helm 3 considered kinds it could not evaluate as ready, so the wait must +// succeed instead of leaving the resource in the Unknown status until the timeout. +func TestStatusWaitCustomResourceUncomputableStatus(t *testing.T) { + t.Parallel() + mapper := newDelayedMapper(true) + sw, resourceList := newRolloutStatusWaiter(t, mapper) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + done := make(chan error, 1) + go func() { + done <- sw.Wait(resourceList, 5*time.Second) + }() + + select { + case err := <-done: + if err != nil { + t.Fatalf("Wait failed: %v", err) + } + case <-ctx.Done(): + t.Fatal("Wait hung: custom resource remained in the Unknown status because its status could not be computed") + } +}