Skip to content

Commit

Permalink
Inject feature gate instance into client-go for kube components.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
benluddy authored and p0lyn0mial committed Jan 15, 2024
1 parent f4aeeb1 commit f0298b3
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 0 deletions.
43 changes: 43 additions & 0 deletions pkg/features/client_adapter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package features

import (
"fmt"

clientfeatures "k8s.io/client-go/features"
"k8s.io/component-base/featuregate"
)

type clientFeatureGateAdapter struct {
mfg featuregate.MutableFeatureGate
}

func (a *clientFeatureGateAdapter) Enabled(name clientfeatures.Feature) bool {
return a.mfg.Enabled(featuregate.Feature(name))
}

func (a *clientFeatureGateAdapter) 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)
}
83 changes: 83 additions & 0 deletions pkg/features/client_adapter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
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 := clientFeatureGateAdapter{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 := clientFeatureGateAdapter{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")
}
}
5 changes: 5 additions & 0 deletions pkg/features/kube_features.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -904,6 +905,10 @@ const (

func init() {
runtime.Must(utilfeature.DefaultMutableFeatureGate.Add(defaultKubernetesFeatureGates))

// replace client-go's feature gate instance with kube's
runtime.Must(clientfeatures.AddFeaturesToExistingFeatureGates(&clientFeatureGateAdapter{utilfeature.DefaultMutableFeatureGate}))
clientfeatures.ReplaceFeatureGates(&clientFeatureGateAdapter{utilfeature.DefaultMutableFeatureGate})
}

// defaultKubernetesFeatureGates consists of all known Kubernetes-specific feature keys.
Expand Down
1 change: 1 addition & 0 deletions vendor/modules.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit f0298b3

Please sign in to comment.