From f27bb5491ed81e695220b209f4d7d83d554a8381 Mon Sep 17 00:00:00 2001 From: Ben Luddy Date: Wed, 23 Oct 2024 16:36:25 -0400 Subject: [PATCH] Wire test-only feature gate for CBOR serving. To mitigate the risk of introducing a new protocol, integration tests for CBOR will be written using a test-only feature gate instance that is not wired to runtime options. On alpha graduation, the test-only feature gate instance will be replaced by a normal feature gate in the existing apiserver feature gate instance. Kubernetes-commit: 0cad1a89b6721308746cc1a12f12de31a259a0d3 --- pkg/features/kube_features.go | 19 +++++++++++++++++++ pkg/server/config.go | 6 +++++- pkg/server/config_test.go | 22 ++++++++++++++++++++++ pkg/util/feature/feature_gate.go | 12 ++++++++++++ 4 files changed, 58 insertions(+), 1 deletion(-) diff --git a/pkg/features/kube_features.go b/pkg/features/kube_features.go index f4c6fb3a7..1c5d1cc6d 100644 --- a/pkg/features/kube_features.go +++ b/pkg/features/kube_features.go @@ -87,6 +87,15 @@ const ( // Allows authorization to use field and label selectors. AuthorizeWithSelectors featuregate.Feature = "AuthorizeWithSelectors" + // owner: @benluddy + // kep: https://kep.k8s.io/4222 + // + // Enables CBOR as a supported encoding for requests and responses, and as the + // preferred storage encoding for custom resources. + // + // This feature is currently PRE-ALPHA and MUST NOT be enabled outside of integration tests. + TestOnlyCBORServingAndStorage featuregate.Feature = "TestOnlyCBORServingAndStorage" + // owner: @serathius // Enables concurrent watch object decoding to avoid starving watch cache when conversion webhook is installed. ConcurrentWatchObjectDecode featuregate.Feature = "ConcurrentWatchObjectDecode" @@ -238,6 +247,7 @@ const ( func init() { runtime.Must(utilfeature.DefaultMutableFeatureGate.Add(defaultKubernetesFeatureGates)) runtime.Must(utilfeature.DefaultMutableFeatureGate.AddVersioned(defaultVersionedKubernetesFeatureGates)) + runtime.Must(utilfeature.TestOnlyMutableFeatureGate.AddVersioned(testOnlyVersionedKubernetesFeatureGates)) } // defaultVersionedKubernetesFeatureGates consists of all known Kubernetes-specific feature keys with VersionedSpecs. @@ -410,3 +420,12 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate // defaultKubernetesFeatureGates consists of legacy unversioned Kubernetes-specific feature keys. // Please do not add to this struct and use defaultVersionedKubernetesFeatureGates instead. var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{} + +// testOnlyVersionedKubernetesFeatureGates consists of features that require programmatic enablement +// for integration testing, but have not yet graduated to alpha in a release and must not be enabled +// by a runtime option. +var testOnlyVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate.VersionedSpecs{ + TestOnlyCBORServingAndStorage: { + {Version: version.MustParse("1.32"), Default: false, PreRelease: featuregate.Alpha}, + }, +} diff --git a/pkg/server/config.go b/pkg/server/config.go index e906f3a1a..7130bc3a0 100644 --- a/pkg/server/config.go +++ b/pkg/server/config.go @@ -742,7 +742,7 @@ func (c *RecommendedConfig) Complete() CompletedConfig { return c.Config.Complete(c.SharedInformerFactory) } -var allowedMediaTypes = []string{ +var defaultAllowedMediaTypes = []string{ runtime.ContentTypeJSON, runtime.ContentTypeYAML, runtime.ContentTypeProtobuf, @@ -755,6 +755,10 @@ func (c completedConfig) New(name string, delegationTarget DelegationTarget) (*G if c.Serializer == nil { return nil, fmt.Errorf("Genericapiserver.New() called with config.Serializer == nil") } + allowedMediaTypes := defaultAllowedMediaTypes + if utilfeature.TestOnlyFeatureGate.Enabled(genericfeatures.TestOnlyCBORServingAndStorage) { + allowedMediaTypes = append(allowedMediaTypes, runtime.ContentTypeCBOR) + } for _, info := range c.Serializer.SupportedMediaTypes() { var ok bool for _, mt := range allowedMediaTypes { diff --git a/pkg/server/config_test.go b/pkg/server/config_test.go index 75314e2ca..a1e4d6f2d 100644 --- a/pkg/server/config_test.go +++ b/pkg/server/config_test.go @@ -29,6 +29,7 @@ import ( "time" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/util/json" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/wait" @@ -40,12 +41,14 @@ import ( "k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/apiserver/pkg/features" "k8s.io/apiserver/pkg/server/healthz" utilfeature "k8s.io/apiserver/pkg/util/feature" utilversion "k8s.io/apiserver/pkg/util/version" "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/rest" + featuregatetesting "k8s.io/component-base/featuregate/testing" "k8s.io/component-base/tracing" "k8s.io/klog/v2/ktesting" netutils "k8s.io/utils/net" @@ -419,3 +422,22 @@ func TestNewErrorForbiddenSerializer(t *testing.T) { t.Errorf("unexpected error: %v", err) } } + +func TestNewFeatureGatedSerializer(t *testing.T) { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.TestOnlyFeatureGate, features.TestOnlyCBORServingAndStorage, true) + + config := NewConfig(serializer.NewCodecFactory(scheme, serializer.WithSerializer(func(creater runtime.ObjectCreater, typer runtime.ObjectTyper) runtime.SerializerInfo { + return runtime.SerializerInfo{ + MediaType: "application/cbor", + MediaTypeType: "application", + MediaTypeSubType: "cbor", + } + }))) + config.ExternalAddress = "192.168.10.4:443" + config.EffectiveVersion = utilversion.NewEffectiveVersion("") + config.LoopbackClientConfig = &rest.Config{} + + if _, err := config.Complete(nil).New("test", NewEmptyDelegate()); err != nil { + t.Errorf("unexpected error: %v", err) + } +} diff --git a/pkg/util/feature/feature_gate.go b/pkg/util/feature/feature_gate.go index 00a9e099b..7c061042a 100644 --- a/pkg/util/feature/feature_gate.go +++ b/pkg/util/feature/feature_gate.go @@ -31,3 +31,15 @@ var ( // Top-level commands/options setup that needs to modify this feature gate should use DefaultMutableFeatureGate. DefaultFeatureGate featuregate.FeatureGate = DefaultMutableFeatureGate ) + +var ( + // TestOnlyMutableFeatureGate is a mutable version of TestOnlyFeatureGate. Only top-level + // commands/options setup and the k8s.io/component-base/featuregate/testing package should + // make use of this. + TestOnlyMutableFeatureGate featuregate.MutableVersionedFeatureGate = featuregate.NewFeatureGate() + + // TestOnlyFeatureGate is a shared global FeatureGate for features that have not yet + // graduated to alpha and require programmatic feature enablement for pre-alpha integration + // testing without exposing the feature as a runtime option. + TestOnlyFeatureGate featuregate.FeatureGate = TestOnlyMutableFeatureGate +)