From 1cca19dc02ae16fbf2ba870b1c70f82b7875d38e Mon Sep 17 00:00:00 2001 From: Ben Luddy Date: Wed, 23 Oct 2024 16:36:37 -0400 Subject: [PATCH] Add test-only client feature gates for CBOR. As with the apiserver feature gate for CBOR as a serving and storage encoding, the client feature gates for CBOR are being initially added through a test-only feature gate instance that is not wired to environment variables or to command-line flags and is intended only to be enabled programmatically from integration tests. The test-only instance will be removed as part of alpha graduation and replaced by conventional client feature gating. Kubernetes-commit: ea13190d8bd3a4bb3e82055b529aa7599ae5c6e1 --- dynamic/scheme.go | 30 +++++++++++++++++++++----- dynamic/simple.go | 13 +++++++++-- features/features.go | 44 +++++++++++++++++++++++++++++++++++++- features/known_features.go | 21 ++++++++++++++++++ 4 files changed, 100 insertions(+), 8 deletions(-) diff --git a/dynamic/scheme.go b/dynamic/scheme.go index 869002284d..dbee05312e 100644 --- a/dynamic/scheme.go +++ b/dynamic/scheme.go @@ -21,7 +21,9 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer/cbor" "k8s.io/apimachinery/pkg/runtime/serializer/json" + "k8s.io/client-go/features" ) var basicScheme = runtime.NewScheme() @@ -35,11 +37,8 @@ func init() { metav1.AddToGroupVersion(parameterScheme, versionV1) } -// basicNegotiatedSerializer is used to handle discovery and error handling serialization -type basicNegotiatedSerializer struct{} - -func (s basicNegotiatedSerializer) SupportedMediaTypes() []runtime.SerializerInfo { - return []runtime.SerializerInfo{ +func newBasicNegotiatedSerializer() basicNegotiatedSerializer { + supportedMediaTypes := []runtime.SerializerInfo{ { MediaType: "application/json", MediaTypeType: "application", @@ -54,6 +53,27 @@ func (s basicNegotiatedSerializer) SupportedMediaTypes() []runtime.SerializerInf }, }, } + if features.TestOnlyFeatureGates.Enabled(features.TestOnlyClientAllowsCBOR) { + supportedMediaTypes = append(supportedMediaTypes, runtime.SerializerInfo{ + MediaType: "application/cbor", + MediaTypeType: "application", + MediaTypeSubType: "cbor", + Serializer: cbor.NewSerializer(unstructuredCreater{basicScheme}, unstructuredTyper{basicScheme}), + StreamSerializer: &runtime.StreamSerializerInfo{ + Serializer: cbor.NewSerializer(basicScheme, basicScheme, cbor.Transcode(false)), + Framer: cbor.NewFramer(), + }, + }) + } + return basicNegotiatedSerializer{supportedMediaTypes: supportedMediaTypes} +} + +type basicNegotiatedSerializer struct { + supportedMediaTypes []runtime.SerializerInfo +} + +func (s basicNegotiatedSerializer) SupportedMediaTypes() []runtime.SerializerInfo { + return s.supportedMediaTypes } func (s basicNegotiatedSerializer) EncoderForVersion(encoder runtime.Encoder, gv runtime.GroupVersioner) runtime.Encoder { diff --git a/dynamic/simple.go b/dynamic/simple.go index 51d96e692f..b476714053 100644 --- a/dynamic/simple.go +++ b/dynamic/simple.go @@ -29,6 +29,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/features" "k8s.io/client-go/rest" "k8s.io/client-go/util/consistencydetector" "k8s.io/client-go/util/watchlist" @@ -45,9 +46,17 @@ var _ Interface = &DynamicClient{} // appropriate dynamic client defaults set. func ConfigFor(inConfig *rest.Config) *rest.Config { config := rest.CopyConfig(inConfig) - config.AcceptContentTypes = "application/json" + config.ContentType = "application/json" - config.NegotiatedSerializer = basicNegotiatedSerializer{} // this gets used for discovery and error handling types + config.AcceptContentTypes = "application/json" + if features.TestOnlyFeatureGates.Enabled(features.TestOnlyClientAllowsCBOR) { + config.AcceptContentTypes = "application/json;q=0.9,application/cbor;q=1" + if features.TestOnlyFeatureGates.Enabled(features.TestOnlyClientPrefersCBOR) { + config.ContentType = "application/cbor" + } + } + + config.NegotiatedSerializer = newBasicNegotiatedSerializer() if config.UserAgent == "" { config.UserAgent = rest.DefaultKubernetesUserAgent() } diff --git a/features/features.go b/features/features.go index afb67f509e..19056df147 100644 --- a/features/features.go +++ b/features/features.go @@ -18,9 +18,11 @@ package features import ( "errors" + "fmt" + "sync" + "sync/atomic" utilruntime "k8s.io/apimachinery/pkg/util/runtime" - "sync/atomic" ) // NOTE: types Feature, FeatureSpec, prerelease (and its values) @@ -141,3 +143,43 @@ var ( // should use AddFeaturesToExistingFeatureGates followed by ReplaceFeatureGates. featureGates = &atomic.Value{} ) + +// TestOnlyFeatureGates is a distinct registry of pre-alpha client features that must not be +// included in runtime wiring to command-line flags or environment variables. It exists as a risk +// mitigation to allow only programmatic enablement of CBOR serialization for integration testing +// purposes. +// +// TODO: Once all required integration test coverage is complete, this will be deleted and the +// test-only feature gates will be replaced by normal feature gates. +var TestOnlyFeatureGates = &testOnlyFeatureGates{ + features: map[Feature]bool{ + TestOnlyClientAllowsCBOR: false, + TestOnlyClientPrefersCBOR: false, + }, +} + +type testOnlyFeatureGates struct { + lock sync.RWMutex + features map[Feature]bool +} + +func (t *testOnlyFeatureGates) Enabled(feature Feature) bool { + t.lock.RLock() + defer t.lock.RUnlock() + + enabled, ok := t.features[feature] + if !ok { + panic(fmt.Sprintf("test-only feature %q not recognized", feature)) + } + return enabled +} + +func (t *testOnlyFeatureGates) Set(feature Feature, enabled bool) error { + t.lock.Lock() + defer t.lock.Unlock() + if _, ok := t.features[feature]; !ok { + return fmt.Errorf("test-only feature %q not recognized", feature) + } + t.features[feature] = enabled + return nil +} diff --git a/features/known_features.go b/features/known_features.go index 0c972a46fd..9a6a736457 100644 --- a/features/known_features.go +++ b/features/known_features.go @@ -41,6 +41,27 @@ const ( // owner: @nilekhc // alpha: v1.30 InformerResourceVersion Feature = "InformerResourceVersion" + + // owner: @benluddy + // kep: https://kep.k8s.io/4222 + // + // If disabled, clients configured to accept "application/cbor" will instead accept + // "application/json" with the same relative preference, and clients configured to write + // "application/cbor" or "application/apply-patch+cbor" will instead write + // "application/json" or "application/apply-patch+yaml", respectively. + // + // This feature is currently PRE-ALPHA and MUST NOT be enabled outside of integration tests. + TestOnlyClientAllowsCBOR Feature = "TestOnlyClientAllowsCBOR" + + // owner: @benluddy + // kep: https://kep.k8s.io/4222 + // + // If enabled AND TestOnlyClientAllowsCBOR is also enabled, the default request content type + // (if not explicitly configured) and the dynamic client's request content type both become + // "application/cbor". + // + // This feature is currently PRE-ALPHA and MUST NOT be enabled outside of integration tests. + TestOnlyClientPrefersCBOR Feature = "TestOnlyClientPrefersCBOR" ) // defaultKubernetesFeatureGates consists of all known Kubernetes-specific feature keys.