Add version mapping in ComponentGlobalsRegistry.

Signed-off-by: Siyuan Zhang <sizhang@google.com>
This commit is contained in:
Siyuan Zhang 2024-05-31 20:29:48 -07:00
parent 701e5fc374
commit 4352c4ad27
32 changed files with 853 additions and 409 deletions

View File

@ -53,7 +53,7 @@ func TestAddFlags(t *testing.T) {
featureGate := featuregate.NewFeatureGate() featureGate := featuregate.NewFeatureGate()
componentRegistry := utilversion.NewComponentGlobalsRegistry() componentRegistry := utilversion.NewComponentGlobalsRegistry()
effectiveVersion := utilversion.NewEffectiveVersion("1.32") effectiveVersion := utilversion.NewEffectiveVersion("1.32")
_ = componentRegistry.Register("test", effectiveVersion, featureGate, true) utilruntime.Must(componentRegistry.Register("test", effectiveVersion, featureGate))
s := NewServerRunOptions(featureGate, effectiveVersion) s := NewServerRunOptions(featureGate, effectiveVersion)
for _, f := range s.Flags().FlagSets { for _, f := range s.Flags().FlagSets {
fs.AddFlagSet(f) fs.AddFlagSet(f)

View File

@ -43,6 +43,7 @@ import (
"k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
utilerrors "k8s.io/apimachinery/pkg/util/errors" utilerrors "k8s.io/apimachinery/pkg/util/errors"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/util/wait"
serveroptions "k8s.io/apiserver/pkg/server/options" serveroptions "k8s.io/apiserver/pkg/server/options"
"k8s.io/apiserver/pkg/storage/storagebackend" "k8s.io/apiserver/pkg/storage/storagebackend"
@ -182,12 +183,12 @@ func StartTestServer(t ktesting.TB, instanceOptions *TestServerInstanceOptions,
fs := pflag.NewFlagSet("test", pflag.PanicOnError) fs := pflag.NewFlagSet("test", pflag.PanicOnError)
featureGate := utilfeature.DefaultMutableFeatureGate featureGate := utilfeature.DefaultMutableFeatureGate
binaryVersion := utilversion.DefaultKubeEffectiveVersion().BinaryVersion().String() effectiveVersion := utilversion.DefaultKubeEffectiveVersion()
if instanceOptions.BinaryVersion != "" { if instanceOptions.BinaryVersion != "" {
binaryVersion = instanceOptions.BinaryVersion effectiveVersion = utilversion.NewEffectiveVersion(instanceOptions.BinaryVersion)
} }
effectiveVersion := utilversion.NewEffectiveVersion(binaryVersion) utilversion.DefaultComponentGlobalsRegistry.Reset()
_ = utilversion.DefaultComponentGlobalsRegistry.Register(utilversion.DefaultKubeComponent, effectiveVersion, featureGate, true) utilruntime.Must(utilversion.DefaultComponentGlobalsRegistry.Register(utilversion.DefaultKubeComponent, effectiveVersion, featureGate))
s := options.NewServerRunOptions(featureGate, effectiveVersion) s := options.NewServerRunOptions(featureGate, effectiveVersion)

View File

@ -46,7 +46,6 @@ import (
clientgoinformers "k8s.io/client-go/informers" clientgoinformers "k8s.io/client-go/informers"
clientgoclientset "k8s.io/client-go/kubernetes" clientgoclientset "k8s.io/client-go/kubernetes"
"k8s.io/client-go/util/keyutil" "k8s.io/client-go/util/keyutil"
"k8s.io/component-base/version"
aggregatorapiserver "k8s.io/kube-aggregator/pkg/apiserver" aggregatorapiserver "k8s.io/kube-aggregator/pkg/apiserver"
openapicommon "k8s.io/kube-openapi/pkg/common" openapicommon "k8s.io/kube-openapi/pkg/common"
@ -172,9 +171,6 @@ func BuildGenericConfig(
sets.NewString("attach", "exec", "proxy", "log", "portforward"), sets.NewString("attach", "exec", "proxy", "log", "portforward"),
) )
kubeVersion := version.Get()
genericConfig.Version = &kubeVersion
if genericConfig.EgressSelector != nil { if genericConfig.EgressSelector != nil {
s.Etcd.StorageConfig.Transport.EgressLookup = genericConfig.EgressSelector.Lookup s.Etcd.StorageConfig.Transport.EgressLookup = genericConfig.EgressSelector.Lookup
} }

View File

@ -48,7 +48,7 @@ func TestAddFlags(t *testing.T) {
featureGate := featuregate.NewFeatureGate() featureGate := featuregate.NewFeatureGate()
effectiveVersion := utilversion.NewEffectiveVersion("1.32") effectiveVersion := utilversion.NewEffectiveVersion("1.32")
componentRegistry := utilversion.NewComponentGlobalsRegistry() componentRegistry := utilversion.NewComponentGlobalsRegistry()
_ = componentRegistry.Register("test", effectiveVersion, featureGate, true) utilruntime.Must(componentRegistry.Register("test", effectiveVersion, featureGate))
s := NewOptions(featureGate, effectiveVersion) s := NewOptions(featureGate, effectiveVersion)
var fss cliflag.NamedFlagSets var fss cliflag.NamedFlagSets
s.AddFlags(&fss) s.AddFlags(&fss)

View File

@ -20,6 +20,7 @@ import (
"context" "context"
"crypto/tls" "crypto/tls"
"encoding/json" "encoding/json"
"fmt"
"io" "io"
"net" "net"
"net/http" "net/http"
@ -118,9 +119,7 @@ func setUp(t *testing.T) (*etcd3testing.EtcdTestServer, Config, *assert.Assertio
t.Fatal(err) t.Fatal(err)
} }
kubeVersion := kubeversion.Get()
config.ControlPlane.Generic.Authorization.Authorizer = authorizerfactory.NewAlwaysAllowAuthorizer() config.ControlPlane.Generic.Authorization.Authorizer = authorizerfactory.NewAlwaysAllowAuthorizer()
config.ControlPlane.Generic.Version = &kubeVersion
config.ControlPlane.StorageFactory = storageFactory config.ControlPlane.StorageFactory = storageFactory
config.ControlPlane.Generic.LoopbackClientConfig = &restclient.Config{APIPath: "/api", ContentConfig: restclient.ContentConfig{NegotiatedSerializer: legacyscheme.Codecs}} config.ControlPlane.Generic.LoopbackClientConfig = &restclient.Config{APIPath: "/api", ContentConfig: restclient.ContentConfig{NegotiatedSerializer: legacyscheme.Codecs}}
config.ControlPlane.Generic.PublicAddress = netutils.ParseIPSloppy("192.168.10.4") config.ControlPlane.Generic.PublicAddress = netutils.ParseIPSloppy("192.168.10.4")
@ -243,9 +242,13 @@ func TestVersion(t *testing.T) {
if err != nil { if err != nil {
t.Errorf("unexpected error: %v", err) t.Errorf("unexpected error: %v", err)
} }
expectedInfo := kubeversion.Get()
kubeVersion := utilversion.DefaultKubeEffectiveVersion().BinaryVersion()
expectedInfo.Major = fmt.Sprintf("%d", kubeVersion.Major())
expectedInfo.Minor = fmt.Sprintf("%d", kubeVersion.Minor())
if !reflect.DeepEqual(kubeversion.Get(), info) { if !reflect.DeepEqual(expectedInfo, info) {
t.Errorf("Expected %#v, Got %#v", kubeversion.Get(), info) t.Errorf("Expected %#v, Got %#v", expectedInfo, info)
} }
} }

View File

@ -40,7 +40,6 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apimachinery/pkg/version"
"k8s.io/apiserver/pkg/endpoints/discovery" "k8s.io/apiserver/pkg/endpoints/discovery"
"k8s.io/apiserver/pkg/endpoints/discovery/aggregated" "k8s.io/apiserver/pkg/endpoints/discovery/aggregated"
genericregistry "k8s.io/apiserver/pkg/registry/generic" genericregistry "k8s.io/apiserver/pkg/registry/generic"
@ -118,12 +117,6 @@ func (cfg *Config) Complete() CompletedConfig {
} }
c.GenericConfig.EnableDiscovery = false c.GenericConfig.EnableDiscovery = false
if c.GenericConfig.Version == nil {
c.GenericConfig.Version = &version.Info{
Major: "0",
Minor: "1",
}
}
return CompletedConfig{&c} return CompletedConfig{&c}
} }

View File

@ -31,6 +31,7 @@ import (
extensionsapiserver "k8s.io/apiextensions-apiserver/pkg/apiserver" extensionsapiserver "k8s.io/apiextensions-apiserver/pkg/apiserver"
"k8s.io/apiextensions-apiserver/pkg/cmd/server/options" "k8s.io/apiextensions-apiserver/pkg/cmd/server/options"
generatedopenapi "k8s.io/apiextensions-apiserver/pkg/generated/openapi" generatedopenapi "k8s.io/apiextensions-apiserver/pkg/generated/openapi"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/util/wait"
openapinamer "k8s.io/apiserver/pkg/endpoints/openapi" openapinamer "k8s.io/apiserver/pkg/endpoints/openapi"
genericapiserver "k8s.io/apiserver/pkg/server" genericapiserver "k8s.io/apiserver/pkg/server"
@ -124,7 +125,8 @@ func StartTestServer(t Logger, _ *TestServerInstanceOptions, customFlags []strin
featureGate := utilfeature.DefaultMutableFeatureGate featureGate := utilfeature.DefaultMutableFeatureGate
effectiveVersion := utilversion.DefaultKubeEffectiveVersion() effectiveVersion := utilversion.DefaultKubeEffectiveVersion()
_ = utilversion.DefaultComponentGlobalsRegistry.Register(utilversion.DefaultKubeComponent, effectiveVersion, featureGate, true) utilversion.DefaultComponentGlobalsRegistry.Reset()
utilruntime.Must(utilversion.DefaultComponentGlobalsRegistry.Register(utilversion.DefaultKubeComponent, effectiveVersion, featureGate))
s := options.NewCustomResourceDefinitionsServerOptions(os.Stdout, os.Stderr, featureGate, effectiveVersion) s := options.NewCustomResourceDefinitionsServerOptions(os.Stdout, os.Stderr, featureGate, effectiveVersion)
utilversion.DefaultComponentGlobalsRegistry.AddFlags(fs) utilversion.DefaultComponentGlobalsRegistry.AddFlags(fs)

View File

@ -23,6 +23,8 @@ import (
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
apimachineryversion "k8s.io/apimachinery/pkg/version"
) )
// Version is an opaque representation of a version number // Version is an opaque representation of a version number
@ -31,6 +33,7 @@ type Version struct {
semver bool semver bool
preRelease string preRelease string
buildMetadata string buildMetadata string
info apimachineryversion.Info
} }
var ( var (
@ -252,19 +255,30 @@ func (v *Version) WithMinor(minor uint) *Version {
return &result return &result
} }
// SubtractMinor returns the version diff minor versions back, with the same major and no patch. // SubtractMinor returns the version with offset from the original minor, with the same major and no patch.
// If diff >= current minor, the minor would be 0. // If -offset >= current minor, the minor would be 0.
func (v *Version) SubtractMinor(diff uint) *Version { func (v *Version) OffsetMinor(offset int) *Version {
var minor uint var minor uint
if offset >= 0 {
minor = v.Minor() + uint(offset)
} else {
diff := uint(-offset)
if diff < v.Minor() { if diff < v.Minor() {
minor = v.Minor() - diff minor = v.Minor() - diff
} }
}
return MajorMinor(v.Major(), minor) return MajorMinor(v.Major(), minor)
} }
// SubtractMinor returns the version diff minor versions back, with the same major and no patch.
// If diff >= current minor, the minor would be 0.
func (v *Version) SubtractMinor(diff uint) *Version {
return v.OffsetMinor(-int(diff))
}
// AddMinor returns the version diff minor versions forward, with the same major and no patch. // AddMinor returns the version diff minor versions forward, with the same major and no patch.
func (v *Version) AddMinor(diff uint) *Version { func (v *Version) AddMinor(diff uint) *Version {
return MajorMinor(v.Major(), v.Minor()+diff) return v.OffsetMinor(int(diff))
} }
// WithPatch returns copy of the version object with requested patch number // WithPatch returns copy of the version object with requested patch number
@ -441,3 +455,30 @@ func (v *Version) Compare(other string) (int, error) {
} }
return v.compareInternal(ov), nil return v.compareInternal(ov), nil
} }
// WithInfo returns copy of the version object with requested info
func (v *Version) WithInfo(info apimachineryversion.Info) *Version {
result := *v
result.info = info
return &result
}
func (v *Version) Info() *apimachineryversion.Info {
if v == nil {
return nil
}
// in case info is empty, or the major and minor in info is different from the actual major and minor
v.info.Major = itoa(v.Major())
v.info.Minor = itoa(v.Minor())
if v.info.GitVersion == "" {
v.info.GitVersion = v.String()
}
return &v.info
}
func itoa(i uint) string {
if i == 0 {
return ""
}
return strconv.Itoa(int(i))
}

View File

