fix(kube): don't hang waiting on resources whose status cannot be computed

Argo Rollout stores status.observedGeneration as a string, unlike the
int64 convention kstatus expects. Helm v4's status wait runs
status.Compute for CRDs, which errors on the string value; the error is
attached to the ResourceStatus as "Unknown" instead of being surfaced,
so the Rollout stays Unknown forever and --wait times out with
"waiting for resource ... kind=Rollout ... actualStatus=Unknown".

Treat a resource whose status could not be computed (Unknown with an
attached error) as ready when waiting for Current, matching Helm 3's
behavior of considering kinds Helm cannot evaluate as ready. Also prime
the RESTMapper before starting the status watcher's informers so a CRD
that is still being established does not cause an informer to be
permanently abandoned.

Signed-off-by: MrJack <36191829+biagiopietro@users.noreply.github.com>
pull/32616/head
MrJack 1 week ago
parent 99a9ed081c
commit 32f33419aa

@ -221,7 +221,7 @@ func (w *statusWaiter) wait(ctx context.Context, resourceList ResourceList, sw w
errs := []error{} errs := []error{}
for _, id := range resources { for _, id := range resources {
rs := statusCollector.ResourceStatuses[id] rs := statusCollector.ResourceStatuses[id]
if rs.Status == status.CurrentStatus { if resourceStatusSatisfied(rs, status.CurrentStatus) {
continue continue
} }
errs = append(errs, fmt.Errorf("resource %s/%s/%s not ready. status: %s, message: %s", 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 { if rs.Status == status.FailedStatus && desired == status.CurrentStatus {
continue continue
} }
rss = append(rss, rs) // A resource whose status could not be computed (Unknown with an attached
if rs.Status != desired { // error) is treated as having reached the desired state. Helm 3 considered
nonDesiredResources = append(nonDesiredResources, rs) // 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 { 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 { type hookOnlyWaiter struct {
sw *statusWaiter sw *statusWaiter
} }

@ -39,10 +39,11 @@ metadata:
namespace: default namespace: default
generation: 1 generation: 1
status: status:
observedGeneration: 1 observedGeneration: "1"
conditions: conditions:
- type: Promoted - type: Promoted
status: "True" status: "True"
lastTransitionTime: "2026-01-01T00:00:00Z"
` `
var rolloutGVR = schema.GroupVersionResource{ 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")
}
}

Loading…
Cancel
Save