Skip to content

Commit 822e018

Browse files
committed
Fix aggregated API server readiness race causing sporadic 503s in envtest suites
Signed-off-by: Andreas Fritzler <andreas.fritzler@sap.com>
1 parent a0e22c5 commit 822e018

1 file changed

Lines changed: 110 additions & 0 deletions

File tree

utils/envtest/envtest.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
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,110 @@ func WaitUntilAPIServicesReady(ctx context.Context, ext *EnvironmentExtensions,
661667
return nil
662668
}
663669

670+
// serveReadinessPollInterval is the interval at which the aggregated API server
671+
// is probed for serving real resource round-trips.
672+
const serveReadinessPollInterval = 100 * time.Millisecond
673+
674+
// serveReadinessStableDuration is how long the aggregated API server must
675+
// continuously serve real resource round-trips before startup is considered
676+
// complete.
677+
//
678+
// The APIService Available condition and group discovery can report ready
679+
// while the aggregated server still returns transient 503 ServiceUnavailable
680+
// responses for a short window afterwards. Requiring sustained success absorbs
681+
// that window so tests observe a genuinely serving API server.
682+
const serveReadinessStableDuration = 500 * time.Millisecond
683+
684+
// waitUntilGroupVersionsServe waits until the aggregated API server actually
685+
// serves resource round-trips for each of the given group versions by issuing a
686+
// real List per group.
687+
//
688+
// Unlike the APIService Available condition and group discovery (which are
689+
// reported by / served from the kube-apiserver), a List is proxied to the
690+
// aggregated server, so it observes the transient 503 startup window and absorbs
691+
// it before the test suite proceeds.
692+
func waitUntilGroupVersionsServe(ctx context.Context, c client.Client, scheme *runtime.Scheme, gvs ...schema.GroupVersion) error {
693+
var lists []client.ObjectList
694+
for _, gv := range gvs {
695+
list, ok := newListForGroupVersion(scheme, gv)
696+
if !ok {
697+
continue
698+
}
699+
lists = append(lists, list)
700+
}
701+
if len(lists) == 0 {
702+
return nil
703+
}
704+
705+
requiredSuccesses := int(serveReadinessStableDuration / serveReadinessPollInterval)
706+
if requiredSuccesses < 1 {
707+
requiredSuccesses = 1
708+
}
709+
710+
successes := 0
711+
var lastErr error
712+
if err := wait.PollUntilContextCancel(ctx, serveReadinessPollInterval, true, func(ctx context.Context) (bool, error) {
713+
for _, list := range lists {
714+
if err := c.List(ctx, list); err != nil {
715+
if !isTransientServeError(err) {
716+
return false, fmt.Errorf("unexpected error probing aggregated api server: %w", err)
717+
}
718+
lastErr = err
719+
successes = 0
720+
return false, nil
721+
}
722+
}
723+
successes++
724+
return successes >= requiredSuccesses, nil
725+
}); err != nil {
726+
if lastErr != nil {
727+
return fmt.Errorf("%w (last error: %v)", err, lastErr)
728+
}
729+
return err
730+
}
731+
return nil
732+
}
733+
734+
// newListForGroupVersion returns a fresh list object for the first list kind
735+
// registered for the given group version (e.g. *VolumeList), constructed via the
736+
// scheme so the scheme stays the single source of truth for type instantiation.
737+
func newListForGroupVersion(scheme *runtime.Scheme, gv schema.GroupVersion) (client.ObjectList, bool) {
738+
kinds := make([]string, 0, len(scheme.KnownTypes(gv)))
739+
for kind := range scheme.KnownTypes(gv) {
740+
kinds = append(kinds, kind)
741+
}
742+
sort.Strings(kinds)
743+
for _, kind := range kinds {
744+
obj, err := scheme.New(gv.WithKind(kind))
745+
if err != nil {
746+
continue
747+
}
748+
if list, ok := obj.(client.ObjectList); ok {
749+
return list, true
750+
}
751+
}
752+
return nil, false
753+
}
754+
755+
// isTransientServeError reports whether an error returned while probing the
756+
// aggregated API server is a transient startup condition worth retrying.
757+
func isTransientServeError(err error) bool {
758+
if err == nil {
759+
return false
760+
}
761+
if apierrors.IsServiceUnavailable(err) ||
762+
apierrors.IsServerTimeout(err) ||
763+
apierrors.IsTimeout(err) ||
764+
apierrors.IsTooManyRequests(err) ||
765+
apierrors.IsInternalError(err) {
766+
return true
767+
}
768+
// Transport-level errors (connection refused, EOF, reset) while the
769+
// aggregated server is still coming up.
770+
var urlErr *url.Error
771+
return errors.As(err, &urlErr)
772+
}
773+
664774
func WaitUntilAPIServicesReadyWithTimeout(timeout time.Duration, ext *EnvironmentExtensions, cfg *rest.Config, c client.Client, scheme *runtime.Scheme) error {
665775
ctx, cancel := context.WithTimeout(context.Background(), timeout)
666776
defer cancel()

0 commit comments

Comments
 (0)