77 "context"
88 "errors"
99 "fmt"
10+ "net/url"
1011 "os"
1112 "path/filepath"
1213 "reflect"
@@ -18,6 +19,7 @@ import (
1819 "github.com/ironcore-dev/ironcore/utils/envtest/internal/testing/addr"
1920 "github.com/ironcore-dev/ironcore/utils/envtest/internal/testing/certs"
2021 corev1 "k8s.io/api/core/v1"
22+ apierrors "k8s.io/apimachinery/pkg/api/errors"
2123 "k8s.io/apimachinery/pkg/api/meta"
2224 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2325 "k8s.io/apimachinery/pkg/runtime"
@@ -591,6 +593,10 @@ func WaitUntilGroupVersionsDiscoverable(ctx context.Context, cfg *rest.Config, c
591593 return fmt .Errorf ("error waiting for GVKs to be discoverable: %w" , err )
592594 }
593595
596+ if err := waitUntilGroupVersionsServe (ctx , c , scheme , gvs ... ); err != nil {
597+ return fmt .Errorf ("error waiting for aggregated api server to serve requests: %w" , err )
598+ }
599+
594600 log .Info ("All group versions and REST mappings are discoverable" , "gvs" , gvs )
595601 return nil
596602}
@@ -661,6 +667,91 @@ func WaitUntilAPIServicesReady(ctx context.Context, ext *EnvironmentExtensions,
661667 return nil
662668}
663669
670+ // poll interval for the aggregated API server serve probe.
671+ const serveReadinessPollInterval = 100 * time .Millisecond
672+
673+ // sustained success window to absorb the 503 startup race after APIService goes Available.
674+ const serveReadinessStableDuration = 500 * time .Millisecond
675+
676+ // waitUntilGroupVersionsServe polls a real List per GV until it succeeds for
677+ // serveReadinessStableDuration, absorbing the aggregated server's 503 window
678+ // after APIService reports Available.
679+ func waitUntilGroupVersionsServe (ctx context.Context , c client.Client , scheme * runtime.Scheme , gvs ... schema.GroupVersion ) error {
680+ var listGVKs []schema.GroupVersionKind
681+ for _ , gv := range gvs {
682+ if listGVK , ok := firstListGVKForGroupVersion (scheme , gv ); ok {
683+ listGVKs = append (listGVKs , listGVK )
684+ }
685+ }
686+ if len (listGVKs ) == 0 {
687+ return nil
688+ }
689+
690+ requiredSuccesses := int (serveReadinessStableDuration / serveReadinessPollInterval )
691+ successes := 0
692+ var lastErr error
693+ if err := wait .PollUntilContextCancel (ctx , serveReadinessPollInterval , true , func (ctx context.Context ) (bool , error ) {
694+ for _ , listGVK := range listGVKs {
695+ list , err := scheme .New (listGVK )
696+ if err != nil {
697+ return false , fmt .Errorf ("unexpected error constructing list for %s: %w" , listGVK , err )
698+ }
699+ if err := c .List (ctx , list .(client.ObjectList )); err != nil {
700+ if ! isTransientServeError (err ) {
701+ return false , fmt .Errorf ("unexpected error probing aggregated api server: %w" , err )
702+ }
703+ lastErr = err
704+ successes = 0
705+ return false , nil
706+ }
707+ }
708+ successes ++
709+ return successes >= requiredSuccesses , nil
710+ }); err != nil {
711+ if lastErr != nil {
712+ return fmt .Errorf ("%w (last error: %v)" , err , lastErr )
713+ }
714+ return err
715+ }
716+ return nil
717+ }
718+
719+ // firstListGVKForGroupVersion returns the first list-kind GVK registered for gv, sorted for determinism.
720+ func firstListGVKForGroupVersion (scheme * runtime.Scheme , gv schema.GroupVersion ) (schema.GroupVersionKind , bool ) {
721+ kinds := make ([]string , 0 , len (scheme .KnownTypes (gv )))
722+ for kind := range scheme .KnownTypes (gv ) {
723+ kinds = append (kinds , kind )
724+ }
725+ sort .Strings (kinds )
726+ for _ , kind := range kinds {
727+ obj , err := scheme .New (gv .WithKind (kind ))
728+ if err != nil {
729+ continue
730+ }
731+ if _ , ok := obj .(client.ObjectList ); ok {
732+ return gv .WithKind (kind ), true
733+ }
734+ }
735+ return schema.GroupVersionKind {}, false
736+ }
737+
738+ // isTransientServeError reports whether err is a transient startup condition worth retrying.
739+ func isTransientServeError (err error ) bool {
740+ if err == nil {
741+ return false
742+ }
743+ switch {
744+ case apierrors .IsServiceUnavailable (err ),
745+ apierrors .IsServerTimeout (err ),
746+ apierrors .IsTimeout (err ),
747+ apierrors .IsTooManyRequests (err ),
748+ apierrors .IsInternalError (err ):
749+ return true
750+ }
751+ var urlErr * url.Error
752+ return errors .As (err , & urlErr )
753+ }
754+
664755func WaitUntilAPIServicesReadyWithTimeout (timeout time.Duration , ext * EnvironmentExtensions , cfg * rest.Config , c client.Client , scheme * runtime.Scheme ) error {
665756 ctx , cancel := context .WithTimeout (context .Background (), timeout )
666757 defer cancel ()
0 commit comments