diff --git a/cmd/webhook/main_test.go b/cmd/webhook/main_test.go index 3166aa68c..80d9d4a7d 100644 --- a/cmd/webhook/main_test.go +++ b/cmd/webhook/main_test.go @@ -413,11 +413,11 @@ func TestResourceClaimValidatingWebhook(t *testing.T) { for name, test := range tests { t.Run(name, func(t *testing.T) { - err := featuregates.FeatureGates.SetEmulationVersion(utilversion.MajorMinor(999, 999)) + err := featuregates.FeatureGates().SetEmulationVersion(utilversion.MajorMinor(999, 999)) require.NoError(t, err) if test.featureGates != nil { - err := featuregates.FeatureGates.SetFromMap(test.featureGates) + err := featuregates.FeatureGates().SetFromMap(test.featureGates) require.NoError(t, err) } diff --git a/pkg/featuregates/featuregates.go b/pkg/featuregates/featuregates.go index 0576927a1..3610f1d6c 100644 --- a/pkg/featuregates/featuregates.go +++ b/pkg/featuregates/featuregates.go @@ -18,6 +18,7 @@ package featuregates import ( "strings" + "sync" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/util/version" @@ -38,10 +39,6 @@ const ( IMEXDaemonsWithDNSNames featuregate.Feature = "IMEXDaemonsWithDNSNames" ) -// FeatureGates is a singleton representing the set of all feature gates and their values. -// It contains both project-specific feature gates and standard Kubernetes logging feature gates. -var FeatureGates featuregate.MutableVersionedFeatureGate - // defaultFeatureGates contains the default settings for all project-specific feature gates. // These will be registered with the standard Kubernetes feature gate system. var defaultFeatureGates = map[featuregate.Feature]featuregate.VersionedSpecs{ @@ -68,9 +65,21 @@ var defaultFeatureGates = map[featuregate.Feature]featuregate.VersionedSpecs{ }, } -// init instantiates and sets the singleton 'FeatureGates' variable with newFeatureGates(). -func init() { - FeatureGates = newFeatureGates(parseProjectVersion()) +var ( + featureGatesOnce sync.Once + featureGates featuregate.MutableVersionedFeatureGate +) + +// FeatureGates instantiates and returns the package-level singleton representing +// the set of all feature gates and their values. +// It contains both project-specific feature gates and standard Kubernetes logging feature gates. +func FeatureGates() featuregate.MutableVersionedFeatureGate { + if featureGates == nil { + featureGatesOnce.Do(func() { + featureGates = newFeatureGates(parseProjectVersion()) + }) + } + return featureGates } // parseProjectVersion parses the project version string and returns major.minor version. @@ -106,12 +115,12 @@ func newFeatureGates(version *version.Version) featuregate.MutableVersionedFeatu // Enabled returns true if the specified feature gate is enabled in the global FeatureGates singleton. // This is a convenience function that uses the global feature gate registry. func Enabled(feature featuregate.Feature) bool { - return FeatureGates.Enabled(feature) + return FeatureGates().Enabled(feature) } // KnownFeatures returns a list of known feature gates with their descriptions. func KnownFeatures() []string { - return FeatureGates.KnownFeatures() + return FeatureGates().KnownFeatures() } // ToMap returns all known feature gates as a map[string]bool suitable for @@ -119,8 +128,8 @@ func KnownFeatures() []string { // Returns an empty map if no feature gates are configured. func ToMap() map[string]bool { result := make(map[string]bool) - for feature := range FeatureGates.GetAll() { - result[string(feature)] = FeatureGates.Enabled(feature) + for feature := range FeatureGates().GetAll() { + result[string(feature)] = FeatureGates().Enabled(feature) } return result } diff --git a/pkg/flags/featuregates.go b/pkg/flags/featuregates.go index d9b8509db..cf06dc134 100644 --- a/pkg/flags/featuregates.go +++ b/pkg/flags/featuregates.go @@ -43,7 +43,7 @@ func (f *FeatureGateConfig) Flags() []cli.Flag { Name: "feature-gates", Usage: "A set of key=value pairs that describe feature gates for alpha/experimental features. " + "Options are:\n " + strings.Join(featuregates.KnownFeatures(), "\n "), - Value: featuregates.FeatureGates.(pflag.Value), //nolint:forcetypeassert // No need for type check: FeatureGates is a *featuregate.featureGate, which implements pflag.Value. + Value: featuregates.FeatureGates().(pflag.Value), //nolint:forcetypeassert // No need for type check: FeatureGates is a *featuregate.featureGate, which implements pflag.Value. }) var flags []cli.Flag diff --git a/pkg/flags/featuregates_test.go b/pkg/flags/featuregates_test.go index 59ce15d26..c55f04eff 100644 --- a/pkg/flags/featuregates_test.go +++ b/pkg/flags/featuregates_test.go @@ -44,7 +44,7 @@ var ( func TestFeatureGateConfigBasicFunctionality(t *testing.T) { // Test that the config provides basic feature gate functionality - fg := featuregates.FeatureGates + fg := featuregates.FeatureGates() // Test that the feature gate is functional require.NotNil(t, fg, "FeatureGate should not be nil") @@ -93,7 +93,7 @@ func TestFeatureGateConfigSeparationOfConcerns(t *testing.T) { func TestFeatureGateConfigSetFromMap(t *testing.T) { t.Run("EnableDisabledFeature", func(t *testing.T) { // Test that we can enable a disabled feature - fg := featuregates.FeatureGates + fg := featuregates.FeatureGates() // Test enabling a disabled feature - we know AllAlpha starts as false require.False(t, featuregates.Enabled(testValidDisabledFeature), "AllAlpha feature should start as false") @@ -116,7 +116,7 @@ func TestFeatureGateConfigSetFromMap(t *testing.T) { t.Run("DisableEnabledFeature", func(t *testing.T) { // Test that we can disable an enabled feature - fg := featuregates.FeatureGates + fg := featuregates.FeatureGates() // First enable AllBeta so we can test disabling it err := fg.SetFromMap(map[string]bool{ @@ -145,7 +145,7 @@ func TestFeatureGateConfigSetFromMap(t *testing.T) { } func TestFeatureGateConfigKnownFeatures(t *testing.T) { - fg := featuregates.FeatureGates + fg := featuregates.FeatureGates() knownFeatures := fg.KnownFeatures() require.NotEmpty(t, knownFeatures, "Should have some known features") @@ -163,7 +163,7 @@ func TestFeatureGateConfigKnownFeatures(t *testing.T) { } func TestFeatureGateConfigErrorHandling(t *testing.T) { - fg := featuregates.FeatureGates + fg := featuregates.FeatureGates() // Test error handling with invalid features err := fg.SetFromMap(map[string]bool{ diff --git a/pkg/flags/logging.go b/pkg/flags/logging.go index c086ebed9..bf16c0630 100644 --- a/pkg/flags/logging.go +++ b/pkg/flags/logging.go @@ -43,7 +43,7 @@ func NewLoggingConfig() *LoggingConfig { // line flags and before running any code which emits log entries. // It uses the global feature gate singleton. func (l *LoggingConfig) Apply() error { - return logsapi.ValidateAndApply(l.config, featuregates.FeatureGates) + return logsapi.ValidateAndApply(l.config, featuregates.FeatureGates()) } // Flags returns the flags for logging configuration (NOT including feature gates).