@ -453,37 +453,42 @@ func TestHighestSupportedVersion(t *testing.T) {
} }
} }
func TestSubtractMinor(t *testing.T) { func TestOffsetMinor(t *testing.T) {
var tests = []struct { var tests = []struct {
version string version string
diff uint diff int
expectedComponents []uint expectedComponents []uint
}{ }{
{ {
version: "1.0.2", version: "1.0.2",
diff: 3, diff: -3,
expectedComponents: []uint{1, 0}, expectedComponents: []uint{1, 0},
}, },
{ {
version: "1.3.2-alpha+001", version: "1.3.2-alpha+001",
diff: 2, diff: -2,
expectedComponents: []uint{1, 1}, expectedComponents: []uint{1, 1},
}, },
{ {
version: "1.3.2-alpha+001", version: "1.3.2-alpha+001",
diff: 3, diff: -3,
expectedComponents: []uint{1, 0}, expectedComponents: []uint{1, 0},
}, },
{ {
version: "1.20", version: "1.20",
diff: 5, diff: -5,
expectedComponents: []uint{1, 15}, expectedComponents: []uint{1, 15},
}, },
{
version: "1.20",
diff: 5,
expectedComponents: []uint{1, 25},
},
} }
for _, test := range tests { for _, test := range tests {
version, _ := ParseGeneric(test.version) version, _ := ParseGeneric(test.version)
if !reflect.DeepEqual(test.expectedComponents, version.SubtractMinor(test.diff).Components()) { if !reflect.DeepEqual(test.expectedComponents, version.OffsetMinor(test.diff).Components()) {
t.Error("parse returned un'expected components") t.Error("parse returned un'expected components")
} }
} }

View File

@ -44,7 +44,6 @@ import (
"k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/version" "k8s.io/apimachinery/pkg/util/version"
utilwaitgroup "k8s.io/apimachinery/pkg/util/waitgroup" utilwaitgroup "k8s.io/apimachinery/pkg/util/waitgroup"
apimachineryversion "k8s.io/apimachinery/pkg/version"
"k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/audit" "k8s.io/apiserver/pkg/audit"
"k8s.io/apiserver/pkg/authentication/authenticator" "k8s.io/apiserver/pkg/authentication/authenticator"
@ -150,8 +149,6 @@ type Config struct {
// done values in this values for this map are ignored. // done values in this values for this map are ignored.
PostStartHooks map[string]PostStartHookConfigEntry PostStartHooks map[string]PostStartHookConfigEntry
// Version will enable the /version endpoint if non-nil
Version *apimachineryversion.Info
// EffectiveVersion determines which apis and features are available // EffectiveVersion determines which apis and features are available
// based on when the api/feature lifecyle. // based on when the api/feature lifecyle.
EffectiveVersion utilversion.EffectiveVersion EffectiveVersion utilversion.EffectiveVersion
@ -702,12 +699,8 @@ func (c *Config) Complete(informers informers.SharedInformerFactory) CompletedCo
} }
c.ExternalAddress = net.JoinHostPort(c.ExternalAddress, strconv.Itoa(port)) c.ExternalAddress = net.JoinHostPort(c.ExternalAddress, strconv.Itoa(port))
} }
var ver *version.Version completeOpenAPI(c.OpenAPIConfig, c.EffectiveVersion.EmulationVersion())
if c.EffectiveVersion != nil { completeOpenAPIV3(c.OpenAPIV3Config, c.EffectiveVersion.EmulationVersion())
ver = c.EffectiveVersion.EmulationVersion()
}
completeOpenAPI(c.OpenAPIConfig, ver)
completeOpenAPIV3(c.OpenAPIV3Config, ver)
if c.DiscoveryAddresses == nil { if c.DiscoveryAddresses == nil {
c.DiscoveryAddresses = discovery.DefaultAddresses{DefaultAddress: c.ExternalAddress} c.DiscoveryAddresses = discovery.DefaultAddresses{DefaultAddress: c.ExternalAddress}
@ -834,7 +827,6 @@ func (c completedConfig) New(name string, delegationTarget DelegationTarget) (*G
StorageVersionManager: c.StorageVersionManager, StorageVersionManager: c.StorageVersionManager,
EffectiveVersion: c.EffectiveVersion, EffectiveVersion: c.EffectiveVersion,
Version: c.Version,
FeatureGate: c.FeatureGate, FeatureGate: c.FeatureGate,
muxAndDiscoveryCompleteSignals: map[string]<-chan struct{}{}, muxAndDiscoveryCompleteSignals: map[string]<-chan struct{}{},
@ -1103,7 +1095,7 @@ func installAPI(s *GenericAPIServer, c *Config) {
} }
} }
routes.Version{Version: c.Version}.Install(s.Handler.GoRestfulContainer) routes.Version{Version: c.EffectiveVersion.BinaryVersion().Info()}.Install(s.Handler.GoRestfulContainer)
if c.EnableDiscovery { if c.EnableDiscovery {
if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.AggregatedDiscoveryEndpoint) { if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.AggregatedDiscoveryEndpoint) {

View File

@ -40,6 +40,7 @@ import (
"k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/server/healthz" "k8s.io/apiserver/pkg/server/healthz"
utilversion "k8s.io/apiserver/pkg/util/version"
"k8s.io/client-go/informers" "k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/kubernetes/fake"
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
@ -90,6 +91,7 @@ func TestNewWithDelegate(t *testing.T) {
delegateConfig.PublicAddress = netutils.ParseIPSloppy("192.168.10.4") delegateConfig.PublicAddress = netutils.ParseIPSloppy("192.168.10.4")
delegateConfig.LegacyAPIGroupPrefixes = sets.NewString("/api") delegateConfig.LegacyAPIGroupPrefixes = sets.NewString("/api")
delegateConfig.LoopbackClientConfig = &rest.Config{} delegateConfig.LoopbackClientConfig = &rest.Config{}
delegateConfig.EffectiveVersion = utilversion.NewEffectiveVersion("")
clientset := fake.NewSimpleClientset() clientset := fake.NewSimpleClientset()
if clientset == nil { if clientset == nil {
t.Fatal("unable to create fake client set") t.Fatal("unable to create fake client set")
@ -122,6 +124,7 @@ func TestNewWithDelegate(t *testing.T) {
wrappingConfig.PublicAddress = netutils.ParseIPSloppy("192.168.10.4") wrappingConfig.PublicAddress = netutils.ParseIPSloppy("192.168.10.4")
wrappingConfig.LegacyAPIGroupPrefixes = sets.NewString("/api") wrappingConfig.LegacyAPIGroupPrefixes = sets.NewString("/api")
wrappingConfig.LoopbackClientConfig = &rest.Config{} wrappingConfig.LoopbackClientConfig = &rest.Config{}
wrappingConfig.EffectiveVersion = utilversion.NewEffectiveVersion("")
wrappingConfig.HealthzChecks = append(wrappingConfig.HealthzChecks, healthz.NamedCheck("wrapping-health", func(r *http.Request) error { wrappingConfig.HealthzChecks = append(wrappingConfig.HealthzChecks, healthz.NamedCheck("wrapping-health", func(r *http.Request) error {
return fmt.Errorf("wrapping failed healthcheck") return fmt.Errorf("wrapping failed healthcheck")

View File

@ -100,7 +100,7 @@ func (e *resourceExpirationEvaluator) shouldServe(gv schema.GroupVersion, versio
} }
introduced, ok := versionedPtr.(introducedInterface) introduced, ok := versionedPtr.(introducedInterface)
// skip the introduced check for test where currentVersion is 0.0 // skip the introduced check for test when currentVersion is 0.0 to test all apis
if ok && (e.currentVersion.Major() > 0 || e.currentVersion.Minor() > 0) { if ok && (e.currentVersion.Major() > 0 || e.currentVersion.Minor() > 0) {
majorIntroduced, minorIntroduced := introduced.APILifecycleIntroduced() majorIntroduced, minorIntroduced := introduced.APILifecycleIntroduced()
verIntroduced := apimachineryversion.MajorMinor(uint(majorIntroduced), uint(minorIntroduced)) verIntroduced := apimachineryversion.MajorMinor(uint(majorIntroduced), uint(minorIntroduced))

View File

@ -40,7 +40,6 @@ import (
"k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/util/wait"
utilwaitgroup "k8s.io/apimachinery/pkg/util/waitgroup" utilwaitgroup "k8s.io/apimachinery/pkg/util/waitgroup"
"k8s.io/apimachinery/pkg/version"
"k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/audit" "k8s.io/apiserver/pkg/audit"
"k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/authorization/authorizer"
@ -238,8 +237,6 @@ type GenericAPIServer struct {
// StorageVersionManager holds the storage versions of the API resources installed by this server. // StorageVersionManager holds the storage versions of the API resources installed by this server.
StorageVersionManager storageversion.Manager StorageVersionManager storageversion.Manager
// Version will enable the /version endpoint if non-nil
Version *version.Info
// EffectiveVersion determines which apis and features are available // EffectiveVersion determines which apis and features are available
// based on when the api/feature lifecyle. // based on when the api/feature lifecyle.
EffectiveVersion utilversion.EffectiveVersion EffectiveVersion utilversion.EffectiveVersion

View File

@ -138,7 +138,7 @@ func setUp(t *testing.T) (Config, *assert.Assertions) {
if clientset == nil { if clientset == nil {
t.Fatal("unable to create fake client set") t.Fatal("unable to create fake client set")
} }
config.EffectiveVersion = utilversion.NewEffectiveVersion("")
config.OpenAPIConfig = DefaultOpenAPIConfig(testGetOpenAPIDefinitions, openapinamer.NewDefinitionNamer(runtime.NewScheme())) config.OpenAPIConfig = DefaultOpenAPIConfig(testGetOpenAPIDefinitions, openapinamer.NewDefinitionNamer(runtime.NewScheme()))
config.OpenAPIConfig.Info.Version = "unversioned" config.OpenAPIConfig.Info.Version = "unversioned"
config.OpenAPIV3Config = DefaultOpenAPIV3Config(testGetOpenAPIDefinitions, openapinamer.NewDefinitionNamer(runtime.NewScheme())) config.OpenAPIV3Config = DefaultOpenAPIV3Config(testGetOpenAPIDefinitions, openapinamer.NewDefinitionNamer(runtime.NewScheme()))
@ -460,8 +460,9 @@ func TestNotRestRoutesHaveAuth(t *testing.T) {
config.EnableProfiling = true config.EnableProfiling = true
kubeVersion := fakeVersion() kubeVersion := fakeVersion()
config.Version = &kubeVersion effectiveVersion := utilversion.NewEffectiveVersion(kubeVersion.String())
config.EffectiveVersion = utilversion.NewEffectiveVersion(kubeVersion.String()) effectiveVersion.Set(effectiveVersion.BinaryVersion().WithInfo(kubeVersion), effectiveVersion.EmulationVersion(), effectiveVersion.MinCompatibilityVersion())
config.EffectiveVersion = effectiveVersion
s, err := config.Complete(nil).New("test", NewEmptyDelegate()) s, err := config.Complete(nil).New("test", NewEmptyDelegate())
if err != nil { if err != nil {

View File

@ -278,7 +278,6 @@ func TestServerRunWithSNI(t *testing.T) {
// launch server // launch server
config := setUp(t) config := setUp(t)
v := fakeVersion() v := fakeVersion()
config.Version = &v
config.EffectiveVersion = utilversion.NewEffectiveVersion(v.String()) config.EffectiveVersion = utilversion.NewEffectiveVersion(v.String())
config.EnableIndex = true config.EnableIndex = true

View File

@ -157,6 +157,7 @@ func emulatedStorageVersion(binaryVersionOfResource schema.GroupVersion, example
} }
// If it was introduced after current compatibility version, don't use it // If it was introduced after current compatibility version, don't use it
// skip the introduced check for test when currentVersion is 0.0 to test all apis
if introduced, hasIntroduced := exampleOfGVK.(introducedInterface); hasIntroduced && (compatibilityVersion.Major() > 0 || compatibilityVersion.Minor() > 0) { if introduced, hasIntroduced := exampleOfGVK.(introducedInterface); hasIntroduced && (compatibilityVersion.Major() > 0 || compatibilityVersion.Minor() > 0) {
// API resource lifecycles should be relative to k8s api version // API resource lifecycles should be relative to k8s api version
majorIntroduced, minorIntroduced := introduced.APILifecycleIntroduced() majorIntroduced, minorIntroduced := introduced.APILifecycleIntroduced()

View File

@ -59,12 +59,29 @@ var DefaultComponentGlobalsRegistry ComponentGlobalsRegistry = NewComponentGloba
const ( const (
DefaultKubeComponent = "kube" DefaultKubeComponent = "kube"
klogLevel = 2
) )
type VersionMapping func(from *version.Version) *version.Version
// ComponentGlobals stores the global variables for a component for easy access. // ComponentGlobals stores the global variables for a component for easy access.
type ComponentGlobals struct { type ComponentGlobals struct {
effectiveVersion MutableEffectiveVersion effectiveVersion MutableEffectiveVersion
featureGate featuregate.MutableVersionedFeatureGate featureGate featuregate.MutableVersionedFeatureGate
// emulationVersionMapping contains the mapping from the emulation version of this component
// to the emulation version of another component.
emulationVersionMapping map[string]VersionMapping
// dependentEmulationVersion stores whether or not this component's EmulationVersion is dependent through mapping on another component.
// If true, the emulation version cannot be set from the flag, or version mapping from another component.
dependentEmulationVersion bool
// minCompatibilityVersionMapping contains the mapping from the min compatibility version of this component
// to the min compatibility version of another component.
minCompatibilityVersionMapping map[string]VersionMapping
// dependentMinCompatibilityVersion stores whether or not this component's MinCompatibilityVersion is dependent through mapping on another component
// If true, the min compatibility version cannot be set from the flag, or version mapping from another component.
dependentMinCompatibilityVersion bool
} }
type ComponentGlobalsRegistry interface { type ComponentGlobalsRegistry interface {
@ -75,9 +92,8 @@ type ComponentGlobalsRegistry interface {
// Returns nil if the component is not registered. // Returns nil if the component is not registered.
FeatureGateFor(component string) featuregate.FeatureGate FeatureGateFor(component string) featuregate.FeatureGate
// Register registers the EffectiveVersion and FeatureGate for a component. // Register registers the EffectiveVersion and FeatureGate for a component.
// Overrides existing ComponentGlobals if it is already in the registry if override is true, // returns error if the component is already registered.
// otherwise returns error if the component is already registered. Register(component string, effectiveVersion MutableEffectiveVersion, featureGate featuregate.MutableVersionedFeatureGate) error
Register(component string, effectiveVersion MutableEffectiveVersion, featureGate featuregate.MutableVersionedFeatureGate, override bool) error
// ComponentGlobalsOrRegister would return the registered global variables for the component if it already exists in the registry. // ComponentGlobalsOrRegister would return the registered global variables for the component if it already exists in the registry.
// Otherwise, the provided variables would be registered under the component, and the same variables would be returned. // Otherwise, the provided variables would be registered under the component, and the same variables would be returned.
ComponentGlobalsOrRegister(component string, effectiveVersion MutableEffectiveVersion, featureGate featuregate.MutableVersionedFeatureGate) (MutableEffectiveVersion, featuregate.MutableVersionedFeatureGate) ComponentGlobalsOrRegister(component string, effectiveVersion MutableEffectiveVersion, featureGate featuregate.MutableVersionedFeatureGate) (MutableEffectiveVersion, featuregate.MutableVersionedFeatureGate)
@ -85,25 +101,43 @@ type ComponentGlobalsRegistry interface {
AddFlags(fs *pflag.FlagSet) AddFlags(fs *pflag.FlagSet)
// Set sets the flags for all global variables for all components registered. // Set sets the flags for all global variables for all components registered.
Set() error Set() error
// SetAllComponents calls the Validate() function for all the global variables for all components registered. // Validate calls the Validate() function for all the global variables for all components registered.
Validate() []error Validate() []error
// Reset removes all stored ComponentGlobals, configurations, and version mappings.
Reset()
// SetEmulationVersionMapping sets the mapping from the emulation version of one component
// to the emulation version of another component.
// Once set, the emulation version of the toComponent will be determined by the emulation version of the fromComponent,
// and cannot be set from cmd flags anymore.
// For a given component, its emulation version can only depend on one other component, no multiple dependency is allowed.
SetEmulationVersionMapping(fromComponent, toComponent string, f VersionMapping) error
} }
type componentGlobalsRegistry struct { type componentGlobalsRegistry struct {
componentGlobals map[string]ComponentGlobals componentGlobals map[string]*ComponentGlobals
mutex sync.RWMutex mutex sync.RWMutex
// map of component name to emulation version set from the flag. // list of component name to emulation version set from the flag.
emulationVersionConfig cliflag.ConfigurationMap emulationVersionConfig []string
// map of component name to the list of feature gates set from the flag. // map of component name to the list of feature gates set from the flag.
featureGatesConfig map[string][]string featureGatesConfig map[string][]string
} }
func NewComponentGlobalsRegistry() ComponentGlobalsRegistry { func NewComponentGlobalsRegistry() *componentGlobalsRegistry {
return &componentGlobalsRegistry{ return &componentGlobalsRegistry{
componentGlobals: make(map[string]ComponentGlobals), componentGlobals: make(map[string]*ComponentGlobals),
emulationVersionConfig: nil,
featureGatesConfig: nil,
} }
} }
func (r *componentGlobalsRegistry) Reset() {
r.mutex.RLock()
defer r.mutex.RUnlock()
r.componentGlobals = make(map[string]*ComponentGlobals)
r.emulationVersionConfig = nil
r.featureGatesConfig = nil
}
func (r *componentGlobalsRegistry) EffectiveVersionFor(component string) EffectiveVersion { func (r *componentGlobalsRegistry) EffectiveVersionFor(component string) EffectiveVersion {
r.mutex.RLock() r.mutex.RLock()
defer r.mutex.RUnlock() defer r.mutex.RUnlock()
@ -124,8 +158,8 @@ func (r *componentGlobalsRegistry) FeatureGateFor(component string) featuregate.
return globals.featureGate return globals.featureGate
} }
func (r *componentGlobalsRegistry) unsafeRegister(component string, effectiveVersion MutableEffectiveVersion, featureGate featuregate.MutableVersionedFeatureGate, override bool) error { func (r *componentGlobalsRegistry) unsafeRegister(component string, effectiveVersion MutableEffectiveVersion, featureGate featuregate.MutableVersionedFeatureGate) error {
if _, ok := r.componentGlobals[component]; ok && !override { if _, ok := r.componentGlobals[component]; ok {
return fmt.Errorf("component globals of %s already registered", component) return fmt.Errorf("component globals of %s already registered", component)
} }
if featureGate != nil { if featureGate != nil {
@ -133,18 +167,23 @@ func (r *componentGlobalsRegistry) unsafeRegister(component string, effectiveVer
return err return err
} }
} }
c := ComponentGlobals{effectiveVersion: effectiveVersion, featureGate: featureGate} c := ComponentGlobals{
r.componentGlobals[component] = c effectiveVersion: effectiveVersion,
featureGate: featureGate,
emulationVersionMapping: make(map[string]VersionMapping),
minCompatibilityVersionMapping: make(map[string]VersionMapping),
}
r.componentGlobals[component] = &c
return nil return nil
} }
func (r *componentGlobalsRegistry) Register(component string, effectiveVersion MutableEffectiveVersion, featureGate featuregate.MutableVersionedFeatureGate, override bool) error { func (r *componentGlobalsRegistry) Register(component string, effectiveVersion MutableEffectiveVersion, featureGate featuregate.MutableVersionedFeatureGate) error {
if effectiveVersion == nil { if effectiveVersion == nil {
return fmt.Errorf("cannot register nil effectiveVersion") return fmt.Errorf("cannot register nil effectiveVersion")
} }
r.mutex.Lock() r.mutex.Lock()
defer r.mutex.Unlock() defer r.mutex.Unlock()
return r.unsafeRegister(component, effectiveVersion, featureGate, override) return r.unsafeRegister(component, effectiveVersion, featureGate)
} }
func (r *componentGlobalsRegistry) ComponentGlobalsOrRegister(component string, effectiveVersion MutableEffectiveVersion, featureGate featuregate.MutableVersionedFeatureGate) (MutableEffectiveVersion, featuregate.MutableVersionedFeatureGate) { func (r *componentGlobalsRegistry) ComponentGlobalsOrRegister(component string, effectiveVersion MutableEffectiveVersion, featureGate featuregate.MutableVersionedFeatureGate) (MutableEffectiveVersion, featuregate.MutableVersionedFeatureGate) {
@ -154,13 +193,11 @@ func (r *componentGlobalsRegistry) ComponentGlobalsOrRegister(component string,
if ok { if ok {
return globals.effectiveVersion, globals.featureGate return globals.effectiveVersion, globals.featureGate
} }
utilruntime.Must(r.unsafeRegister(component, effectiveVersion, featureGate, false)) utilruntime.Must(r.unsafeRegister(component, effectiveVersion, featureGate))
return effectiveVersion, featureGate return effectiveVersion, featureGate
} }
func (r *componentGlobalsRegistry) knownFeatures() []string { func (r *componentGlobalsRegistry) unsafeKnownFeatures() []string {
r.mutex.Lock()
defer r.mutex.Unlock()
var known []string var known []string
for component, globals := range r.componentGlobals { for component, globals := range r.componentGlobals {
if globals.featureGate == nil { if globals.featureGate == nil {
@ -174,18 +211,22 @@ func (r *componentGlobalsRegistry) knownFeatures() []string {
return known return known
} }
func (r *componentGlobalsRegistry) versionFlagOptions(isEmulation bool) []string { func (r *componentGlobalsRegistry) unsafeVersionFlagOptions(isEmulation bool) []string {
r.mutex.Lock()
defer r.mutex.Unlock()
var vs []string var vs []string
for component, globals := range r.componentGlobals { for component, globals := range r.componentGlobals {
binaryVer := globals.effectiveVersion.BinaryVersion() binaryVer := globals.effectiveVersion.BinaryVersion()
if isEmulation { if isEmulation {
if globals.dependentEmulationVersion {
continue
}
// emulated version could be between binaryMajor.{binaryMinor} and binaryMajor.{binaryMinor} // emulated version could be between binaryMajor.{binaryMinor} and binaryMajor.{binaryMinor}
// TODO: change to binaryMajor.{binaryMinor-1} and binaryMajor.{binaryMinor} in 1.32 // TODO: change to binaryMajor.{binaryMinor-1} and binaryMajor.{binaryMinor} in 1.32
vs = append(vs, fmt.Sprintf("%s=%s..%s (default=%s)", component, vs = append(vs, fmt.Sprintf("%s=%s..%s (default=%s)", component,
binaryVer.SubtractMinor(0).String(), binaryVer.String(), globals.effectiveVersion.EmulationVersion().String())) binaryVer.SubtractMinor(0).String(), binaryVer.String(), globals.effectiveVersion.EmulationVersion().String()))
} else { } else {
if globals.dependentMinCompatibilityVersion {
continue
}
// min compatibility version could be between binaryMajor.{binaryMinor-1} and binaryMajor.{binaryMinor} // min compatibility version could be between binaryMajor.{binaryMinor-1} and binaryMajor.{binaryMinor}
vs = append(vs, fmt.Sprintf("%s=%s..%s (default=%s)", component, vs = append(vs, fmt.Sprintf("%s=%s..%s (default=%s)", component,
binaryVer.SubtractMinor(1).String(), binaryVer.String(), globals.effectiveVersion.MinCompatibilityVersion().String())) binaryVer.SubtractMinor(1).String(), binaryVer.String(), globals.effectiveVersion.MinCompatibilityVersion().String()))
@ -200,51 +241,133 @@ func (r *componentGlobalsRegistry) AddFlags(fs *pflag.FlagSet) {
return return
} }
r.mutex.Lock() r.mutex.Lock()
defer r.mutex.Unlock()
for _, globals := range r.componentGlobals { for _, globals := range r.componentGlobals {
if globals.featureGate != nil { if globals.featureGate != nil {
globals.featureGate.Close() globals.featureGate.Close()
} }
} }
r.emulationVersionConfig = make(cliflag.ConfigurationMap) if r.emulationVersionConfig != nil || r.featureGatesConfig != nil {
klog.Warning("calling componentGlobalsRegistry.AddFlags more than once, the registry will be set by the latest flags")
}
r.emulationVersionConfig = []string{}
r.featureGatesConfig = make(map[string][]string) r.featureGatesConfig = make(map[string][]string)
r.mutex.Unlock()
fs.Var(&r.emulationVersionConfig, "emulated-version", ""+ fs.StringSliceVar(&r.emulationVersionConfig, "emulated-version", r.emulationVersionConfig, ""+
"The versions different components emulate their capabilities (APIs, features, ...) of.\n"+ "The versions different components emulate their capabilities (APIs, features, ...) of.\n"+
"If set, the component will emulate the behavior of this version instead of the underlying binary version.\n"+ "If set, the component will emulate the behavior of this version instead of the underlying binary version.\n"+
"Version format could only be major.minor, for example: '--emulated-version=wardle=1.2,kube=1.31'. Options are:\n"+strings.Join(r.versionFlagOptions(true), "\n")) "Version format could only be major.minor, for example: '--emulated-version=wardle=1.2,kube=1.31'. Options are:\n"+strings.Join(r.unsafeVersionFlagOptions(true), "\n")+
"If the component is not specified, defaults to \"kube\"")
fs.Var(cliflag.NewColonSeparatedMultimapStringStringAllowDefaultEmptyKey(&r.featureGatesConfig), "feature-gates", "Comma-separated list of component:key=value pairs that describe feature gates for alpha/experimental features of different components.\n"+ fs.Var(cliflag.NewColonSeparatedMultimapStringStringAllowDefaultEmptyKey(&r.featureGatesConfig), "feature-gates", "Comma-separated list of component:key=value pairs that describe feature gates for alpha/experimental features of different components.\n"+
"If the component is not specified, defaults to \"kube\". This flag can be repeatedly invoked. For example: --feature-gates 'wardle:featureA=true,wardle:featureB=false' --feature-gates 'kube:featureC=true'"+ "If the component is not specified, defaults to \"kube\". This flag can be repeatedly invoked. For example: --feature-gates 'wardle:featureA=true,wardle:featureB=false' --feature-gates 'kube:featureC=true'"+
"Options are:\n"+strings.Join(r.knownFeatures(), "\n")) "Options are:\n"+strings.Join(r.unsafeKnownFeatures(), "\n"))
}
type componentVersion struct {
component string
ver *version.Version
}
// getFullEmulationVersionConfig expands the given version config with version registered version mapping,
// and returns the map of component to Version.
func (r *componentGlobalsRegistry) getFullEmulationVersionConfig(
versionConfigMap map[string]*version.Version) (map[string]*version.Version, error) {
result := map[string]*version.Version{}
setQueue := []componentVersion{}
for comp, ver := range versionConfigMap {
if _, ok := r.componentGlobals[comp]; !ok {
return result, fmt.Errorf("component not registered: %s", comp)
}
klog.V(klogLevel).Infof("setting version %s=%s", comp, ver.String())
setQueue = append(setQueue, componentVersion{comp, ver})
}
for len(setQueue) > 0 {
cv := setQueue[0]
if _, visited := result[cv.component]; visited {
return result, fmt.Errorf("setting version of %s more than once, probably version mapping loop", cv.component)
}
setQueue = setQueue[1:]
result[cv.component] = cv.ver
for toComp, f := range r.componentGlobals[cv.component].emulationVersionMapping {
toVer := f(cv.ver)
if toVer == nil {
return result, fmt.Errorf("got nil version from mapping of %s=%s to component:%s", cv.component, cv.ver.String(), toComp)
}
klog.V(klogLevel).Infof("setting version %s=%s from version mapping of %s=%s", toComp, toVer.String(), cv.component, cv.ver.String())
setQueue = append(setQueue, componentVersion{toComp, toVer})
}
}
return result, nil
}
func toVersionMap(versionConfig []string) (map[string]*version.Version, error) {
m := map[string]*version.Version{}
for _, compVer := range versionConfig {
// default to "kube" of component is not specified
k := "kube"
v := compVer
if strings.Contains(compVer, "=") {
arr := strings.SplitN(compVer, "=", 2)
if len(arr) != 2 {
return m, fmt.Errorf("malformed pair, expect string=string")
}
k = strings.TrimSpace(arr[0])
v = strings.TrimSpace(arr[1])
}
ver, err := version.Parse(v)
if err != nil {
return m, err
}
if ver.Patch() != 0 {
return m, fmt.Errorf("patch version not allowed, got: %s=%s", k, ver.String())
}
if existingVer, ok := m[k]; ok {
return m, fmt.Errorf("duplicate version flag, %s=%s and %s=%s", k, existingVer.String(), k, ver.String())
}
m[k] = ver
}
return m, nil
} }
func (r *componentGlobalsRegistry) Set() error { func (r *componentGlobalsRegistry) Set() error {
r.mutex.Lock() r.mutex.Lock()
defer r.mutex.Unlock() defer r.mutex.Unlock()
for comp, emuVer := range r.emulationVersionConfig { emulationVersionConfigMap, err := toVersionMap(r.emulationVersionConfig)
if _, ok := r.componentGlobals[comp]; !ok {
return fmt.Errorf("component not registered: %s", comp)
}
klog.V(2).Infof("setting %s:emulation version to %s\n", comp, emuVer)
v, err := version.Parse(emuVer)
if err != nil { if err != nil {
return err return err
} }
r.componentGlobals[comp].effectiveVersion.SetEmulationVersion(v) for comp := range emulationVersionConfigMap {
if _, ok := r.componentGlobals[comp]; !ok {
return fmt.Errorf("component not registered: %s", comp)
}
// only components without any dependencies can be set from the flag.
if r.componentGlobals[comp].dependentEmulationVersion {
return fmt.Errorf("EmulationVersion of %s is set by mapping, cannot set it by flag", comp)
}
}
if emulationVersions, err := r.getFullEmulationVersionConfig(emulationVersionConfigMap); err != nil {
return err
} else {
for comp, ver := range emulationVersions {
r.componentGlobals[comp].effectiveVersion.SetEmulationVersion(ver)
}
} }
// Set feature gate emulation version before setting feature gate flag values. // Set feature gate emulation version before setting feature gate flag values.
for comp, globals := range r.componentGlobals { for comp, globals := range r.componentGlobals {
if globals.featureGate == nil { if globals.featureGate == nil {
continue continue
} }
klog.V(2).Infof("setting %s:feature gate emulation version to %s\n", comp, globals.effectiveVersion.EmulationVersion().String()) klog.V(klogLevel).Infof("setting %s:feature gate emulation version to %s", comp, globals.effectiveVersion.EmulationVersion().String())
if err := globals.featureGate.SetEmulationVersion(globals.effectiveVersion.EmulationVersion()); err != nil { if err := globals.featureGate.SetEmulationVersion(globals.effectiveVersion.EmulationVersion()); err != nil {
return err return err
} }
} }
for comp, fg := range r.featureGatesConfig { for comp, fg := range r.featureGatesConfig {
if comp == "" { if comp == "" {
if _, ok := r.featureGatesConfig[DefaultKubeComponent]; ok {
return fmt.Errorf("set kube feature gates with default empty prefix or kube: prefix consistently, do not mix use")
}
comp = DefaultKubeComponent comp = DefaultKubeComponent
} }
if _, ok := r.componentGlobals[comp]; !ok { if _, ok := r.componentGlobals[comp]; !ok {
@ -255,7 +378,7 @@ func (r *componentGlobalsRegistry) Set() error {
return fmt.Errorf("component featureGate not registered: %s", comp) return fmt.Errorf("component featureGate not registered: %s", comp)
} }
flagVal := strings.Join(fg, ",") flagVal := strings.Join(fg, ",")
klog.V(2).Infof("setting %s:feature-gates=%s\n", comp, flagVal) klog.V(klogLevel).Infof("setting %s:feature-gates=%s", comp, flagVal)
if err := featureGate.Set(flagVal); err != nil { if err := featureGate.Set(flagVal); err != nil {
return err return err
} }
@ -275,3 +398,39 @@ func (r *componentGlobalsRegistry) Validate() []error {
} }
return errs return errs
} }
func (r *componentGlobalsRegistry) SetEmulationVersionMapping(fromComponent, toComponent string, f VersionMapping) error {
if f == nil {
return nil
}
klog.V(klogLevel).Infof("setting EmulationVersion mapping from %s to %s", fromComponent, toComponent)
r.mutex.Lock()
defer r.mutex.Unlock()
if _, ok := r.componentGlobals[fromComponent]; !ok {
return fmt.Errorf("component not registered: %s", fromComponent)
}
if _, ok := r.componentGlobals[toComponent]; !ok {
return fmt.Errorf("component not registered: %s", toComponent)
}
// check multiple dependency
if r.componentGlobals[toComponent].dependentEmulationVersion {
return fmt.Errorf("mapping of %s already exists from another component", toComponent)
}
r.componentGlobals[toComponent].dependentEmulationVersion = true
versionMapping := r.componentGlobals[fromComponent].emulationVersionMapping
if _, ok := versionMapping[toComponent]; ok {
return fmt.Errorf("EmulationVersion from %s to %s already exists", fromComponent, toComponent)
}
versionMapping[toComponent] = f
klog.V(klogLevel).Infof("setting the default EmulationVersion of %s based on mapping from the default EmulationVersion of %s", fromComponent, toComponent)
defaultFromVersion := r.componentGlobals[fromComponent].effectiveVersion.EmulationVersion()
emulationVersions, err := r.getFullEmulationVersionConfig(map[string]*version.Version{fromComponent: defaultFromVersion})
if err != nil {
return err
}
for comp, ver := range emulationVersions {
r.componentGlobals[comp].effectiveVersion.SetEmulationVersion(ver)
}
return nil
}

View File

@ -22,8 +22,8 @@ import (
"testing" "testing"
"github.com/spf13/pflag" "github.com/spf13/pflag"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/version" "k8s.io/apimachinery/pkg/util/version"
cliflag "k8s.io/component-base/cli/flag"
"k8s.io/component-base/featuregate" "k8s.io/component-base/featuregate"
) )
@ -39,30 +39,23 @@ func TestEffectiveVersionRegistry(t *testing.T) {
if r.EffectiveVersionFor(testComponent) != nil { if r.EffectiveVersionFor(testComponent) != nil {
t.Fatalf("expected nil EffectiveVersion initially") t.Fatalf("expected nil EffectiveVersion initially")
} }
if err := r.Register(testComponent, ver1, nil, false); err != nil { if err := r.Register(testComponent, ver1, nil); err != nil {
t.Fatalf("expected no error to register new component, but got err: %v", err) t.Fatalf("expected no error to register new component, but got err: %v", err)
} }
if !r.EffectiveVersionFor(testComponent).EqualTo(ver1) { if !r.EffectiveVersionFor(testComponent).EqualTo(ver1) {
t.Fatalf("expected EffectiveVersionFor to return the version registered") t.Fatalf("expected EffectiveVersionFor to return the version registered")
} }
// overwrite // overwrite
if err := r.Register(testComponent, ver2, nil, false); err == nil { if err := r.Register(testComponent, ver2, nil); err == nil {
t.Fatalf("expected error to register existing component when override is false") t.Fatalf("expected error to register existing component when override is false")
} }
if err := r.Register(testComponent, ver2, nil, true); err != nil { if !r.EffectiveVersionFor(testComponent).EqualTo(ver1) {
t.Fatalf("expected no error to overriding existing component, but got err: %v", err)
}
if !r.EffectiveVersionFor(testComponent).EqualTo(ver2) {
t.Fatalf("expected EffectiveVersionFor to return the version overridden") t.Fatalf("expected EffectiveVersionFor to return the version overridden")
} }
} }
func testRegistry(t *testing.T) *componentGlobalsRegistry { func testRegistry(t *testing.T) *componentGlobalsRegistry {
r := componentGlobalsRegistry{ r := NewComponentGlobalsRegistry()
componentGlobals: map[string]ComponentGlobals{},
emulationVersionConfig: make(cliflag.ConfigurationMap),
featureGatesConfig: make(map[string][]string),
}
verKube := NewEffectiveVersion("1.31") verKube := NewEffectiveVersion("1.31")
fgKube := featuregate.NewVersionedFeatureGate(version.MustParse("0.0")) fgKube := featuregate.NewVersionedFeatureGate(version.MustParse("0.0"))
err := fgKube.AddVersioned(map[featuregate.Feature]featuregate.VersionedSpecs{ err := fgKube.AddVersioned(map[featuregate.Feature]featuregate.VersionedSpecs{
@ -102,19 +95,35 @@ func testRegistry(t *testing.T) *componentGlobalsRegistry {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
_ = r.Register(DefaultKubeComponent, verKube, fgKube, true) utilruntime.Must(r.Register(DefaultKubeComponent, verKube, fgKube))
_ = r.Register(testComponent, verTest, fgTest, true) utilruntime.Must(r.Register(testComponent, verTest, fgTest))
return &r return r
} }
func TestVersionFlagOptions(t *testing.T) { func TestVersionFlagOptions(t *testing.T) {
r := testRegistry(t) r := testRegistry(t)
emuVers := strings.Join(r.versionFlagOptions(true), "\n") emuVers := strings.Join(r.unsafeVersionFlagOptions(true), "\n")
expectedEmuVers := "kube=1.31..1.31 (default=1.31)\ntest=2.8..2.8 (default=2.8)" expectedEmuVers := "kube=1.31..1.31 (default=1.31)\ntest=2.8..2.8 (default=2.8)"
if emuVers != expectedEmuVers { if emuVers != expectedEmuVers {
t.Errorf("wanted emulation version flag options to be: %s, got %s", expectedEmuVers, emuVers) t.Errorf("wanted emulation version flag options to be: %s, got %s", expectedEmuVers, emuVers)
} }
minCompVers := strings.Join(r.versionFlagOptions(false), "\n") minCompVers := strings.Join(r.unsafeVersionFlagOptions(false), "\n")
expectedMinCompVers := "kube=1.30..1.31 (default=1.30)\ntest=2.7..2.8 (default=2.7)"
if minCompVers != expectedMinCompVers {
t.Errorf("wanted min compatibility version flag options to be: %s, got %s", expectedMinCompVers, minCompVers)
}
}
func TestVersionFlagOptionsWithMapping(t *testing.T) {
r := testRegistry(t)
utilruntime.Must(r.SetEmulationVersionMapping(testComponent, DefaultKubeComponent,
func(from *version.Version) *version.Version { return from.OffsetMinor(3) }))
emuVers := strings.Join(r.unsafeVersionFlagOptions(true), "\n")
expectedEmuVers := "test=2.8..2.8 (default=2.8)"
if emuVers != expectedEmuVers {
t.Errorf("wanted emulation version flag options to be: %s, got %s", expectedEmuVers, emuVers)
}
minCompVers := strings.Join(r.unsafeVersionFlagOptions(false), "\n")
expectedMinCompVers := "kube=1.30..1.31 (default=1.30)\ntest=2.7..2.8 (default=2.7)" expectedMinCompVers := "kube=1.30..1.31 (default=1.30)\ntest=2.7..2.8 (default=2.7)"
if minCompVers != expectedMinCompVers { if minCompVers != expectedMinCompVers {
t.Errorf("wanted min compatibility version flag options to be: %s, got %s", expectedMinCompVers, minCompVers) t.Errorf("wanted min compatibility version flag options to be: %s, got %s", expectedMinCompVers, minCompVers)
@ -123,7 +132,7 @@ func TestVersionFlagOptions(t *testing.T) {
func TestVersionedFeatureGateFlag(t *testing.T) { func TestVersionedFeatureGateFlag(t *testing.T) {
r := testRegistry(t) r := testRegistry(t)
known := strings.Join(r.knownFeatures(), "\n") known := strings.Join(r.unsafeKnownFeatures(), "\n")
expectedKnown := "kube:AllAlpha=true|false (ALPHA - default=false)\n" + expectedKnown := "kube:AllAlpha=true|false (ALPHA - default=false)\n" +
"kube:AllBeta=true|false (BETA - default=false)\n" + "kube:AllBeta=true|false (BETA - default=false)\n" +
"kube:commonC=true|false (BETA - default=true)\n" + "kube:commonC=true|false (BETA - default=true)\n" +
@ -140,85 +149,126 @@ func TestVersionedFeatureGateFlag(t *testing.T) {
func TestFlags(t *testing.T) { func TestFlags(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
emulationVersionFlag string flags []string
featureGatesFlag string
parseError string parseError string
expectedKubeEmulationVersion *version.Version expectedKubeEmulationVersion string
expectedTestEmulationVersion *version.Version expectedTestEmulationVersion string
expectedKubeFeatureValues map[featuregate.Feature]bool expectedKubeFeatureValues map[featuregate.Feature]bool
expectedTestFeatureValues map[featuregate.Feature]bool expectedTestFeatureValues map[featuregate.Feature]bool
}{ }{
{ {
name: "setting kube emulation version", name: "setting kube emulation version",
emulationVersionFlag: "kube=1.30", flags: []string{"--emulated-version=kube=1.30"},
expectedKubeEmulationVersion: version.MajorMinor(1, 30), expectedKubeEmulationVersion: "1.30",
}, },
{ {
name: "setting kube emulation version, prefix v ok", name: "setting kube emulation version twice",
emulationVersionFlag: "kube=v1.30", flags: []string{
expectedKubeEmulationVersion: version.MajorMinor(1, 30), "--emulated-version=kube=1.30",
"--emulated-version=kube=1.32",
},
parseError: "duplicate version flag, kube=1.30 and kube=1.32",
},
{
name: "prefix v ok",
flags: []string{"--emulated-version=kube=v1.30"},
expectedKubeEmulationVersion: "1.30",
},
{
name: "patch version not ok",
flags: []string{"--emulated-version=kube=1.30.2"},
parseError: "patch version not allowed, got: kube=1.30.2",
}, },
{ {
name: "setting test emulation version", name: "setting test emulation version",
emulationVersionFlag: "test=2.7", flags: []string{"--emulated-version=test=2.7"},
expectedKubeEmulationVersion: version.MajorMinor(1, 31), expectedKubeEmulationVersion: "1.31",
expectedTestEmulationVersion: version.MajorMinor(2, 7), expectedTestEmulationVersion: "2.7",
}, },
{ {
name: "version missing component", name: "version missing component default to kube",
emulationVersionFlag: "1.31", flags: []string{"--emulated-version=1.30"},
parseError: "component not registered: 1.31", expectedKubeEmulationVersion: "1.30",
},
{
name: "version missing component default to kube with duplicate",
flags: []string{"--emulated-version=1.30", "--emulated-version=kube=1.30"},
parseError: "duplicate version flag, kube=1.30 and kube=1.30",
}, },
{ {
name: "version unregistered component", name: "version unregistered component",
emulationVersionFlag: "test3=1.31", flags: []string{"--emulated-version=test3=1.31"},
parseError: "component not registered: test3", parseError: "component not registered: test3",
}, },
{ {
name: "invalid version", name: "invalid version",
emulationVersionFlag: "test=1.foo", flags: []string{"--emulated-version=test=1.foo"},
parseError: "illegal version string \"1.foo\"", parseError: "illegal version string \"1.foo\"",
}, },
{ {
name: "setting test feature flag", name: "setting test feature flag",
emulationVersionFlag: "test=2.7", flags: []string{
featureGatesFlag: "test:testA=true", "--emulated-version=test=2.7",
expectedKubeEmulationVersion: version.MajorMinor(1, 31), "--feature-gates=test:testA=true",
expectedTestEmulationVersion: version.MajorMinor(2, 7), },
expectedKubeEmulationVersion: "1.31",
expectedTestEmulationVersion: "2.7",
expectedKubeFeatureValues: map[featuregate.Feature]bool{"kubeA": true, "kubeB": false, "commonC": true}, expectedKubeFeatureValues: map[featuregate.Feature]bool{"kubeA": true, "kubeB": false, "commonC": true},
expectedTestFeatureValues: map[featuregate.Feature]bool{"testA": true, "testB": false, "commonC": false}, expectedTestFeatureValues: map[featuregate.Feature]bool{"testA": true, "testB": false, "commonC": false},
}, },
{ {
name: "setting future test feature flag", name: "setting future test feature flag",
emulationVersionFlag: "test=2.7", flags: []string{
featureGatesFlag: "test:testA=true,test:testB=true", "--emulated-version=test=2.7",
"--feature-gates=test:testA=true,test:testB=true",
},
parseError: "cannot set feature gate testB to true, feature is PreAlpha at emulated version 2.7", parseError: "cannot set feature gate testB to true, feature is PreAlpha at emulated version 2.7",
}, },
{ {
name: "setting kube feature flag", name: "setting kube feature flag",
emulationVersionFlag: "test=2.7,kube=1.30", flags: []string{
featureGatesFlag: "test:commonC=true,commonC=false,kube:kubeB=true", "--emulated-version=test=2.7",
expectedKubeEmulationVersion: version.MajorMinor(1, 30), "--emulated-version=kube=1.30",
expectedTestEmulationVersion: version.MajorMinor(2, 7), "--feature-gates=kubeB=false,test:commonC=true",
"--feature-gates=commonC=false,kubeB=true",
},
expectedKubeEmulationVersion: "1.30",
expectedTestEmulationVersion: "2.7",
expectedKubeFeatureValues: map[featuregate.Feature]bool{"kubeA": false, "kubeB": true, "commonC": false}, expectedKubeFeatureValues: map[featuregate.Feature]bool{"kubeA": false, "kubeB": true, "commonC": false},
expectedTestFeatureValues: map[featuregate.Feature]bool{"testA": false, "testB": false, "commonC": true}, expectedTestFeatureValues: map[featuregate.Feature]bool{"testA": false, "testB": false, "commonC": true},
}, },
{
name: "setting kube feature flag with different prefix",
flags: []string{
"--emulated-version=test=2.7",
"--emulated-version=kube=1.30",
"--feature-gates=kube:kubeB=false,test:commonC=true",
"--feature-gates=commonC=false,kubeB=true",
},
parseError: "set kube feature gates with default empty prefix or kube: prefix consistently, do not mix use",
},
{ {
name: "setting locked kube feature flag", name: "setting locked kube feature flag",
emulationVersionFlag: "test=2.7", flags: []string{
featureGatesFlag: "kubeA=false", "--emulated-version=test=2.7",
"--feature-gates=kubeA=false",
},
parseError: "cannot set feature gate kubeA to false, feature is locked to true", parseError: "cannot set feature gate kubeA to false, feature is locked to true",
}, },
{ {
name: "setting unknown test feature flag", name: "setting unknown test feature flag",
emulationVersionFlag: "test=2.7", flags: []string{
featureGatesFlag: "test:testD=true", "--emulated-version=test=2.7",
"--feature-gates=test:testD=true",
},
parseError: "unrecognized feature gate: testD", parseError: "unrecognized feature gate: testD",
}, },
{ {
name: "setting unknown component feature flag", name: "setting unknown component feature flag",
emulationVersionFlag: "test=2.7", flags: []string{
featureGatesFlag: "test3:commonC=true", "--emulated-version=test=2.7",
"--feature-gates=test3:commonC=true",
},
parseError: "component not registered: test3", parseError: "component not registered: test3",
}, },
} }
@ -227,9 +277,7 @@ func TestFlags(t *testing.T) {
fs := pflag.NewFlagSet("testflag", pflag.ContinueOnError) fs := pflag.NewFlagSet("testflag", pflag.ContinueOnError)
r := testRegistry(t) r := testRegistry(t)
r.AddFlags(fs) r.AddFlags(fs)
err := fs.Parse(test.flags)
err := fs.Parse([]string{fmt.Sprintf("--emulated-version=%s", test.emulationVersionFlag),
fmt.Sprintf("--feature-gates=%s", test.featureGatesFlag)})
if err == nil { if err == nil {
err = r.Set() err = r.Set()
} }
@ -242,19 +290,11 @@ func TestFlags(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("%d: Parse() expected: nil, got: %v", i, err) t.Fatalf("%d: Parse() expected: nil, got: %v", i, err)
} }
if test.expectedKubeEmulationVersion != nil { if len(test.expectedKubeEmulationVersion) > 0 {
v := r.EffectiveVersionFor("kube").EmulationVersion() assertVersionEqualTo(t, r.EffectiveVersionFor(DefaultKubeComponent).EmulationVersion(), test.expectedKubeEmulationVersion)
if !v.EqualTo(test.expectedKubeEmulationVersion) {
t.Fatalf("%d: EmulationVersion expected: %s, got: %s", i, test.expectedKubeEmulationVersion.String(), v.String())
return
}
}
if test.expectedTestEmulationVersion != nil {
v := r.EffectiveVersionFor("test").EmulationVersion()
if !v.EqualTo(test.expectedTestEmulationVersion) {
t.Fatalf("%d: EmulationVersion expected: %s, got: %s", i, test.expectedTestEmulationVersion.String(), v.String())
return
} }
if len(test.expectedTestEmulationVersion) > 0 {
assertVersionEqualTo(t, r.EffectiveVersionFor(testComponent).EmulationVersion(), test.expectedTestEmulationVersion)
} }
for f, v := range test.expectedKubeFeatureValues { for f, v := range test.expectedKubeFeatureValues {
if r.FeatureGateFor(DefaultKubeComponent).Enabled(f) != v { if r.FeatureGateFor(DefaultKubeComponent).Enabled(f) != v {
@ -269,3 +309,110 @@ func TestFlags(t *testing.T) {
}) })
} }
} }
func TestVersionMapping(t *testing.T) {
r := NewComponentGlobalsRegistry()
ver1 := NewEffectiveVersion("0.58")
ver2 := NewEffectiveVersion("1.28")
ver3 := NewEffectiveVersion("2.10")
utilruntime.Must(r.Register("test1", ver1, nil))
utilruntime.Must(r.Register("test2", ver2, nil))
utilruntime.Must(r.Register("test3", ver3, nil))
assertVersionEqualTo(t, r.EffectiveVersionFor("test1").EmulationVersion(), "0.58")
assertVersionEqualTo(t, r.EffectiveVersionFor("test2").EmulationVersion(), "1.28")
assertVersionEqualTo(t, r.EffectiveVersionFor("test3").EmulationVersion(), "2.10")
utilruntime.Must(r.SetEmulationVersionMapping("test2", "test3",
func(from *version.Version) *version.Version {
return version.MajorMinor(from.Major()+1, from.Minor()-19)
}))
utilruntime.Must(r.SetEmulationVersionMapping("test1", "test2",
func(from *version.Version) *version.Version {
return version.MajorMinor(from.Major()+1, from.Minor()-28)
}))
assertVersionEqualTo(t, r.EffectiveVersionFor("test1").EmulationVersion(), "0.58")
assertVersionEqualTo(t, r.EffectiveVersionFor("test2").EmulationVersion(), "1.30")
assertVersionEqualTo(t, r.EffectiveVersionFor("test3").EmulationVersion(), "2.11")
fs := pflag.NewFlagSet("testflag", pflag.ContinueOnError)
r.AddFlags(fs)
if err := fs.Parse([]string{fmt.Sprintf("--emulated-version=%s", "test1=0.56")}); err != nil {
t.Fatal(err)
return
}
if err := r.Set(); err != nil {
t.Fatal(err)
return
}
assertVersionEqualTo(t, r.EffectiveVersionFor("test1").EmulationVersion(), "0.56")
assertVersionEqualTo(t, r.EffectiveVersionFor("test2").EmulationVersion(), "1.28")
assertVersionEqualTo(t, r.EffectiveVersionFor("test3").EmulationVersion(), "2.09")
}
func TestVersionMappingWithMultipleDependency(t *testing.T) {
r := NewComponentGlobalsRegistry()
ver1 := NewEffectiveVersion("0.58")
ver2 := NewEffectiveVersion("1.28")
ver3 := NewEffectiveVersion("2.10")
utilruntime.Must(r.Register("test1", ver1, nil))
utilruntime.Must(r.Register("test2", ver2, nil))
utilruntime.Must(r.Register("test3", ver3, nil))
assertVersionEqualTo(t, r.EffectiveVersionFor("test1").EmulationVersion(), "0.58")
assertVersionEqualTo(t, r.EffectiveVersionFor("test2").EmulationVersion(), "1.28")
assertVersionEqualTo(t, r.EffectiveVersionFor("test3").EmulationVersion(), "2.10")
utilruntime.Must(r.SetEmulationVersionMapping("test1", "test2",
func(from *version.Version) *version.Version {
return version.MajorMinor(from.Major()+1, from.Minor()-28)
}))
err := r.SetEmulationVersionMapping("test3", "test2",
func(from *version.Version) *version.Version {
return version.MajorMinor(from.Major()-1, from.Minor()+19)
})
if err == nil {
t.Errorf("expect error when setting 2nd mapping to test2")
}
}
func TestVersionMappingWithCyclicDependency(t *testing.T) {
r := NewComponentGlobalsRegistry()
ver1 := NewEffectiveVersion("0.58")
ver2 := NewEffectiveVersion("1.28")
ver3 := NewEffectiveVersion("2.10")
utilruntime.Must(r.Register("test1", ver1, nil))
utilruntime.Must(r.Register("test2", ver2, nil))
utilruntime.Must(r.Register("test3", ver3, nil))
assertVersionEqualTo(t, r.EffectiveVersionFor("test1").EmulationVersion(), "0.58")
assertVersionEqualTo(t, r.EffectiveVersionFor("test2").EmulationVersion(), "1.28")
assertVersionEqualTo(t, r.EffectiveVersionFor("test3").EmulationVersion(), "2.10")
utilruntime.Must(r.SetEmulationVersionMapping("test1", "test2",
func(from *version.Version) *version.Version {
return version.MajorMinor(from.Major()+1, from.Minor()-28)
}))
utilruntime.Must(r.SetEmulationVersionMapping("test2", "test3",
func(from *version.Version) *version.Version {
return version.MajorMinor(from.Major()+1, from.Minor()-19)
}))
err := r.SetEmulationVersionMapping("test3", "test1",
func(from *version.Version) *version.Version {
return version.MajorMinor(from.Major()-2, from.Minor()+48)
})
if err == nil {
t.Errorf("expect cyclic version mapping error")
}
}
func assertVersionEqualTo(t *testing.T, ver *version.Version, expectedVer string) {
if ver.EqualTo(version.MustParse(expectedVer)) {
return
}
t.Errorf("expected: %s, got %s", expectedVer, ver.String())
}

View File

@ -18,10 +18,8 @@ package version
import ( import (
"fmt" "fmt"
"strings"
"sync/atomic" "sync/atomic"
"github.com/spf13/pflag"
"k8s.io/apimachinery/pkg/util/version" "k8s.io/apimachinery/pkg/util/version"
baseversion "k8s.io/component-base/version" baseversion "k8s.io/component-base/version"
) )
@ -40,37 +38,6 @@ type MutableEffectiveVersion interface {
Set(binaryVersion, emulationVersion, minCompatibilityVersion *version.Version) Set(binaryVersion, emulationVersion, minCompatibilityVersion *version.Version)
SetEmulationVersion(emulationVersion *version.Version) SetEmulationVersion(emulationVersion *version.Version)
SetMinCompatibilityVersion(minCompatibilityVersion *version.Version) SetMinCompatibilityVersion(minCompatibilityVersion *version.Version)
// AddFlags adds the "{prefix}-emulated-version" to the flagset.
AddFlags(fs *pflag.FlagSet, prefix string)
}
type VersionVar struct {
val atomic.Pointer[version.Version]
}
// Set sets the flag value
func (v *VersionVar) Set(s string) error {
components := strings.Split(s, ".")
if len(components) != 2 {
return fmt.Errorf("version %s is not in the format of major.minor", s)
}
ver, err := version.ParseGeneric(s)
if err != nil {
return err
}
v.val.Store(ver)
return nil
}
// String returns the flag value
func (v *VersionVar) String() string {
ver := v.val.Load()
return ver.String()
}
// Type gets the flag type
func (v *VersionVar) Type() string {
return "version"
} }
type effectiveVersion struct { type effectiveVersion struct {
@ -78,9 +45,9 @@ type effectiveVersion struct {
// If the emulationVersion is set by the users, it could only contain major and minor versions. // If the emulationVersion is set by the users, it could only contain major and minor versions.
// In tests, emulationVersion could be the same as the binary version, or set directly, // In tests, emulationVersion could be the same as the binary version, or set directly,
// which can have "alpha" as pre-release to continue serving expired apis while we clean up the test. // which can have "alpha" as pre-release to continue serving expired apis while we clean up the test.
emulationVersion VersionVar emulationVersion atomic.Pointer[version.Version]
// minCompatibilityVersion could only contain major and minor versions. // minCompatibilityVersion could only contain major and minor versions.
minCompatibilityVersion VersionVar minCompatibilityVersion atomic.Pointer[version.Version]
} }
func (m *effectiveVersion) BinaryVersion() *version.Version { func (m *effectiveVersion) BinaryVersion() *version.Version {
@ -88,13 +55,17 @@ func (m *effectiveVersion) BinaryVersion() *version.Version {
} }
func (m *effectiveVersion) EmulationVersion() *version.Version { func (m *effectiveVersion) EmulationVersion() *version.Version {
ver := m.emulationVersion.Load()
if ver != nil {
// Emulation version can have "alpha" as pre-release to continue serving expired apis while we clean up the test. // Emulation version can have "alpha" as pre-release to continue serving expired apis while we clean up the test.
// The pre-release should not be accessible to the users. // The pre-release should not be accessible to the users.
return m.emulationVersion.val.Load().WithPreRelease(m.BinaryVersion().PreRelease()) return ver.WithPreRelease(m.BinaryVersion().PreRelease())
}
return ver
} }
func (m *effectiveVersion) MinCompatibilityVersion() *version.Version { func (m *effectiveVersion) MinCompatibilityVersion() *version.Version {
return m.minCompatibilityVersion.val.Load() return m.minCompatibilityVersion.Load()
} }
func (m *effectiveVersion) EqualTo(other EffectiveVersion) bool { func (m *effectiveVersion) EqualTo(other EffectiveVersion) bool {
@ -109,26 +80,33 @@ func (m *effectiveVersion) String() string {
m.BinaryVersion().String(), m.EmulationVersion().String(), m.MinCompatibilityVersion().String()) m.BinaryVersion().String(), m.EmulationVersion().String(), m.MinCompatibilityVersion().String())
} }
func majorMinor(ver *version.Version) *version.Version {
if ver == nil {
return ver
}
return version.MajorMinor(ver.Major(), ver.Minor())
}
func (m *effectiveVersion) Set(binaryVersion, emulationVersion, minCompatibilityVersion *version.Version) { func (m *effectiveVersion) Set(binaryVersion, emulationVersion, minCompatibilityVersion *version.Version) {
m.binaryVersion.Store(binaryVersion) m.binaryVersion.Store(binaryVersion)
m.emulationVersion.val.Store(version.MajorMinor(emulationVersion.Major(), emulationVersion.Minor())) m.emulationVersion.Store(majorMinor(emulationVersion))
m.minCompatibilityVersion.val.Store(version.MajorMinor(minCompatibilityVersion.Major(), minCompatibilityVersion.Minor())) m.minCompatibilityVersion.Store(majorMinor(minCompatibilityVersion))
} }
func (m *effectiveVersion) SetEmulationVersion(emulationVersion *version.Version) { func (m *effectiveVersion) SetEmulationVersion(emulationVersion *version.Version) {
m.emulationVersion.val.Store(version.MajorMinor(emulationVersion.Major(), emulationVersion.Minor())) m.emulationVersion.Store(majorMinor(emulationVersion))
} }
func (m *effectiveVersion) SetMinCompatibilityVersion(minCompatibilityVersion *version.Version) { func (m *effectiveVersion) SetMinCompatibilityVersion(minCompatibilityVersion *version.Version) {
m.minCompatibilityVersion.val.Store(version.MajorMinor(minCompatibilityVersion.Major(), minCompatibilityVersion.Minor())) m.minCompatibilityVersion.Store(majorMinor(minCompatibilityVersion))
} }
func (m *effectiveVersion) Validate() []error { func (m *effectiveVersion) Validate() []error {
var errs []error var errs []error
// Validate only checks the major and minor versions. // Validate only checks the major and minor versions.
binaryVersion := m.binaryVersion.Load().WithPatch(0) binaryVersion := m.binaryVersion.Load().WithPatch(0)
emulationVersion := m.emulationVersion.val.Load() emulationVersion := m.emulationVersion.Load()
minCompatibilityVersion := m.minCompatibilityVersion.val.Load() minCompatibilityVersion := m.minCompatibilityVersion.Load()
// emulationVersion can only be 1.{binaryMinor-1}...1.{binaryMinor}. // emulationVersion can only be 1.{binaryMinor-1}...1.{binaryMinor}.
maxEmuVer := binaryVersion maxEmuVer := binaryVersion
@ -151,45 +129,36 @@ func (m *effectiveVersion) Validate() []error {
return errs return errs
} }
// AddFlags adds the "{prefix}-emulated-version" to the flagset. func newEffectiveVersion(binaryVersion *version.Version) MutableEffectiveVersion {
func (m *effectiveVersion) AddFlags(fs *pflag.FlagSet, prefix string) {
if m == nil {
return
}
if len(prefix) > 0 && !strings.HasSuffix(prefix, "-") {
prefix += "-"
}
fs.Var(&m.emulationVersion, prefix+"emulated-version", ""+
"The version the K8s component emulates its capabilities (APIs, features, ...) of.\n"+
"If set, the component will emulate the behavior of this version instead of the underlying binary version.\n"+
"Any capabilities present in the binary version that were introduced after the emulated version will be unavailable and any capabilities removed after the emulated version will be available.\n"+
"This flag applies only to component capabilities, and does not disable bug fixes and performance improvements present in the binary version.\n"+
"Defaults to the binary version. The value should be between 1.{binaryMinorVersion-1} and 1.{binaryMinorVersion}.\n"+
"Format could only be major.minor")
}
func NewEffectiveVersion(binaryVer string) MutableEffectiveVersion {
effective := &effectiveVersion{} effective := &effectiveVersion{}
binaryVersion := version.MustParse(binaryVer)
compatVersion := binaryVersion.SubtractMinor(1) compatVersion := binaryVersion.SubtractMinor(1)
effective.Set(binaryVersion, binaryVersion, compatVersion) effective.Set(binaryVersion, binaryVersion, compatVersion)
return effective return effective
} }
func NewEffectiveVersion(binaryVer string) MutableEffectiveVersion {
if binaryVer == "" {
return &effectiveVersion{}
}
binaryVersion := version.MustParse(binaryVer)
return newEffectiveVersion(binaryVersion)
}
// DefaultBuildEffectiveVersion returns the MutableEffectiveVersion based on the // DefaultBuildEffectiveVersion returns the MutableEffectiveVersion based on the
// current build information. // current build information.
func DefaultBuildEffectiveVersion() MutableEffectiveVersion { func DefaultBuildEffectiveVersion() MutableEffectiveVersion {
verInfo := baseversion.Get() verInfo := baseversion.Get()
ver := NewEffectiveVersion(verInfo.String()) binaryVersion := version.MustParse(verInfo.String()).WithInfo(verInfo)
if ver.BinaryVersion().Major() == 0 && ver.BinaryVersion().Minor() == 0 { if binaryVersion.Major() == 0 && binaryVersion.Minor() == 0 {
ver = DefaultKubeEffectiveVersion() return DefaultKubeEffectiveVersion()
} }
return ver return newEffectiveVersion(binaryVersion)
} }
// DefaultKubeEffectiveVersion returns the MutableEffectiveVersion based on the // DefaultKubeEffectiveVersion returns the MutableEffectiveVersion based on the
// latest K8s release. // latest K8s release.
// Should update for each minor release! // Should update for each minor release!
func DefaultKubeEffectiveVersion() MutableEffectiveVersion { func DefaultKubeEffectiveVersion() MutableEffectiveVersion {
return NewEffectiveVersion("1.31") binaryVersion := version.MustParse("1.31").WithInfo(baseversion.Get())
return newEffectiveVersion(binaryVersion)
} }

View File

@ -17,11 +17,8 @@ limitations under the License.
package version package version
import ( import (
"fmt"
"strings"
"testing" "testing"
"github.com/spf13/pflag"
"k8s.io/apimachinery/pkg/util/version" "k8s.io/apimachinery/pkg/util/version"
) )
@ -127,54 +124,3 @@ func TestValidate(t *testing.T) {
}) })
} }
} }
func TestEffectiveVersionsFlag(t *testing.T) {
tests := []struct {
name string
emulationVersion string
expectedEmulationVersion *version.Version
parseError string
}{
{
name: "major.minor ok",
emulationVersion: "1.30",
expectedEmulationVersion: version.MajorMinor(1, 30),
},
{
name: "v prefix ok",
emulationVersion: "v1.30",
expectedEmulationVersion: version.MajorMinor(1, 30),
},
{
name: "semantic version not ok",
emulationVersion: "1.30.1",
parseError: "version 1.30.1 is not in the format of major.minor",
},
{
name: "invalid version",
emulationVersion: "1.foo",
parseError: "illegal version string",
},
}
for i, test := range tests {
t.Run(test.name, func(t *testing.T) {
fs := pflag.NewFlagSet("testflag", pflag.ContinueOnError)
effective := NewEffectiveVersion("1.30")
effective.AddFlags(fs, "test")
err := fs.Parse([]string{fmt.Sprintf("--test-emulated-version=%s", test.emulationVersion)})
if test.parseError != "" {
if !strings.Contains(err.Error(), test.parseError) {
t.Fatalf("%d: Parse() Expected %v, Got %v", i, test.parseError, err)
}
return
}
if err != nil {
t.Fatalf("%d: Parse() Expected nil, Got %v", i, err)
}
if !effective.EmulationVersion().EqualTo(test.expectedEmulationVersion) {
t.Errorf("%d: EmulationVersion Expected %s, Got %s", i, test.expectedEmulationVersion.String(), effective.EmulationVersion().String())
}
})
}
}

View File

@ -127,6 +127,9 @@ type MutableFeatureGate interface {
AddFlag(fs *pflag.FlagSet) AddFlag(fs *pflag.FlagSet)
// Close sets closed to true, and prevents subsequent calls to Add // Close sets closed to true, and prevents subsequent calls to Add
Close() Close()
// OpenForModification sets closedForModification to false, and allows subsequent calls to SetEmulationVersion to change enabled features
// before the next Enabled is called.
OpenForModification()
// Set parses and stores flag gates for known features // Set parses and stores flag gates for known features
// from a string like feature1=true,feature2=false,... // from a string like feature1=true,feature2=false,...
Set(value string) error Set(value string) error
@ -163,11 +166,25 @@ type MutableVersionedFeatureGate interface {
// SetEmulationVersion overrides the emulationVersion of the feature gate. // SetEmulationVersion overrides the emulationVersion of the feature gate.
// Otherwise, the emulationVersion will be the same as the binary version. // Otherwise, the emulationVersion will be the same as the binary version.
// If set, the feature defaults and availability will be as if the binary is at the emulated version. // If set, the feature defaults and availability will be as if the binary is at the emulated version.
// Returns error if the new emulationVersion will change the enablement state of a feature that has already been queried.
// If you have to use featureGate.Enabled before parsing the flags, call featureGate.OpenForModification following featureGate.Enabled.
SetEmulationVersion(emulationVersion *version.Version) error SetEmulationVersion(emulationVersion *version.Version) error
// GetAll returns a copy of the map of known feature names to versioned feature specs. // GetAll returns a copy of the map of known feature names to versioned feature specs.
GetAllVersioned() map[Feature]VersionedSpecs GetAllVersioned() map[Feature]VersionedSpecs
// AddVersioned adds versioned feature specs to the featureGate. // AddVersioned adds versioned feature specs to the featureGate.
AddVersioned(features map[Feature]VersionedSpecs) error AddVersioned(features map[Feature]VersionedSpecs) error
// OverrideDefaultAtVersion sets a local override for the registered default value of a named
// feature for the prerelease lifecycle the given version is at.
// If the feature has not been previously registered (e.g. by a call to Add),
// has a locked default, or if the gate has already registered itself with a FlagSet, a non-nil
// error is returned.
//
// When two or more components consume a common feature, one component can override its
// default at runtime in order to adopt new defaults before or after the other
// components. For example, a new feature can be evaluated with a limited blast radius by
// overriding its default to true for a limited number of components without simultaneously
// changing its default for all consuming components.
OverrideDefaultAtVersion(name Feature, override bool, ver *version.Version) error
} }
// MutableVersionedFeatureGateForTests is a feature gate interface that should only be used in tests. // MutableVersionedFeatureGateForTests is a feature gate interface that should only be used in tests.
@ -197,6 +214,13 @@ type featureGate struct {
enabledRaw atomic.Value enabledRaw atomic.Value
// closed is set to true when AddFlag is called, and prevents subsequent calls to Add // closed is set to true when AddFlag is called, and prevents subsequent calls to Add
closed bool closed bool
// closedForModification is set to true when Enabled is called, and prevents subsequent calls to SetEmulationVersion to change the enabled features.
// TODO: after all feature gates have migrated to versioned feature gates,
// closedForModification should also prevents subsequent calls to Set and SetFromMap to change the enabled features
closedForModification atomic.Bool
// queriedFeatures stores all the features that have been queried through the Enabled interface.
// It is reset when closedForModification is reset.
queriedFeatures atomic.Value
emulationVersion atomic.Pointer[version.Version] emulationVersion atomic.Pointer[version.Version]
} }
@ -205,8 +229,8 @@ func setUnsetAlphaGates(known map[Feature]VersionedSpecs, enabled map[Feature]bo
if k == "AllAlpha" || k == "AllBeta" { if k == "AllAlpha" || k == "AllBeta" {
continue continue
} }
currentVersion := getCurrentVersion(v, cVer) featureSpec := featureSpecAtEmulationVersion(v, cVer)
if currentVersion.PreRelease == Alpha { if featureSpec.PreRelease == Alpha {
if _, found := enabled[k]; !found { if _, found := enabled[k]; !found {
enabled[k] = val enabled[k] = val
} }
@ -219,8 +243,8 @@ func setUnsetBetaGates(known map[Feature]VersionedSpecs, enabled map[Feature]boo
if k == "AllAlpha" || k == "AllBeta" { if k == "AllAlpha" || k == "AllBeta" {
continue continue
} }
currentVersion := getCurrentVersion(v, cVer) featureSpec := featureSpecAtEmulationVersion(v, cVer)
if currentVersion.PreRelease == Beta { if featureSpec.PreRelease == Beta {
if _, found := enabled[k]; !found { if _, found := enabled[k]; !found {
enabled[k] = val enabled[k] = val
} }
@ -251,6 +275,7 @@ func NewVersionedFeatureGate(emulationVersion *version.Version) *featureGate {
f.enabled.Store(map[Feature]bool{}) f.enabled.Store(map[Feature]bool{})
f.enabledRaw.Store(map[string]bool{}) f.enabledRaw.Store(map[string]bool{})
f.emulationVersion.Store(emulationVersion) f.emulationVersion.Store(emulationVersion)
f.queriedFeatures.Store(map[Feature]struct{}{})
klog.V(1).Infof("new feature gate with emulationVersion=%s", f.emulationVersion.Load().String()) klog.V(1).Infof("new feature gate with emulationVersion=%s", f.emulationVersion.Load().String())
return f return f
} }
@ -291,11 +316,11 @@ func (f *featureGate) Validate() []error {
return []error{fmt.Errorf("cannot cast enabledRaw to map[string]bool")} return []error{fmt.Errorf("cannot cast enabledRaw to map[string]bool")}
} }
enabled := map[Feature]bool{} enabled := map[Feature]bool{}
return f.unsafeSetFromMap(enabled, m) return f.unsafeSetFromMap(enabled, m, f.EmulationVersion())
} }
// unsafeSetFromMap stores flag gates for known features from a map[string]bool into an enabled map. // unsafeSetFromMap stores flag gates for known features from a map[string]bool into an enabled map.
func (f *featureGate) unsafeSetFromMap(enabled map[Feature]bool, m map[string]bool) []error { func (f *featureGate) unsafeSetFromMap(enabled map[Feature]bool, m map[string]bool, emulationVersion *version.Version) []error {
var errs []error var errs []error
// Copy existing state // Copy existing state
known := map[Feature]VersionedSpecs{} known := map[Feature]VersionedSpecs{}
@ -312,26 +337,26 @@ func (f *featureGate) unsafeSetFromMap(enabled map[Feature]bool, m map[string]bo
errs = append(errs, fmt.Errorf("unrecognized feature gate: %s", k)) errs = append(errs, fmt.Errorf("unrecognized feature gate: %s", k))
return errs return errs
} }
currentVersion := f.getCurrentVersion(versionedSpecs) featureSpec := featureSpecAtEmulationVersion(versionedSpecs, emulationVersion)
if currentVersion.LockToDefault && currentVersion.Default != v { if featureSpec.LockToDefault && featureSpec.Default != v {
errs = append(errs, fmt.Errorf("cannot set feature gate %v to %v, feature is locked to %v", k, v, currentVersion.Default)) errs = append(errs, fmt.Errorf("cannot set feature gate %v to %v, feature is locked to %v", k, v, featureSpec.Default))
continue continue
} }
// Handle "special" features like "all alpha gates" // Handle "special" features like "all alpha gates"
if fn, found := f.special[key]; found { if fn, found := f.special[key]; found {
fn(known, enabled, v, f.emulationVersion.Load()) fn(known, enabled, v, emulationVersion)
enabled[key] = v enabled[key] = v
continue continue
} }
if currentVersion.PreRelease == PreAlpha { if featureSpec.PreRelease == PreAlpha {
errs = append(errs, fmt.Errorf("cannot set feature gate %v to %v, feature is PreAlpha at emulated version %s", k, v, f.EmulationVersion().String())) errs = append(errs, fmt.Errorf("cannot set feature gate %v to %v, feature is PreAlpha at emulated version %s", k, v, emulationVersion.String()))
continue continue
} }
enabled[key] = v enabled[key] = v
if currentVersion.PreRelease == Deprecated { if featureSpec.PreRelease == Deprecated {
klog.Warningf("Setting deprecated feature gate %s=%t. It will be removed in a future release.", k, v) klog.Warningf("Setting deprecated feature gate %s=%t. It will be removed in a future release.", k, v)
} else if currentVersion.PreRelease == GA { } else if featureSpec.PreRelease == GA {
klog.Warningf("Setting GA feature gate %s=%t. It will be removed in a future release.", k, v) klog.Warningf("Setting GA feature gate %s=%t. It will be removed in a future release.", k, v)
} }
} }
@ -361,7 +386,7 @@ func (f *featureGate) SetFromMap(m map[string]bool) error {
} }
f.enabledRaw.Store(enabledRaw) f.enabledRaw.Store(enabledRaw)
errs := f.unsafeSetFromMap(enabled, enabledRaw) errs := f.unsafeSetFromMap(enabled, enabledRaw, f.EmulationVersion())
if len(errs) == 0 { if len(errs) == 0 {
// Persist changes // Persist changes
f.enabled.Store(enabled) f.enabled.Store(enabled)
@ -429,6 +454,10 @@ func (f *featureGate) AddVersioned(features map[Feature]VersionedSpecs) error {
} }
func (f *featureGate) OverrideDefault(name Feature, override bool) error { func (f *featureGate) OverrideDefault(name Feature, override bool) error {
return f.OverrideDefaultAtVersion(name, override, f.EmulationVersion())
}
func (f *featureGate) OverrideDefaultAtVersion(name Feature, override bool, ver *version.Version) error {
f.lock.Lock() f.lock.Lock()
defer f.lock.Unlock() defer f.lock.Unlock()
@ -446,12 +475,12 @@ func (f *featureGate) OverrideDefault(name Feature, override bool) error {
if !ok { if !ok {
return fmt.Errorf("cannot override default: feature %q is not registered", name) return fmt.Errorf("cannot override default: feature %q is not registered", name)
} }
spec := f.getCurrentVersion(specs) spec := featureSpecAtEmulationVersion(specs, ver)
switch { switch {
case spec.LockToDefault: case spec.LockToDefault:
return fmt.Errorf("cannot override default: feature %q default is locked to %t", name, spec.Default) return fmt.Errorf("cannot override default: feature %q default is locked to %t", name, spec.Default)
case spec.PreRelease == PreAlpha: case spec.PreRelease == PreAlpha:
return fmt.Errorf("cannot override default: feature %q is not available before emulation version %s", name, f.EmulationVersion().String()) return fmt.Errorf("cannot override default: feature %q is not available before version %s", name, ver.String())
case spec.PreRelease == Deprecated: case spec.PreRelease == Deprecated:
klog.Warningf("Overriding default of deprecated feature gate %s=%t. It will be removed in a future release.", name, override) klog.Warningf("Overriding default of deprecated feature gate %s=%t. It will be removed in a future release.", name, override)
case spec.PreRelease == GA: case spec.PreRelease == GA:
@ -469,12 +498,12 @@ func (f *featureGate) OverrideDefault(name Feature, override bool) error {
func (f *featureGate) GetAll() map[Feature]FeatureSpec { func (f *featureGate) GetAll() map[Feature]FeatureSpec {
retval := map[Feature]FeatureSpec{} retval := map[Feature]FeatureSpec{}
for k, v := range f.GetAllVersioned() { for k, v := range f.GetAllVersioned() {
spec := f.getCurrentVersion(v) spec := f.featureSpecAtEmulationVersion(v)
if spec.PreRelease == PreAlpha { if spec.PreRelease == PreAlpha {
// The feature is not available at the emulation version. // The feature is not available at the emulation version.
continue continue
} }
retval[k] = *f.getCurrentVersion(v) retval[k] = *f.featureSpecAtEmulationVersion(v)
} }
return retval return retval
} }
@ -492,7 +521,6 @@ func (f *featureGate) SetEmulationVersion(emulationVersion *version.Version) err
f.lock.Lock() f.lock.Lock()
defer f.lock.Unlock() defer f.lock.Unlock()
klog.V(1).Infof("set feature gate emulationVersion to %s", emulationVersion.String()) klog.V(1).Infof("set feature gate emulationVersion to %s", emulationVersion.String())
f.emulationVersion.Store(emulationVersion)
// Copy existing state // Copy existing state
enabledRaw := map[string]bool{} enabledRaw := map[string]bool{}
@ -501,10 +529,24 @@ func (f *featureGate) SetEmulationVersion(emulationVersion *version.Version) err
} }
// enabled map should be reset whenever emulationVersion is changed. // enabled map should be reset whenever emulationVersion is changed.
enabled := map[Feature]bool{} enabled := map[Feature]bool{}
errs := f.unsafeSetFromMap(enabled, enabledRaw) errs := f.unsafeSetFromMap(enabled, enabledRaw, emulationVersion)
if f.closedForModification.Load() {
queriedFeatures := f.queriedFeatures.Load().(map[Feature]struct{})
known := f.known.Load().(map[Feature]VersionedSpecs)
for feature := range queriedFeatures {
newVal := featureEnabled(feature, enabled, known, emulationVersion)
oldVal := f.Enabled(feature)
// it is ok to modify emulation version if it does not result in feature enablemennt change for features that have already been queried.
if newVal != oldVal {
errs = append(errs, fmt.Errorf("SetEmulationVersion will change already queried feature:%s from %v to %v\ncall featureGate.OpenForModification() first to override", feature, oldVal, newVal))
}
}
}
if len(errs) == 0 { if len(errs) == 0 {
// Persist changes // Persist changes
f.enabled.Store(enabled) f.enabled.Store(enabled)
f.emulationVersion.Store(emulationVersion)
} }
return utilerrors.NewAggregate(errs) return utilerrors.NewAggregate(errs)
} }
@ -514,32 +556,49 @@ func (f *featureGate) EmulationVersion() *version.Version {
} }
// FeatureSpec returns the FeatureSpec at the EmulationVersion if the key exists, an error otherwise. // FeatureSpec returns the FeatureSpec at the EmulationVersion if the key exists, an error otherwise.
// This is useful to keep multiple implementations of a feature based on the PreRelease or Version info.
func (f *featureGate) FeatureSpec(key Feature) (FeatureSpec, error) { func (f *featureGate) FeatureSpec(key Feature) (FeatureSpec, error) {
if v, ok := f.known.Load().(map[Feature]VersionedSpecs)[key]; ok { if v, ok := f.known.Load().(map[Feature]VersionedSpecs)[key]; ok {
currentVersion := f.getCurrentVersion(v) featureSpec := f.featureSpecAtEmulationVersion(v)
return *currentVersion, nil return *featureSpec, nil
} }
return FeatureSpec{}, fmt.Errorf("feature %q is not registered in FeatureGate %q", key, f.featureGateName) return FeatureSpec{}, fmt.Errorf("feature %q is not registered in FeatureGate %q", key, f.featureGateName)
} }
// Enabled returns true if the key is enabled. If the key is not known, this call will panic. func (f *featureGate) recordQueried(key Feature) {
func (f *featureGate) Enabled(key Feature) bool { queriedFeatures := map[Feature]struct{}{}
// fallback to default behavior, since we don't have emulation version set for k := range f.queriedFeatures.Load().(map[Feature]struct{}) {
if v, ok := f.enabled.Load().(map[Feature]bool)[key]; ok { queriedFeatures[k] = struct{}{}
}
queriedFeatures[key] = struct{}{}
f.queriedFeatures.Store(queriedFeatures)
f.closedForModification.Store(true)
}
func featureEnabled(key Feature, enabled map[Feature]bool, known map[Feature]VersionedSpecs, emulationVersion *version.Version) bool {
// check explicitly set enabled list
if v, ok := enabled[key]; ok {
return v return v
} }
if v, ok := f.known.Load().(map[Feature]VersionedSpecs)[key]; ok { if v, ok := known[key]; ok {
return f.getCurrentVersion(v).Default return featureSpecAtEmulationVersion(v, emulationVersion).Default
} }
panic(fmt.Errorf("feature %q is not registered in FeatureGate %q", key, f.featureGateName)) panic(fmt.Errorf("feature %q is not registered in FeatureGate", key))
} }
func (f *featureGate) getCurrentVersion(v VersionedSpecs) *FeatureSpec { // Enabled returns true if the key is enabled. If the key is not known, this call will panic.
return getCurrentVersion(v, f.EmulationVersion()) func (f *featureGate) Enabled(key Feature) bool {
v := featureEnabled(key, f.enabled.Load().(map[Feature]bool), f.known.Load().(map[Feature]VersionedSpecs), f.EmulationVersion())
f.recordQueried(key)
return v
} }
func getCurrentVersion(v VersionedSpecs, emulationVersion *version.Version) *FeatureSpec { func (f *featureGate) featureSpecAtEmulationVersion(v VersionedSpecs) *FeatureSpec {
return featureSpecAtEmulationVersion(v, f.EmulationVersion())
}
func featureSpecAtEmulationVersion(v VersionedSpecs, emulationVersion *version.Version) *FeatureSpec {
i := len(v) - 1 i := len(v) - 1
for ; i >= 0; i-- { for ; i >= 0; i-- {
if v[i].Version.GreaterThan(emulationVersion) { if v[i].Version.GreaterThan(emulationVersion) {
@ -561,6 +620,18 @@ func (f *featureGate) Close() {
f.lock.Unlock() f.lock.Unlock()
} }
// OpenForModification sets closedForModification to false, and allows subsequent calls to SetEmulationVersion to change enabled features
// before the next Enabled is called.
func (f *featureGate) OpenForModification() {
queriedFeatures := []Feature{}
for feature := range f.queriedFeatures.Load().(map[Feature]struct{}) {
queriedFeatures = append(queriedFeatures, feature)
}
klog.Warningf("open feature gate for modification after querying features: %v.", queriedFeatures)
f.closedForModification.Store(false)
f.queriedFeatures.Store(map[Feature]struct{}{})
}
// AddFlag adds a flag for setting global feature gates to the specified FlagSet. // AddFlag adds a flag for setting global feature gates to the specified FlagSet.
func (f *featureGate) AddFlag(fs *pflag.FlagSet) { func (f *featureGate) AddFlag(fs *pflag.FlagSet) {
// TODO(mtaufen): Shouldn't we just close it on the first Set/SetFromMap instead? // TODO(mtaufen): Shouldn't we just close it on the first Set/SetFromMap instead?
@ -590,11 +661,11 @@ func (f *featureGate) KnownFeatures() []string {
known = append(known, fmt.Sprintf("%s=true|false (%s - default=%t)", k, v[0].PreRelease, v[0].Default)) known = append(known, fmt.Sprintf("%s=true|false (%s - default=%t)", k, v[0].PreRelease, v[0].Default))
continue continue
} }
currentV := f.getCurrentVersion(v) featureSpec := f.featureSpecAtEmulationVersion(v)
if currentV.PreRelease == GA || currentV.PreRelease == Deprecated || currentV.PreRelease == PreAlpha { if featureSpec.PreRelease == GA || featureSpec.PreRelease == Deprecated || featureSpec.PreRelease == PreAlpha {
continue continue
} }
known = append(known, fmt.Sprintf("%s=true|false (%s - default=%t)", k, currentV.PreRelease, currentV.Default)) known = append(known, fmt.Sprintf("%s=true|false (%s - default=%t)", k, featureSpec.PreRelease, featureSpec.Default))
} }
sort.Strings(known) sort.Strings(known)
return known return known
@ -620,7 +691,7 @@ func (f *featureGate) DeepCopy() MutableVersionedFeatureGate {
// Construct a new featureGate around the copied state. // Construct a new featureGate around the copied state.
// Note that specialFeatures is treated as immutable by convention, // Note that specialFeatures is treated as immutable by convention,
// and we maintain the value of f.closed across the copy. // and we maintain the value of f.closed across the copy, but resets closedForModification.
fg := &featureGate{ fg := &featureGate{
special: specialFeatures, special: specialFeatures,
closed: f.closed, closed: f.closed,
@ -629,6 +700,7 @@ func (f *featureGate) DeepCopy() MutableVersionedFeatureGate {
fg.known.Store(known) fg.known.Store(known)
fg.enabled.Store(enabled) fg.enabled.Store(enabled)
fg.enabledRaw.Store(enabledRaw) fg.enabledRaw.Store(enabledRaw)
fg.queriedFeatures.Store(map[Feature]struct{}{})
return fg return fg
} }
@ -636,9 +708,15 @@ func (f *featureGate) DeepCopy() MutableVersionedFeatureGate {
func (f *featureGate) Reset(m map[string]bool) { func (f *featureGate) Reset(m map[string]bool) {
enabled := map[Feature]bool{} enabled := map[Feature]bool{}
enabledRaw := map[string]bool{} enabledRaw := map[string]bool{}
queriedFeatures := map[Feature]struct{}{}
f.enabled.Store(enabled) f.enabled.Store(enabled)
f.enabledRaw.Store(enabledRaw) f.enabledRaw.Store(enabledRaw)
_ = f.SetFromMap(m) _ = f.SetFromMap(m)
f.closedForModification.Store(false)
f.queriedFeatures.Store(queriedFeatures)
f.lock.Lock()
defer f.lock.Unlock()
f.closed = false
} }
func (f *featureGate) EnabledRawMap() map[string]bool { func (f *featureGate) EnabledRawMap() map[string]bool {

View File

@ -1280,6 +1280,48 @@ func TestVersionedFeatureGateOverrideDefault(t *testing.T) {
} }
}) })
t.Run("overrides at specific version take effect", func(t *testing.T) {
f := NewVersionedFeatureGate(version.MustParse("1.29"))
require.NoError(t, f.SetEmulationVersion(version.MustParse("1.28")))
if err := f.AddVersioned(map[Feature]VersionedSpecs{
"TestFeature1": {
{Version: version.MustParse("1.28"), Default: true},
},
"TestFeature2": {
{Version: version.MustParse("1.26"), Default: false},
{Version: version.MustParse("1.29"), Default: false},
},
}); err != nil {
t.Fatal(err)
}
if f.OverrideDefaultAtVersion("TestFeature1", false, version.MustParse("1.27")) == nil {
t.Error("expected error when attempting to override the default for a feature not available at given version")
}
require.NoError(t, f.OverrideDefaultAtVersion("TestFeature2", true, version.MustParse("1.27")))
if !f.Enabled("TestFeature1") {
t.Error("expected TestFeature1 to have effective default of true")
}
if !f.Enabled("TestFeature2") {
t.Error("expected TestFeature2 to have effective default of true")
}
f.OpenForModification()
require.NoError(t, f.SetEmulationVersion(version.MustParse("1.29")))
if !f.Enabled("TestFeature1") {
t.Error("expected TestFeature1 to have effective default of true")
}
if f.Enabled("TestFeature2") {
t.Error("expected TestFeature2 to have effective default of false")
}
f.OpenForModification()
require.NoError(t, f.SetEmulationVersion(version.MustParse("1.26")))
if f.Enabled("TestFeature1") {
t.Error("expected TestFeature1 to have effective default of false")
}
if !f.Enabled("TestFeature2") {
t.Error("expected TestFeature2 to have effective default of true")
}
})
t.Run("overrides are preserved across deep copies", func(t *testing.T) { t.Run("overrides are preserved across deep copies", func(t *testing.T) {
f := NewVersionedFeatureGate(version.MustParse("1.29")) f := NewVersionedFeatureGate(version.MustParse("1.29"))
require.NoError(t, f.SetEmulationVersion(version.MustParse("1.28"))) require.NoError(t, f.SetEmulationVersion(version.MustParse("1.28")))
@ -1433,7 +1475,7 @@ func TestVersionedFeatureGateOverrideDefault(t *testing.T) {
}) })
} }
func TestGetCurrentVersion(t *testing.T) { func TestFeatureSpecAtEmulationVersion(t *testing.T) {
specs := VersionedSpecs{{Version: version.MustParse("1.29"), Default: true, PreRelease: GA}, specs := VersionedSpecs{{Version: version.MustParse("1.29"), Default: true, PreRelease: GA},
{Version: version.MustParse("1.28"), Default: false, PreRelease: Beta}, {Version: version.MustParse("1.28"), Default: false, PreRelease: Beta},
{Version: version.MustParse("1.25"), Default: false, PreRelease: Alpha}, {Version: version.MustParse("1.25"), Default: false, PreRelease: Alpha},
@ -1469,11 +1511,41 @@ func TestGetCurrentVersion(t *testing.T) {
}, },
} }
for i, test := range tests { for i, test := range tests {
t.Run(fmt.Sprintf("getCurrentVersion for emulationVersion %s", test.cVersion), func(t *testing.T) { t.Run(fmt.Sprintf("featureSpecAtEmulationVersion for emulationVersion %s", test.cVersion), func(t *testing.T) {
result := getCurrentVersion(specs, version.MustParse(test.cVersion)) result := featureSpecAtEmulationVersion(specs, version.MustParse(test.cVersion))
if !reflect.DeepEqual(*result, test.expect) { if !reflect.DeepEqual(*result, test.expect) {
t.Errorf("%d: getCurrentVersion(, %s) Expected %v, Got %v", i, test.cVersion, test.expect, result) t.Errorf("%d: featureSpecAtEmulationVersion(, %s) Expected %v, Got %v", i, test.cVersion, test.expect, result)
} }
}) })
} }
} }
func TestOpenForModification(t *testing.T) {
const testBetaGate Feature = "testBetaGate"
f := NewVersionedFeatureGate(version.MustParse("1.29"))
err := f.AddVersioned(map[Feature]VersionedSpecs{
testBetaGate: {
{Version: version.MustParse("1.29"), Default: true, PreRelease: Beta},
{Version: version.MustParse("1.28"), Default: false, PreRelease: Beta},
{Version: version.MustParse("1.26"), Default: false, PreRelease: Alpha},
},
})
require.NoError(t, err)
if f.Enabled(testBetaGate) != true {
t.Errorf("Expected true")
}
err = f.SetEmulationVersion(version.MustParse("1.28"))
if err == nil {
t.Fatalf("Expected error when SetEmulationVersion after querying features")
}
if f.Enabled(testBetaGate) != true {
t.Errorf("Expected true")
}
f.OpenForModification()
require.NoError(t, f.SetEmulationVersion(version.MustParse("1.28")))
if f.Enabled(testBetaGate) != false {
t.Errorf("Expected false at 1.28")
}
}

View File

@ -38,7 +38,6 @@ import (
"k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes"
"k8s.io/client-go/transport" "k8s.io/client-go/transport"
"k8s.io/component-base/tracing" "k8s.io/component-base/tracing"
"k8s.io/component-base/version"
v1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1" v1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
v1helper "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1/helper" v1helper "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1/helper"
"k8s.io/kube-aggregator/pkg/apis/apiregistration/v1beta1" "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1beta1"
@ -185,8 +184,6 @@ func (cfg *Config) Complete() CompletedConfig {
// the kube aggregator wires its own discovery mechanism // the kube aggregator wires its own discovery mechanism
// TODO eventually collapse this by extracting all of the discovery out // TODO eventually collapse this by extracting all of the discovery out
c.GenericConfig.EnableDiscovery = false c.GenericConfig.EnableDiscovery = false
version := version.Get()
c.GenericConfig.Version = &version
return CompletedConfig{&c} return CompletedConfig{&c}
} }

View File

@ -17,17 +17,13 @@ limitations under the License.
package apiserver package apiserver
import ( import (
"strconv"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/version"
"k8s.io/apiserver/pkg/registry/rest" "k8s.io/apiserver/pkg/registry/rest"
genericapiserver "k8s.io/apiserver/pkg/server" genericapiserver "k8s.io/apiserver/pkg/server"
utilversion "k8s.io/apiserver/pkg/util/version"
"k8s.io/sample-apiserver/pkg/apis/wardle" "k8s.io/sample-apiserver/pkg/apis/wardle"
"k8s.io/sample-apiserver/pkg/apis/wardle/install" "k8s.io/sample-apiserver/pkg/apis/wardle/install"
wardleregistry "k8s.io/sample-apiserver/pkg/registry" wardleregistry "k8s.io/sample-apiserver/pkg/registry"
@ -94,11 +90,6 @@ func (cfg *Config) Complete() CompletedConfig {
cfg.GenericConfig.Complete(), cfg.GenericConfig.Complete(),
&cfg.ExtraConfig, &cfg.ExtraConfig,
} }
wardleEffectiveVersion := utilversion.DefaultComponentGlobalsRegistry.EffectiveVersionFor(WardleComponentName)
c.GenericConfig.Version = &version.Info{
Major: strconv.Itoa(int(wardleEffectiveVersion.BinaryVersion().Major())),
Minor: strconv.Itoa(int(wardleEffectiveVersion.BinaryVersion().Minor())),
}
return CompletedConfig{&c} return CompletedConfig{&c}
} }

View File

@ -59,23 +59,15 @@ type WardleServerOptions struct {
AlternateDNS []string AlternateDNS []string
} }
func mapWardleEffectiveVersionToKubeEffectiveVersion(registry utilversion.ComponentGlobalsRegistry) error { func wardleEmulationVersionToKubeEmulationVersion(ver *version.Version) *version.Version {
wardleVer := registry.EffectiveVersionFor(apiserver.WardleComponentName) if ver.Major() != 1 {
kubeVer := registry.EffectiveVersionFor(utilversion.DefaultKubeComponent).(utilversion.MutableEffectiveVersion)
// map from wardle emulation version to kube emulation version.
emulationVersionMap := map[string]string{
"1.2": kubeVer.BinaryVersion().AddMinor(1).String(),
"1.1": kubeVer.BinaryVersion().String(),
"1.0": kubeVer.BinaryVersion().SubtractMinor(1).String(),
}
wardleEmulationVer := wardleVer.EmulationVersion()
if kubeEmulationVer, ok := emulationVersionMap[wardleEmulationVer.String()]; ok {
kubeVer.SetEmulationVersion(version.MustParse(kubeEmulationVer))
} else {
return fmt.Errorf("cannot find mapping from wardle emulation version: %s to kube version", wardleVer.EmulationVersion().String())
}
return nil return nil
} }
kubeVer := utilversion.DefaultKubeEffectiveVersion().BinaryVersion()
// "1.1" maps to kubeVer
offset := int(ver.Minor()) - 1
return kubeVer.OffsetMinor(offset)
}
// NewWardleServerOptions returns a new WardleServerOptions // NewWardleServerOptions returns a new WardleServerOptions
func NewWardleServerOptions(out, errOut io.Writer) *WardleServerOptions { func NewWardleServerOptions(out, errOut io.Writer) *WardleServerOptions {
@ -100,11 +92,7 @@ func NewCommandStartWardleServer(ctx context.Context, defaults *WardleServerOpti
Short: "Launch a wardle API server", Short: "Launch a wardle API server",
Long: "Launch a wardle API server", Long: "Launch a wardle API server",
PersistentPreRunE: func(*cobra.Command, []string) error { PersistentPreRunE: func(*cobra.Command, []string) error {
if err := utilversion.DefaultComponentGlobalsRegistry.Set(); err != nil { return utilversion.DefaultComponentGlobalsRegistry.Set()
return err
}
// convert wardle effective version to kube effective version to be used in generic api server, and set the generic api server feature gate.
return mapWardleEffectiveVersionToKubeEffectiveVersion(utilversion.DefaultComponentGlobalsRegistry)
}, },
RunE: func(c *cobra.Command, args []string) error { RunE: func(c *cobra.Command, args []string) error {
if err := o.Complete(); err != nil { if err := o.Complete(); err != nil {
@ -133,10 +121,10 @@ func NewCommandStartWardleServer(ctx context.Context, defaults *WardleServerOpti
{Version: version.MustParse("1.0"), Default: false, PreRelease: featuregate.Alpha}, {Version: version.MustParse("1.0"), Default: false, PreRelease: featuregate.Alpha},
}, },
})) }))
utilruntime.Must(utilversion.DefaultComponentGlobalsRegistry.Register(apiserver.WardleComponentName, wardleEffectiveVersion, wardleFeatureGate, false)) utilruntime.Must(utilversion.DefaultComponentGlobalsRegistry.Register(apiserver.WardleComponentName, wardleEffectiveVersion, wardleFeatureGate))
_, _ = utilversion.DefaultComponentGlobalsRegistry.ComponentGlobalsOrRegister( _, _ = utilversion.DefaultComponentGlobalsRegistry.ComponentGlobalsOrRegister(
utilversion.DefaultKubeComponent, utilversion.DefaultKubeEffectiveVersion(), utilfeature.DefaultMutableFeatureGate) utilversion.DefaultKubeComponent, utilversion.DefaultKubeEffectiveVersion(), utilfeature.DefaultMutableFeatureGate)
utilruntime.Must(utilversion.DefaultComponentGlobalsRegistry.SetEmulationVersionMapping(apiserver.WardleComponentName, utilversion.DefaultKubeComponent, wardleEmulationVersionToKubeEmulationVersion))
utilversion.DefaultComponentGlobalsRegistry.AddFlags(flags) utilversion.DefaultComponentGlobalsRegistry.AddFlags(flags)
return cmd return cmd
@ -189,8 +177,8 @@ func (o *WardleServerOptions) Config() (*apiserver.Config, error) {
serverConfig.OpenAPIV3Config.Info.Title = "Wardle" serverConfig.OpenAPIV3Config.Info.Title = "Wardle"
serverConfig.OpenAPIV3Config.Info.Version = "0.1" serverConfig.OpenAPIV3Config.Info.Version = "0.1"
serverConfig.FeatureGate = utilversion.DefaultComponentGlobalsRegistry.FeatureGateFor(utilversion.DefaultKubeComponent) serverConfig.FeatureGate = utilversion.DefaultComponentGlobalsRegistry.FeatureGateFor(apiserver.WardleComponentName)
serverConfig.EffectiveVersion = utilversion.DefaultComponentGlobalsRegistry.EffectiveVersionFor(utilversion.DefaultKubeComponent) serverConfig.EffectiveVersion = utilversion.DefaultComponentGlobalsRegistry.EffectiveVersionFor(apiserver.WardleComponentName)
if err := o.RecommendedOptions.ApplyTo(serverConfig); err != nil { if err := o.RecommendedOptions.ApplyTo(serverConfig); err != nil {
return nil, err return nil, err

View File

@ -21,13 +21,11 @@ import (
"k8s.io/apimachinery/pkg/util/version" "k8s.io/apimachinery/pkg/util/version"
utilversion "k8s.io/apiserver/pkg/util/version" utilversion "k8s.io/apiserver/pkg/util/version"
"k8s.io/sample-apiserver/pkg/apiserver"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestMapBinaryEffectiveVersionToKubeEffectiveVersion(t *testing.T) { func TestWardleEmulationVersionToKubeEmulationVersion(t *testing.T) {
wardleEffectiveVersion := utilversion.NewEffectiveVersion("1.2")
defaultKubeEffectiveVersion := utilversion.DefaultKubeEffectiveVersion() defaultKubeEffectiveVersion := utilversion.DefaultKubeEffectiveVersion()
testCases := []struct { testCases := []struct {
@ -35,32 +33,26 @@ func TestMapBinaryEffectiveVersionToKubeEffectiveVersion(t *testing.T) {
wardleEmulationVer *version.Version wardleEmulationVer *version.Version
expectedKubeEmulationVer *version.Version expectedKubeEmulationVer *version.Version
}{ }{
{
desc: "same version as than kube binary",
wardleEmulationVer: version.MajorMinor(1, 1),
expectedKubeEmulationVer: defaultKubeEffectiveVersion.BinaryVersion(),
},
{ {
desc: "1 version higher than kube binary", desc: "1 version higher than kube binary",
wardleEmulationVer: version.MajorMinor(1, 2), wardleEmulationVer: version.MajorMinor(1, 2),
expectedKubeEmulationVer: defaultKubeEffectiveVersion.BinaryVersion().AddMinor(1), expectedKubeEmulationVer: defaultKubeEffectiveVersion.BinaryVersion().OffsetMinor(1),
}, },
{ {
desc: "no mapping", desc: "no mapping",
wardleEmulationVer: version.MajorMinor(1, 10), wardleEmulationVer: version.MajorMinor(2, 10),
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) { t.Run(tc.desc, func(t *testing.T) {
registry := utilversion.NewComponentGlobalsRegistry() mappedKubeEmulationVer := wardleEmulationVersionToKubeEmulationVersion(tc.wardleEmulationVer)
_ = registry.Register(apiserver.WardleComponentName, wardleEffectiveVersion, nil, true) assert.True(t, mappedKubeEmulationVer.EqualTo(tc.expectedKubeEmulationVer))
_ = registry.Register(utilversion.DefaultKubeComponent, defaultKubeEffectiveVersion, nil, true)
wardleEffectiveVersion.SetEmulationVersion(tc.wardleEmulationVer)
err := mapWardleEffectiveVersionToKubeEffectiveVersion(registry)
if tc.expectedKubeEmulationVer == nil {
if err == nil {
t.Fatal("expected error, no error found")
}
} else {
assert.True(t, registry.EffectiveVersionFor(utilversion.DefaultKubeComponent).EmulationVersion().EqualTo(tc.expectedKubeEmulationVer))
}
}) })
} }
} }

View File

@ -56,6 +56,7 @@ import (
"k8s.io/apimachinery/pkg/watch" "k8s.io/apimachinery/pkg/watch"
"k8s.io/apiserver/pkg/endpoints/handlers" "k8s.io/apiserver/pkg/endpoints/handlers"
"k8s.io/apiserver/pkg/storage/storagebackend" "k8s.io/apiserver/pkg/storage/storagebackend"
utilversion "k8s.io/apiserver/pkg/util/version"
"k8s.io/client-go/discovery/cached/memory" "k8s.io/client-go/discovery/cached/memory"
"k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic"
clientset "k8s.io/client-go/kubernetes" clientset "k8s.io/client-go/kubernetes"
@ -3103,6 +3104,48 @@ func TestEmulatedStorageVersion(t *testing.T) {
} }
} }
// TestAllowedEmulationVersions tests the TestServer can start without problem for all allowed emulation versions.
func TestAllowedEmulationVersions(t *testing.T) {
tcs := []struct {
name string
emulationVersion string
}{
{
name: "default",
emulationVersion: utilversion.DefaultKubeEffectiveVersion().EmulationVersion().String(),
},
}
for _, tc := range tcs {
t.Run(tc.emulationVersion, func(t *testing.T) {
server := kubeapiservertesting.StartTestServerOrDie(t, nil,
[]string{fmt.Sprintf("--emulated-version=kube=%s", tc.emulationVersion)}, framework.SharedEtcd())
defer server.TearDownFn()
rt, err := restclient.TransportFor(server.ClientConfig)
if err != nil {
t.Fatal(err)
}
req, err := http.NewRequest("GET", server.ClientConfig.Host+"/", nil)
if err != nil {
t.Fatal(err)
}
resp, err := rt.RoundTrip(req)
if err != nil {
t.Fatal(err)
}
expectedStatusCode := 200
if resp.StatusCode != expectedStatusCode {
t.Errorf("expect status code: %d, got : %d\n", expectedStatusCode, resp.StatusCode)
}
defer func() {
_ = resp.Body.Close()
}()
})
}
}
func TestEnableEmulationVersion(t *testing.T) { func TestEnableEmulationVersion(t *testing.T) {
server := kubeapiservertesting.StartTestServerOrDie(t, server := kubeapiservertesting.StartTestServerOrDie(t,
&kubeapiservertesting.TestServerInstanceOptions{BinaryVersion: "1.32"}, &kubeapiservertesting.TestServerInstanceOptions{BinaryVersion: "1.32"},

View File

@ -48,6 +48,7 @@ import (
clientset "k8s.io/client-go/kubernetes" clientset "k8s.io/client-go/kubernetes"
"k8s.io/utils/pointer" "k8s.io/utils/pointer"
utilversion "k8s.io/apiserver/pkg/util/version"
"k8s.io/component-base/version" "k8s.io/component-base/version"
kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing" kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
"k8s.io/kubernetes/pkg/api/legacyscheme" "k8s.io/kubernetes/pkg/api/legacyscheme"
@ -65,7 +66,12 @@ func TestClient(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }
if e, a := version.Get(), *info; !reflect.DeepEqual(e, a) { expectedInfo := version.Get()
kubeVersion := utilversion.DefaultKubeEffectiveVersion().BinaryVersion()
expectedInfo.Major = fmt.Sprintf("%d", kubeVersion.Major())
expectedInfo.Minor = fmt.Sprintf("%d", kubeVersion.Minor())
if e, a := expectedInfo, *info; !reflect.DeepEqual(e, a) {
t.Errorf("expected %#v, got %#v", e, a) t.Errorf("expected %#v, got %#v", e, a)
} }

View File

@ -32,11 +32,9 @@ import (
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/util/wait"
utilversion "k8s.io/apiserver/pkg/util/version"
"k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic"
apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1" apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
apiregistrationclient "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/typed/apiregistration/v1" apiregistrationclient "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/typed/apiregistration/v1"
"k8s.io/kubernetes/cmd/kube-apiserver/app/options"
) )
// TestOverlappingBuiltInResources ensures the list of group-resources the custom resource finalizer should skip is up to date // TestOverlappingBuiltInResources ensures the list of group-resources the custom resource finalizer should skip is up to date
@ -71,9 +69,7 @@ func TestOverlappingBuiltInResources(t *testing.T) {
// TestOverlappingCustomResourceAPIService ensures creating and deleting a custom resource overlapping with APIServices does not destroy APIService data // TestOverlappingCustomResourceAPIService ensures creating and deleting a custom resource overlapping with APIServices does not destroy APIService data
func TestOverlappingCustomResourceAPIService(t *testing.T) { func TestOverlappingCustomResourceAPIService(t *testing.T) {
apiServer := StartRealAPIServerOrDie(t, func(opts *options.ServerRunOptions) { apiServer := StartRealAPIServerOrDie(t)
opts.GenericServerRunOptions.EffectiveVersion = utilversion.NewEffectiveVersion("1.30")
})
defer apiServer.Cleanup() defer apiServer.Cleanup()
apiServiceClient, err := apiregistrationclient.NewForConfig(apiServer.Config) apiServiceClient, err := apiregistrationclient.NewForConfig(apiServer.Config)
@ -235,9 +231,7 @@ func TestOverlappingCustomResourceAPIService(t *testing.T) {
// TestOverlappingCustomResourceCustomResourceDefinition ensures creating and deleting a custom resource overlapping with CustomResourceDefinition does not destroy CustomResourceDefinition data // TestOverlappingCustomResourceCustomResourceDefinition ensures creating and deleting a custom resource overlapping with CustomResourceDefinition does not destroy CustomResourceDefinition data
func TestOverlappingCustomResourceCustomResourceDefinition(t *testing.T) { func TestOverlappingCustomResourceCustomResourceDefinition(t *testing.T) {
apiServer := StartRealAPIServerOrDie(t, func(opts *options.ServerRunOptions) { apiServer := StartRealAPIServerOrDie(t)
opts.GenericServerRunOptions.EffectiveVersion = utilversion.NewEffectiveVersion("1.30")
})
defer apiServer.Cleanup() defer apiServer.Cleanup()
crdClient, err := crdclient.NewForConfig(apiServer.Config) crdClient, err := crdclient.NewForConfig(apiServer.Config)

View File

@ -39,7 +39,7 @@ import (
func TestCrossGroupStorage(t *testing.T) { func TestCrossGroupStorage(t *testing.T) {
apiServer := StartRealAPIServerOrDie(t, func(opts *options.ServerRunOptions) { apiServer := StartRealAPIServerOrDie(t, func(opts *options.ServerRunOptions) {
// force enable all resources so we can check storage. // force enable all resources so we can check storage.
opts.GenericServerRunOptions.EffectiveVersion = utilversion.NewEffectiveVersion("1.30") opts.GenericServerRunOptions.EffectiveVersion = utilversion.NewEffectiveVersion("0.0")
}) })
defer apiServer.Cleanup() defer apiServer.Cleanup()

View File

@ -226,6 +226,18 @@ func TestAPIServiceWaitOnStart(t *testing.T) {
} }
func TestAggregatedAPIServer(t *testing.T) { func TestAggregatedAPIServer(t *testing.T) {
t.Run("WithoutWardleFeatureGateAtV1.2", func(t *testing.T) {
testAggregatedAPIServer(t, false, "1.2")
})
t.Run("WithoutWardleFeatureGateAtV1.1", func(t *testing.T) {
testAggregatedAPIServer(t, false, "1.1")
})
t.Run("WithWardleFeatureGateAtV1.1", func(t *testing.T) {
testAggregatedAPIServer(t, true, "1.1")
})
}
func testAggregatedAPIServer(t *testing.T, enableWardleFeatureGate bool, emulationVersion string) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
t.Cleanup(cancel) t.Cleanup(cancel)
@ -240,7 +252,7 @@ func TestAggregatedAPIServer(t *testing.T) {
// endpoints cannot have loopback IPs so we need to override the resolver itself // endpoints cannot have loopback IPs so we need to override the resolver itself
t.Cleanup(app.SetServiceResolverForTests(staticURLServiceResolver(fmt.Sprintf("https://127.0.0.1:%d", wardlePort)))) t.Cleanup(app.SetServiceResolverForTests(staticURLServiceResolver(fmt.Sprintf("https://127.0.0.1:%d", wardlePort))))
testServer := kastesting.StartTestServerOrDie(t, &kastesting.TestServerInstanceOptions{EnableCertAuth: true}, nil, framework.SharedEtcd()) testServer := kastesting.StartTestServerOrDie(t, &kastesting.TestServerInstanceOptions{EnableCertAuth: true, BinaryVersion: "1.32"}, nil, framework.SharedEtcd())
defer testServer.TearDownFn() defer testServer.TearDownFn()
kubeClientConfig := rest.CopyConfig(testServer.ClientConfig) kubeClientConfig := rest.CopyConfig(testServer.ClientConfig)
// force json because everything speaks it // force json because everything speaks it
@ -286,15 +298,18 @@ func TestAggregatedAPIServer(t *testing.T) {
o.RecommendedOptions.SecureServing.Listener = listener o.RecommendedOptions.SecureServing.Listener = listener
o.RecommendedOptions.SecureServing.BindAddress = netutils.ParseIPSloppy("127.0.0.1") o.RecommendedOptions.SecureServing.BindAddress = netutils.ParseIPSloppy("127.0.0.1")
wardleCmd := sampleserver.NewCommandStartWardleServer(ctx, o) wardleCmd := sampleserver.NewCommandStartWardleServer(ctx, o)
wardleCmd.SetArgs([]string{ args := []string{
"--authentication-kubeconfig", wardleToKASKubeConfigFile, "--authentication-kubeconfig", wardleToKASKubeConfigFile,
"--authorization-kubeconfig", wardleToKASKubeConfigFile, "--authorization-kubeconfig", wardleToKASKubeConfigFile,
"--etcd-servers", framework.GetEtcdURL(), "--etcd-servers", framework.GetEtcdURL(),
"--cert-dir", wardleCertDir, "--cert-dir", wardleCertDir,
"--kubeconfig", wardleToKASKubeConfigFile, "--kubeconfig", wardleToKASKubeConfigFile,
"--emulated-version", "wardle=1.1", "--emulated-version", fmt.Sprintf("wardle=%s", emulationVersion),
"--feature-gates", "wardle:BanFlunder=true", }
}) if enableWardleFeatureGate {
args = append(args, "--feature-gates", "wardle:BanFlunder=true")
}
wardleCmd.SetArgs(args)
if err := wardleCmd.Execute(); err != nil { if err := wardleCmd.Execute(); err != nil {
t.Error(err) t.Error(err)
} }
@ -393,6 +408,8 @@ func TestAggregatedAPIServer(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
// clean up data after test is done
defer wardleClient.Fischers().Delete(ctx, "panda", metav1.DeleteOptions{})
fischersList, err := wardleClient.Fischers().List(ctx, metav1.ListOptions{}) fischersList, err := wardleClient.Fischers().List(ctx, metav1.ListOptions{})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -409,8 +426,16 @@ func TestAggregatedAPIServer(t *testing.T) {
Name: "badname", Name: "badname",
}, },
}, metav1.CreateOptions{}) }, metav1.CreateOptions{})
if err == nil { banFlunder := enableWardleFeatureGate || emulationVersion == "1.2"
t.Fatal("expect flunder:badname not admitted") if banFlunder && err == nil {
t.Fatal("expect flunder:badname not admitted when wardle feature gates are specified")
}
if !banFlunder {
if err != nil {
t.Fatal("expect flunder:badname admitted when wardle feature gates are not specified")
} else {
defer wardleClient.Flunders(metav1.NamespaceSystem).Delete(ctx, "badname", metav1.DeleteOptions{})
}
} }
_, err = wardleClient.Flunders(metav1.NamespaceSystem).Create(ctx, &wardlev1alpha1.Flunder{ _, err = wardleClient.Flunders(metav1.NamespaceSystem).Create(ctx, &wardlev1alpha1.Flunder{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
@ -420,12 +445,17 @@ func TestAggregatedAPIServer(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer wardleClient.Flunders(metav1.NamespaceSystem).Delete(ctx, "panda", metav1.DeleteOptions{})
flunderList, err := wardleClient.Flunders(metav1.NamespaceSystem).List(ctx, metav1.ListOptions{}) flunderList, err := wardleClient.Flunders(metav1.NamespaceSystem).List(ctx, metav1.ListOptions{})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if len(flunderList.Items) != 1 { expectedFlunderCount := 2
t.Errorf("expected one flunder: %#v", flunderList.Items) if banFlunder {
expectedFlunderCount = 1
}
if len(flunderList.Items) != expectedFlunderCount {
t.Errorf("expected %d flunder: %#v", expectedFlunderCount, flunderList.Items)
} }
if len(flunderList.ResourceVersion) == 0 { if len(flunderList.ResourceVersion) == 0 {
t.Error("expected non-empty resource version for flunder list") t.Error("expected non-empty resource version for flunder list")

View File

@ -29,7 +29,6 @@ import (
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/util/wait"
utilfeature "k8s.io/apiserver/pkg/util/feature" utilfeature "k8s.io/apiserver/pkg/util/feature"
utilversion "k8s.io/apiserver/pkg/util/version"
"k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes"
featuregatetesting "k8s.io/component-base/featuregate/testing" featuregatetesting "k8s.io/component-base/featuregate/testing"
"k8s.io/kubernetes/cmd/kube-apiserver/app/options" "k8s.io/kubernetes/cmd/kube-apiserver/app/options"
@ -123,7 +122,6 @@ func TestServiceAllocIPAddress(t *testing.T) {
ModifyServerRunOptions: func(opts *options.ServerRunOptions) { ModifyServerRunOptions: func(opts *options.ServerRunOptions) {
opts.ServiceClusterIPRanges = serviceCIDR opts.ServiceClusterIPRanges = serviceCIDR
opts.GenericServerRunOptions.AdvertiseAddress = netutils.ParseIPSloppy("2001:db8::10") opts.GenericServerRunOptions.AdvertiseAddress = netutils.ParseIPSloppy("2001:db8::10")
opts.GenericServerRunOptions.EffectiveVersion = utilversion.NewEffectiveVersion("1.31")
opts.APIEnablement.RuntimeConfig.Set("networking.k8s.io/v1alpha1=true") opts.APIEnablement.RuntimeConfig.Set("networking.k8s.io/v1alpha1=true")
}, },
}) })