diff --git a/dynamic/scheme.go b/dynamic/scheme.go index 869002284..dbee05312 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 51d96e692..b47671405 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 afb67f509..19056df14 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 0c972a46f..9a6a73645 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. diff --git a/go.mod b/go.mod index cd7adda74..90ed64355 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,7 @@ require ( google.golang.org/protobuf v1.34.2 gopkg.in/evanphx/json-patch.v4 v4.12.0 k8s.io/api v0.0.0-20241024015157-dac1d89c7f69 - k8s.io/apimachinery v0.0.0-20241018042225-cfee47580787 + k8s.io/apimachinery v0.0.0-20241025000453-124c262107b0 k8s.io/klog/v2 v2.130.1 k8s.io/kube-openapi v0.0.0-20240827152857-f7e401e7b4c2 k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 diff --git a/go.sum b/go.sum index 27946f4b2..678f4f64c 100644 --- a/go.sum +++ b/go.sum @@ -157,8 +157,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= k8s.io/api v0.0.0-20241024015157-dac1d89c7f69 h1:vzBFFi/vVmi3J3HyNc02Bijse3LusTUDzvNXYpBdqHM= k8s.io/api v0.0.0-20241024015157-dac1d89c7f69/go.mod h1:OIAurRK8KzzpBNXtbVYeGeeoww3j5JLZFLfrV8ZAy0Y= -k8s.io/apimachinery v0.0.0-20241018042225-cfee47580787 h1:cxDsuM/daoEa1+BWlatVGAHIkKOHpZ+2BCdP25Qmw+E= -k8s.io/apimachinery v0.0.0-20241018042225-cfee47580787/go.mod h1:y/FzDt/GaPgPceo5rJcCtD4qW5l8SwtbzESSMGEY6P8= +k8s.io/apimachinery v0.0.0-20241025000453-124c262107b0 h1:6dJVqURMs0HNPdaaIJ0UqpwB39zuTdaMxYMTNKXiAis= +k8s.io/apimachinery v0.0.0-20241025000453-124c262107b0/go.mod h1:y/FzDt/GaPgPceo5rJcCtD4qW5l8SwtbzESSMGEY6P8= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20240827152857-f7e401e7b4c2 h1:GKE9U8BH16uynoxQii0auTjmmmuZ3O0LFMN6S0lPPhI=