diff --git a/staging/src/k8s.io/component-base/featuregate/feature_gate.go b/staging/src/k8s.io/component-base/featuregate/feature_gate.go index 30e430d56e9dd..447bd37ecdfac 100644 --- a/staging/src/k8s.io/component-base/featuregate/feature_gate.go +++ b/staging/src/k8s.io/component-base/featuregate/feature_gate.go @@ -165,6 +165,7 @@ type MutableVersionedFeatureGate interface { // 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. SetEmulationVersion(emulationVersion *version.Version) error + // GetAll returns a copy of the map of known feature names to versioned feature specs. GetAllVersioned() map[Feature]VersionedSpecs // AddVersioned adds versioned feature specs to the featureGate. @@ -543,6 +544,16 @@ func (f *featureGate) GetAllVersioned() map[Feature]VersionedSpecs { } func (f *featureGate) SetEmulationVersion(emulationVersion *version.Version) error { + return f.setEmulationVersionWithContext(context.Background(), emulationVersion) +} + +// SetEmulationVersionWithContext allows passing a context for test logger support. +// This is primarily intended for testing scenarios. +func (f *featureGate) SetEmulationVersionWithContext(ctx context.Context, emulationVersion *version.Version) error { + return f.setEmulationVersionWithContext(ctx, emulationVersion) +} + +func (f *featureGate) setEmulationVersionWithContext(ctx context.Context, emulationVersion *version.Version) error { if emulationVersion.EqualTo(f.EmulationVersion()) { return nil } @@ -561,11 +572,19 @@ func (f *featureGate) SetEmulationVersion(emulationVersion *version.Version) err queriedFeatures := f.queriedFeatures.Load().(sets.Set[Feature]) known := f.known.Load().(map[Feature]VersionedSpecs) + contextLogger := getContextLogger(ctx) + for feature := range queriedFeatures { newVal := featureEnabled(feature, enabled, known, emulationVersion) oldVal := featureEnabled(feature, f.enabled.Load().(map[Feature]bool), known, f.EmulationVersion()) if newVal != oldVal { - klog.Warningf("SetEmulationVersion will change already queried feature:%s from %v to %v", feature, oldVal, newVal) + warningMsg := fmt.Sprintf("SetEmulationVersion will change already queried feature:%s from %v to %v", feature, oldVal, newVal) + // Try to log to context logger first, fallback to global klog + if contextLogger != nil { + contextLogger.Logf("WARNING: %s", warningMsg) + } else { + klog.Warningf("%s", warningMsg) + } } } @@ -669,6 +688,32 @@ func (f *featureGate) AddMetrics() { } } +// testLoggerKey is used to store test logger in context +type testLoggerKey struct{} + +// WithTestLoggerContext returns a context with test logger for redirecting warnings to test output +func WithTestLoggerContext(ctx context.Context, logger interface { + Logf(format string, args ...interface{}) +}) context.Context { + return context.WithValue(ctx, testLoggerKey{}, logger) +} + +// getContextLogger retrieves logger from context if available +func getContextLogger(ctx context.Context) interface { + Logf(format string, args ...interface{}) +} { + if ctx == nil { + return nil + } + logger, ok := ctx.Value(testLoggerKey{}).(interface { + Logf(format string, args ...interface{}) + }) + if ok { + return logger + } + return nil +} + // KnownFeatures returns a slice of strings describing the FeatureGate's known features. // preAlpha, Deprecated and GA features are hidden from the list. func (f *featureGate) KnownFeatures() []string { diff --git a/staging/src/k8s.io/component-base/featuregate/testing/feature_gate.go b/staging/src/k8s.io/component-base/featuregate/testing/feature_gate.go index 1d7fc46768ee8..67e8fa4a309d7 100644 --- a/staging/src/k8s.io/component-base/featuregate/testing/feature_gate.go +++ b/staging/src/k8s.io/component-base/featuregate/testing/feature_gate.go @@ -17,6 +17,7 @@ limitations under the License. package testing import ( + "context" "fmt" "strings" "sync" @@ -124,18 +125,37 @@ func SetFeatureGateEmulationVersionDuringTest(tb TB, gate featuregate.FeatureGat tb.Helper() detectParallelOverrideCleanup := detectParallelOverrideEmulationVersion(tb, ver) originalEmuVer := gate.(featuregate.MutableVersionedFeatureGate).EmulationVersion() - if err := gate.(featuregate.MutableVersionedFeatureGate).SetEmulationVersion(ver); err != nil { + + if err := setEmulationVersionForTest(gate.(featuregate.MutableVersionedFeatureGate), ver, tb); err != nil { tb.Fatalf("failed to set emulation version to %s during test: %v", ver.String(), err) } tb.Cleanup(func() { tb.Helper() detectParallelOverrideCleanup() - if err := gate.(featuregate.MutableVersionedFeatureGate).SetEmulationVersion(originalEmuVer); err != nil { + if err := setEmulationVersionForTest(gate.(featuregate.MutableVersionedFeatureGate), originalEmuVer, tb); err != nil { tb.Fatalf("failed to restore emulation version to %s during test", originalEmuVer.String()) } }) } +// setEmulationVersionForTest sets emulation version with test-friendly warning handling. +// This uses context to redirect warnings to test output. +func setEmulationVersionForTest(gate featuregate.MutableVersionedFeatureGate, ver *version.Version, tb TB) error { + // Check if this gate supports context-aware emulation version setting + type contextAwareGate interface { + SetEmulationVersionWithContext(ctx context.Context, emulationVersion *version.Version) error + } + + if contextGate, ok := gate.(contextAwareGate); ok { + // Use context with test logger to redirect warnings to test output + ctx := featuregate.WithTestLoggerContext(context.Background(), tb) + return contextGate.SetEmulationVersionWithContext(ctx, ver) + } + + // Fallback to regular method for implementations that don't support context + return gate.SetEmulationVersion(ver) +} + func detectParallelOverride(tb TB, f featuregate.Feature) func() { tb.Helper() overrideLock.Lock() @@ -196,5 +216,6 @@ type TB interface { Fatal(args ...any) Fatalf(format string, args ...any) Helper() + Logf(format string, args ...interface{}) Name() string }