From 538480c31c3e3b5a808476c9af26952d0dc47698 Mon Sep 17 00:00:00 2001 From: raphael Date: Thu, 16 Jul 2026 22:19:45 +0100 Subject: [PATCH 1/2] fix: TestStatusWaitForDelete flakes due to WaitForDelete informer initialization race Signed-off-by: raphael --- pkg/kube/statuswait.go | 46 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/pkg/kube/statuswait.go b/pkg/kube/statuswait.go index 12c3e95a6..c34599884 100644 --- a/pkg/kube/statuswait.go +++ b/pkg/kube/statuswait.go @@ -33,7 +33,9 @@ import ( "github.com/fluxcd/cli-utils/pkg/kstatus/watcher" "github.com/fluxcd/cli-utils/pkg/object" appsv1 "k8s.io/api/apps/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/client-go/dynamic" watchtools "k8s.io/client-go/tools/watch" @@ -136,16 +138,55 @@ func (w *statusWaiter) WaitForDelete(resourceList ResourceList, timeout time.Dur } func (w *statusWaiter) waitForDelete(ctx context.Context, resourceList ResourceList, sw watcher.StatusWatcher) error { + if err := ctx.Err(); err != nil { + return err + } cancelCtx, cancel := context.WithCancel(ctx) defer cancel() resources := []object.ObjMetadata{} for _, resource := range resourceList { + gvk := resource.Object.GetObjectKind().GroupVersionKind() + mapping, err := w.restMapper.RESTMapping(gvk.GroupKind(), gvk.Version) + if err != nil { + return err + } + var name, namespace string + if u, ok := resource.Object.(*unstructured.Unstructured); ok { + name = u.GetName() + namespace = u.GetNamespace() + } else { + accessor, err := meta.Accessor(resource.Object) + if err != nil { + return err + } + name = accessor.GetName() + namespace = accessor.GetNamespace() + } + var resourceClient dynamic.ResourceInterface + if namespace != "" { + resourceClient = w.client.Resource(mapping.Resource).Namespace(namespace) + } else { + resourceClient = w.client.Resource(mapping.Resource) + } + _, err = resourceClient.Get(ctx, name, metav1.GetOptions{}) + if err != nil { + if apierrors.IsNotFound(err) { + continue + } + return err + } + obj, err := object.RuntimeToObjMeta(resource.Object) if err != nil { return err } resources = append(resources, obj) } + + if len(resources) == 0 { + return nil + } + eventCh := sw.Watch(cancelCtx, resources, watcher.Options{ RESTScopeStrategy: watcher.RESTScopeNamespace, }) @@ -241,11 +282,6 @@ func statusObserver(cancel context.CancelFunc, desired status.Status, logger *sl if rs == nil { continue } - // If a resource is already deleted before waiting has started, it will show as unknown. - // This check ensures we don't wait forever for a resource that is already deleted. - if rs.Status == status.UnknownStatus && desired == status.NotFoundStatus { - continue - } // Failed is a terminal state. This check ensures we don't wait forever for a resource // that has already failed, as intervention is required to resolve the failure. if rs.Status == status.FailedStatus && desired == status.CurrentStatus { From 998ecee347bd63020143053122692ab0d9e5747a Mon Sep 17 00:00:00 2001 From: raphael Date: Fri, 17 Jul 2026 00:22:04 +0100 Subject: [PATCH 2/2] refactor: use resource.Info metadata for delete pre-check Signed-off-by: raphael --- pkg/kube/statuswait.go | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/pkg/kube/statuswait.go b/pkg/kube/statuswait.go index c34599884..b4f179972 100644 --- a/pkg/kube/statuswait.go +++ b/pkg/kube/statuswait.go @@ -145,30 +145,23 @@ func (w *statusWaiter) waitForDelete(ctx context.Context, resourceList ResourceL defer cancel() resources := []object.ObjMetadata{} for _, resource := range resourceList { - gvk := resource.Object.GetObjectKind().GroupVersionKind() - mapping, err := w.restMapper.RESTMapping(gvk.GroupKind(), gvk.Version) - if err != nil { - return err - } - var name, namespace string - if u, ok := resource.Object.(*unstructured.Unstructured); ok { - name = u.GetName() - namespace = u.GetNamespace() - } else { - accessor, err := meta.Accessor(resource.Object) + mapping := resource.Mapping + if mapping == nil { + gvk := resource.Object.GetObjectKind().GroupVersionKind() + var err error + mapping, err = w.restMapper.RESTMapping(gvk.GroupKind(), gvk.Version) if err != nil { return err } - name = accessor.GetName() - namespace = accessor.GetNamespace() } + var resourceClient dynamic.ResourceInterface - if namespace != "" { - resourceClient = w.client.Resource(mapping.Resource).Namespace(namespace) + if resource.Namespace != "" { + resourceClient = w.client.Resource(mapping.Resource).Namespace(resource.Namespace) } else { resourceClient = w.client.Resource(mapping.Resource) } - _, err = resourceClient.Get(ctx, name, metav1.GetOptions{}) + _, err := resourceClient.Get(ctx, resource.Name, metav1.GetOptions{}) if err != nil { if apierrors.IsNotFound(err) { continue