From 995135973d67313d65b4ba5df4cc2dad81428675 Mon Sep 17 00:00:00 2001 From: Ben Luddy Date: Fri, 13 Oct 2023 14:16:52 -0400 Subject: [PATCH] Inject feature gate instance into client-go for kube components. In order to avoid a dependency cycle between component-base and client-go, client-go maintains parallel definitions of component-base's feature types and constants. Passing kube's default feature gate instance to client-go requires an adapter. --- pkg/features/client_adapter.go | 69 ++++++++++++++++++++ pkg/features/client_adapter_test.go | 99 +++++++++++++++++++++++++++++ pkg/features/kube_features.go | 10 +++ vendor/modules.txt | 1 + 4 files changed, 179 insertions(+) create mode 100644 pkg/features/client_adapter.go create mode 100644 pkg/features/client_adapter_test.go diff --git a/pkg/features/client_adapter.go b/pkg/features/client_adapter.go new file mode 100644 index 00000000000..de03d78ef2b --- /dev/null +++ b/pkg/features/client_adapter.go @@ -0,0 +1,69 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package features + +import ( + "fmt" + + clientfeatures "k8s.io/client-go/features" + "k8s.io/component-base/featuregate" +) + +// clientAdapter adapts a k8s.io/component-base/featuregate.MutableFeatureGate to client-go's +// feature Gate and Registry interfaces. The component-base types Feature, FeatureSpec, and +// prerelease, and the component-base prerelease constants, are duplicated by parallel types and +// constants in client-go. The parallel types exist to allow the feature gate mechanism to be used +// for client-go features without introducing a circular dependency between component-base and +// client-go. +type clientAdapter struct { + mfg featuregate.MutableFeatureGate +} + +var _ clientfeatures.Gates = &clientAdapter{} + +func (a *clientAdapter) Enabled(name clientfeatures.Feature) bool { + return a.mfg.Enabled(featuregate.Feature(name)) +} + +var _ clientfeatures.Registry = &clientAdapter{} + +func (a *clientAdapter) Add(in map[clientfeatures.Feature]clientfeatures.FeatureSpec) error { + out := map[featuregate.Feature]featuregate.FeatureSpec{} + for name, spec := range in { + converted := featuregate.FeatureSpec{ + Default: spec.Default, + LockToDefault: spec.LockToDefault, + } + switch spec.PreRelease { + case clientfeatures.Alpha: + converted.PreRelease = featuregate.Alpha + case clientfeatures.Beta: + converted.PreRelease = featuregate.Beta + case clientfeatures.GA: + converted.PreRelease = featuregate.GA + case clientfeatures.Deprecated: + converted.PreRelease = featuregate.Deprecated + default: + // The default case implies programmer error. The same set of prerelease + // constants must exist in both component-base and client-go, and each one + // must have a case here. + panic(fmt.Sprintf("unrecognized prerelease %q of feature %q", spec.PreRelease, name)) + } + out[featuregate.Feature(name)] = converted + } + return a.mfg.Add(out) +} diff --git a/pkg/features/client_adapter_test.go b/pkg/features/client_adapter_test.go new file mode 100644 index 00000000000..13ec29094ec --- /dev/null +++ b/pkg/features/client_adapter_test.go @@ -0,0 +1,99 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package features + +import ( + "testing" + + clientfeatures "k8s.io/client-go/features" + "k8s.io/component-base/featuregate" +) + +func TestClientAdapterEnabled(t *testing.T) { + fg := featuregate.NewFeatureGate() + if err := fg.Add(map[featuregate.Feature]featuregate.FeatureSpec{ + "Foo": {Default: true}, + }); err != nil { + t.Fatal(err) + } + + a := &clientAdapter{fg} + if !a.Enabled("Foo") { + t.Error("expected Enabled(\"Foo\") to return true") + } + var r interface{} + func() { + defer func() { + r = recover() + }() + a.Enabled("Bar") + }() + if r == nil { + t.Error("expected Enabled(\"Bar\") to panic due to unknown feature name") + } +} + +func TestClientAdapterAdd(t *testing.T) { + fg := featuregate.NewFeatureGate() + a := &clientAdapter{fg} + defaults := fg.GetAll() + if err := a.Add(map[clientfeatures.Feature]clientfeatures.FeatureSpec{ + "FeatureAlpha": {PreRelease: clientfeatures.Alpha, Default: true}, + "FeatureBeta": {PreRelease: clientfeatures.Beta, Default: false}, + "FeatureGA": {PreRelease: clientfeatures.GA, Default: true, LockToDefault: true}, + "FeatureDeprecated": {PreRelease: clientfeatures.Deprecated, Default: false, LockToDefault: true}, + }); err != nil { + t.Fatal(err) + } + all := fg.GetAll() + allexpected := map[featuregate.Feature]featuregate.FeatureSpec{ + "FeatureAlpha": {PreRelease: featuregate.Alpha, Default: true}, + "FeatureBeta": {PreRelease: featuregate.Beta, Default: false}, + "FeatureGA": {PreRelease: featuregate.GA, Default: true, LockToDefault: true}, + "FeatureDeprecated": {PreRelease: featuregate.Deprecated, Default: false, LockToDefault: true}, + } + for name, spec := range defaults { + allexpected[name] = spec + } + if len(all) != len(allexpected) { + t.Errorf("expected %d registered features, got %d", len(allexpected), len(all)) + } + for name, expected := range allexpected { + actual, ok := all[name] + if !ok { + t.Errorf("expected feature %q not found", name) + continue + } + + if actual != expected { + t.Errorf("expected feature %q spec %#v, got spec %#v", name, expected, actual) + } + } + + var r interface{} + func() { + defer func() { + r = recover() + }() + _ = a.Add(map[clientfeatures.Feature]clientfeatures.FeatureSpec{ + "FeatureAlpha": {PreRelease: "foobar"}, + }) + }() + if r == nil { + t.Error("expected panic when adding feature with unknown prerelease") + } +} diff --git a/pkg/features/kube_features.go b/pkg/features/kube_features.go index 045aeb1d384..aadd72ea065 100644 --- a/pkg/features/kube_features.go +++ b/pkg/features/kube_features.go @@ -21,6 +21,7 @@ import ( "k8s.io/apimachinery/pkg/util/runtime" genericfeatures "k8s.io/apiserver/pkg/features" utilfeature "k8s.io/apiserver/pkg/util/feature" + clientfeatures "k8s.io/client-go/features" "k8s.io/component-base/featuregate" ) @@ -904,6 +905,15 @@ const ( func init() { runtime.Must(utilfeature.DefaultMutableFeatureGate.Add(defaultKubernetesFeatureGates)) + + // Register all client-go features with kube's feature gate instance and make all client-go + // feature checks use kube's instance. The effect is that for kube binaries, client-go + // features are wired to the existing --feature-gates flag just as all other features + // are. Further, client-go features automatically support the existing mechanisms for + // feature enablement metrics and test overrides. + ca := &clientAdapter{utilfeature.DefaultMutableFeatureGate} + runtime.Must(clientfeatures.AddFeaturesToExistingFeatureGates(ca)) + clientfeatures.ReplaceFeatureGates(ca) } // defaultKubernetesFeatureGates consists of all known Kubernetes-specific feature keys. diff --git a/vendor/modules.txt b/vendor/modules.txt index c8f4b242fea..f48aaa6fd5a 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1688,6 +1688,7 @@ k8s.io/client-go/dynamic k8s.io/client-go/dynamic/dynamicinformer k8s.io/client-go/dynamic/dynamiclister k8s.io/client-go/dynamic/fake +k8s.io/client-go/features k8s.io/client-go/informers k8s.io/client-go/informers/admissionregistration k8s.io/client-go/informers/admissionregistration/v1