diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index 5b551ec4c..6cade0059 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -31,11 +31,13 @@ import ( mutatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating" validatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/validating" genericopenapi "k8s.io/apiserver/pkg/endpoints/openapi" + genericfeatures "k8s.io/apiserver/pkg/features" apirest "k8s.io/apiserver/pkg/registry/rest" genericapiserver "k8s.io/apiserver/pkg/server" "k8s.io/apiserver/pkg/server/dynamiccertificates" genericoptions "k8s.io/apiserver/pkg/server/options" apiservercompatibility "k8s.io/apiserver/pkg/util/compatibility" + utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/client-go/discovery" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" @@ -146,6 +148,10 @@ type NewAPIServerOpts struct { } func NewAPIServer(clientConfig *rest.Config, coreClient kubernetes.Interface, kcClient kcclient.Interface, opts NewAPIServerOpts) (*APIServer, error) { //nolint + if err := disableWatchListFeatureGate(); err != nil { + return nil, err + } + aggClient, err := aggregatorclient.NewForConfig(clientConfig) if err != nil { return nil, fmt.Errorf("building aggregation client: %v", err) @@ -201,6 +207,29 @@ func NewAPIServer(clientConfig *rest.Config, coreClient kubernetes.Interface, kc return &APIServer{server, make(chan struct{}), aggClient, opts.Logger}, nil } +// disableWatchListFeatureGate turns off the WatchList feature for the packaging +// aggregated API server. This server is a thin proxy whose storage is the host +// cluster's InternalPackage(Metadata) CRDs, and its custom REST watch does not +// synthesize the WatchList "initial-events-end" bookmark. +// +// With WatchList enabled, the embedded apiserver's SetListOptionsDefaults injects +// sendInitialEvents + resourceVersionMatch=NotOlderThan into watch requests with +// resourceVersion=0. Those defaulted options are then rejected with 422 by host +// clusters that ship the WatchList feature gate disabled (e.g. Kubernetes 1.33, +// which turned it off by default), breaking a plain "watch&resourceVersion=0". +// +// Disabling it here means such requests are served as a plain watch, and +// WatchList-capable clients (e.g. client-go informers) receive a 422 for the +// streaming request and transparently fall back to list+watch. +func disableWatchListFeatureGate() error { + if err := utilfeature.DefaultMutableFeatureGate.SetFromMap(map[string]bool{ + string(genericfeatures.WatchList): false, + }); err != nil { + return fmt.Errorf("disabling WatchList feature gate: %v", err) + } + return nil +} + // Run spawns a go routine that exits when apiserver is stopped. func (as *APIServer) Run() error { go func() { diff --git a/pkg/apiserver/apiserver_test.go b/pkg/apiserver/apiserver_test.go index 4384a0d41..33d8cba5f 100644 --- a/pkg/apiserver/apiserver_test.go +++ b/pkg/apiserver/apiserver_test.go @@ -11,12 +11,25 @@ import ( "github.com/go-logr/logr" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + genericfeatures "k8s.io/apiserver/pkg/features" "k8s.io/apiserver/pkg/server/dynamiccertificates" + utilfeature "k8s.io/apiserver/pkg/util/feature" clienttesting "k8s.io/client-go/testing" apiregv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1" fakeaggregator "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/fake" ) +// The packaging apiserver must run with the WatchList feature disabled so that +// watch&resourceVersion=0 requests are not defaulted with sendInitialEvents, +// which host clusters that ship WatchList off (e.g. K8s 1.33) reject with 422. +func Test_disableWatchListFeatureGate(t *testing.T) { + err := disableWatchListFeatureGate() + require.NoError(t, err) + + require.False(t, utilfeature.DefaultFeatureGate.Enabled(genericfeatures.WatchList), + "WatchList feature gate must be disabled for the packaging apiserver") +} + // fakeCAProvider implements dynamiccertificates.CAContentProvider type fakeCAProvider struct { bundle []byte