Kubelet Startup - FeatureGate Initialization

Kubernetes has many feature functionalities, and these features are generally associated with Kubernetes Enhancement Proposals (KEPs). Feature functionalities go through stages of development such as alpha, beta, GA (Generally Available), and deprecated. Alpha signifies an unstable phase, beta is relatively stable but may contain bugs, GA indicates full stability and usability, and deprecated means the feature is being phased out. The lifecycle of a feature typically involves the proposal of a KEP, alpha phase, beta phase, GA phase, and eventual deprecation. Features in the alpha phase are not enabled by default, while features in beta are enabled by default. For more information on feature gates, visit the Feature Gates documentation and KEP documentation.

Feature gates are used to control whether a specific feature is enabled or disabled. It’s important to note that GA features cannot be disabled. In this article, we’ll use the kubelet source code as an example to analyze how feature gates work.

This analysis is based on Kubernetes version 1.18.6. You can visit the source code analysis repository for more details.

In the cmd/kubelet/app/server.go file, the following packages are loaded: k8s.io/apiserver/pkg/util/feature, k8s.io/component-base/featuregate, and k8s.io/kubernetes/pkg/features. Among them, k8s.io/apiserver/pkg/util/feature defines two global variables: DefaultMutableFeatureGate and DefaultFeatureGate. DefaultMutableFeatureGate provides both read and write capabilities, while DefaultFeatureGate is read-only.

DefaultMutableFeatureGate and DefaultFeatureGate are essentially the same, with DefaultMutableFeatureGate providing the ability to read and write externally, while DefaultFeatureGate is read-only.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package feature

import (
	"k8s.io/component-base/featuregate"
)

var (
	// DefaultMutableFeatureGate is a mutable version of DefaultFeatureGate.
	// Only top-level commands/options setup and the k8s.io/component-base/featuregate/testing package should make use of this.
	// Tests that need to modify feature gates for the duration of their test should use:
	//   defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.<FeatureName>, <value>)()
	DefaultMutableFeatureGate featuregate.MutableFeatureGate = featuregate.NewFeatureGate()

	// DefaultFeatureGate is a shared global FeatureGate.
	// Top-level commands/options setup that needs to modify this feature gate should use DefaultMutableFeatureGate.
	DefaultFeatureGate featuregate.FeatureGate = DefaultMutableFeatureGate
)

featuregate.NewFeatureGate() returns a featureGate instance with a known field that contains AllAlpha and AllBeta feature specifications. AllAlpha represents all alpha-stage feature gates, and it is disabled by default, while AllBeta represents all beta-stage feature gates, also disabled by default.

1
2
3
4
5
6
&featureGate{
		featureGateName: "k8s.io/apiserver/pkg/util/feature/feature_gate.go",
		known:           knownValue,  // an atomic.Value containing FeatureSpecs for AllAlpha and AllBeta
		special:         specialFeatures,
		enabled:         enabledValue, // an empty atomic.Value
	}

staging\src\k8s.io\component-base\featuregate\feature_gate.go

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
const (
	flagName = "feature-gates"

	// allAlphaGate is a global toggle for alpha features. Per-feature key
	// values override the default set by allAlphaGate. Examples:
	//   AllAlpha=false,NewFeature=true  will result in newFeature=true
	//   AllAlpha=true,NewFeature=false  will result in newFeature=false
	allAlphaGate Feature = "AllAlpha"

	// allBetaGate is a global toggle for beta features. Per-feature key
	// values override the default set by allBetaGate. Examples:
	//   AllBeta=false,NewFeature=true  will result in NewFeature=true
	//   AllBeta=true,NewFeature=false  will result in NewFeature=false
	allBetaGate Feature = "AllBeta"
)

var (
	// The generic features.
	defaultFeatures = map[Feature]FeatureSpec{
		allAlphaGate: {Default: false, PreRelease: Alpha},
		allBetaGate:  {Default: false, PreRelease: Beta},
	}

    	// Special handling for a few gates.
	specialFeatures = map[Feature]func(known map[Feature]FeatureSpec, enabled map[Feature]bool, val bool){
		allAlphaGate: setUnsetAlphaGates,
		allBetaGate:  setUnsetBetaGates,
	}
)

type FeatureSpec struct {
	// Default is the default enablement state for the feature
	Default bool
	// LockToDefault indicates that the feature is locked to its default and cannot be changed
	LockToDefault bool
	// PreRelease indicates the maturity level of the feature
	PreRelease prerelease
}

// FeatureGate indicates whether a given feature is enabled or not
type FeatureGate interface {
	// Enabled returns true if the key is enabled.
	Enabled(key Feature) bool
	// KnownFeatures returns a slice of strings describing the FeatureGate's known features.
	KnownFeatures() []string
	// DeepCopy returns a deep copy of the FeatureGate object, such that gates can be
	// set on the copy without mutating the original. This is useful for validating
	// config against potential feature gate changes before committing those changes.
	DeepCopy() MutableFeatureGate
}

// MutableFeatureGate parses and stores flag gates for known features from
// a string like feature1=true,feature2=false,...
type MutableFeatureGate interface {
	FeatureGate

	// AddFlag adds a flag for setting global feature gates to the specified FlagSet.
	AddFlag(fs *pflag.FlagSet)
	// Set parses and stores flag gates for known features
	// from a string like feature1=true,feature2=false,...
	Set(value string) error
	// SetFromMap stores flag gates for known features from a map[string]bool or returns an error
	SetFromMap(m map[string]bool) error
	// Add adds features to the featureGate.
	Add(features map[Feature]FeatureSpec) error
}

// featureGate implements FeatureGate as well as pflag.Value for flag parsing.
type featureGate struct {
	featureGateName string

	special map[Feature]func(map[Feature]FeatureSpec, map[Feature]bool, bool)

	// lock guards writes to known, enabled, and reads/writes of closed
	lock sync.Mutex
	// known holds a map[Feature]FeatureSpec
	known *atomic.Value
	// enabled holds a map[Feature]bool
	enabled *atomic.Value
	// closed is set to true when AddFlag is called, and prevents subsequent calls to Add
	closed bool
}

type prerelease string

const (
	// Values for PreRelease.
	Alpha = prerelease("ALPHA")
	Beta  = prerelease("BETA")
	GA    = prerelease("")

	// Deprecated
	Deprecated = prerelease("DEPRECATED")
)

func NewFeatureGate() *featureGate {
	known := map[Feature]FeatureSpec{}
	for k, v := range defaultFeatures {
		known[k] = v
	}

	knownValue := &atomic.Value{}
	knownValue.Store(known)

	enabled := map[Feature]bool{}
	enabledValue := &atomic.Value{}
	enabledValue.Store(enabled)

	f := &featureGate{
		featureGateName: naming.GetNameFromCallsite(internalPackages...),
		known:           knownValue,
		special:         specialFeatures,
		enabled:         enabledValue,
	}
	return f
}

Involves defining all known features, their default enablement settings, and maturity levels. The init() function imports the default configuration into utilfeature.DefaultMutableFeatureGate. Therefore, DefaultMutableFeatureGate or DefaultFeatureGate holds all the default FeatureSpec settings along with AllAlpha and AllBeta. However, DefaultFeatureGate.enabled remains empty.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

const (
	// Every feature gate should add method here following this template:
	//
	// // owner: @username
	// // alpha: v1.X
	// MyFeature featuregate.Feature = "MyFeature"

	// owner: @tallclair
	// beta: v1.4
	AppArmor featuregate.Feature = "AppArmor"

	// owner: @mtaufen
	// alpha: v1.4
	// beta: v1.11
	DynamicKubeletConfig featuregate.Feature = "DynamicKubeletConfig"
    .......
)

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

// defaultKubernetesFeatureGates consists of all known Kubernetes-specific feature keys.
// To add a new feature, define a key for it above and add it here. The features will be
// available throughout Kubernetes binaries.
var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{
	AppArmor:             {Default: true, PreRelease: featuregate.Beta},
	DynamicKubeletConfig: {Default: true, PreRelease: featuregate.Beta},
	ExperimentalHostUserNamespaceDefaultingGate: {Default: false, PreRelease: featuregate.Beta},
	DevicePlugins:                  {Default: true, PreRelease: featuregate.Beta},
    .........
}

The FeatureGate.Add method is defined in staging\src\k8s.io\component-base\featuregate\feature_gate.go. It iterates through defaultKubernetesFeatureGates. If a feature is not present in the known set of DefaultMutableFeatureGate, it is added to the known set. If the feature already exists in known and has the same FeatureSpec, it is ignored. However, if the feature exists but has a different FeatureSpec, an error is reported.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// Add adds features to the featureGate.
func (f *featureGate) Add(features map[Feature]FeatureSpec) error {
	f.lock.Lock()
	defer f.lock.Unlock()

	if f.closed {
		return fmt.Errorf("cannot add a feature gate after adding it to the flag set")
	}

	// Copy existing state
	known := map[Feature]FeatureSpec{}
	for k, v := range f.known.Load().(map[Feature]FeatureSpec) {
		known[k] = v
	}

	for name, spec := range features {
		if existingSpec, found := known[name]; found {
			if existingSpec == spec {
				continue
			}
			return fmt.Errorf("feature gate %q with different spec already exists: %v", name, existingSpec)
		}

		known[name] = spec
	}

	// Persist updated state
	f.known.Store(known)

	return nil
}

There are two ways to set feature gates. One is through the command line, for example, --feature-gates=HPAScaleToZero=true,EphemeralContainers=true. The other way is to configure them in a configuration file (or using dynamic kubelet configuration), like this:

1
2
featureGates:
  SupportNodePidsLimit: true

The --feature-gates command line option defines the configuration as follows:

1
2
fs.Var(cliflag.NewMapStringBool(&c.FeatureGates), "feature-gates", "A set of key=value pairs that describe feature gates for alpha/experimental features. "+
		"Options are:\n"+strings.Join(utilfeature.DefaultFeatureGate.KnownFeatures(), "\n"))

During the startup of the kubelet, the specified feature settings are saved in DefaultFeatureGate.enabled as follows:

  • Executing cleanFlagSet.Parse(args) performs command line parsing and ultimately saves the command line parameters in KubeletConfigure.FeatureGates.
  • Executing utilfeature.DefaultMutableFeatureGate.SetFromMap(kubeletConfig.FeatureGates) saves kubeletConfig.FeatureGates in DefaultFeatureGate.
  • If feature gates are set in the configuration file, they are merged with the command line feature gates, with conflicts resolved in favor of the command line settings.
  • If dynamic kubelet configuration is used and there exists an “assigned” or “last-known-good” file on disk, the feature gates in it are merged with the command line feature gates, with conflicts resolved in favor of the command line settings. A more detailed analysis of dynamic kubelet configuration will be covered in future articles.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
		Run: func(cmd *cobra.Command, args []string) {
			// initial flag parse, since we disable cobra's flag parsing
			if err := cleanFlagSet.Parse(args); err != nil {
				cmd.Usage()
				klog.Fatal(err)
			}	
			.......
			// set feature gates from initial flags-based config
			if err := utilfeature.DefaultMutableFeatureGate.SetFromMap(kubeletConfig.FeatureGates); err != nil {
				klog.Fatal(err)
			}

			// validate the initial KubeletFlags
			if err := options.ValidateKubeletFlags(kubeletFlags); err != nil {
				klog.Fatal(err)
			}

			if kubeletFlags.ContainerRuntime == "remote" && cleanFlagSet.Changed("pod-infra-container-image") {
				klog.Warning("Warning: For remote container runtime, --pod-infra-container-image is ignored in kubelet, which should be set in that remote runtime instead")
			}

			// load kubelet config file, if provided
			if configFile := kubeletFlags.KubeletConfigFile; len(configFile) > 0 {
				kubeletConfig, err = loadConfigFile(configFile)
				if err != nil {
					klog.Fatal(err)
				}
				// We must enforce flag precedence by re-parsing the command line into the new object.
				// This is necessary to preserve backwards-compatibility across binary upgrades.
				// See issue #56171 for more details.
				// 重新解析args,命令行选项覆盖kubeletConfig里的选项
				// 其中FeatureGates为合并
				// 忽略Credential、klog、cadvisor选项--这些选项不在kubeletConfig里
				if err := kubeletConfigFlagPrecedence(kubeletConfig, args); err != nil {
					klog.Fatal(err)
				}
				// update feature gates based on new config
				if err := utilfeature.DefaultMutableFeatureGate.SetFromMap(kubeletConfig.FeatureGates); err != nil {
					klog.Fatal(err)
				}
			}

			// We always validate the local configuration (command line + config file).
			// This is the default "last-known-good" config for dynamic config, and must always remain valid.
			if err := kubeletconfigvalidation.ValidateKubeletConfiguration(kubeletConfig); err != nil {
				klog.Fatal(err)
			}

			// use dynamic kubelet config, if enabled
			var kubeletConfigController *dynamickubeletconfig.Controller
			if dynamicConfigDir := kubeletFlags.DynamicConfigDir.Value(); len(dynamicConfigDir) > 0 {
				var dynamicKubeletConfig *kubeletconfiginternal.KubeletConfiguration
				dynamicKubeletConfig, kubeletConfigController, err = BootstrapKubeletConfigController(dynamicConfigDir,
					func(kc *kubeletconfiginternal.KubeletConfiguration) error {
						// Here, we enforce flag precedence inside the controller, prior to the controller's validation sequence,
						// so that we get a complete validation at the same point where we can decide to reject dynamic config.
						// This fixes the flag-precedence component of issue #63305.
						// See issue #56171 for general details on flag precedence.
						return kubeletConfigFlagPrecedence(kc, args)
					})
				if err != nil {
					klog.Fatal(err)
				}
				// If we should just use our existing, local config, the controller will return a nil config
				if dynamicKubeletConfig != nil {
					kubeletConfig = dynamicKubeletConfig
					// Note: flag precedence was already enforced in the controller, prior to validation,
					// by our above transform function. Now we simply update feature gates from the new config.
					if err := utilfeature.DefaultMutableFeatureGate.SetFromMap(kubeletConfig.FeatureGates); err != nil {
						klog.Fatal(err)
					}
				}
			}

SetFromMap

The SetFromMap function is used to save the feature gates from kubeletConfig.FeatureGates into utilfeature.DefaultFeatureGate. When saving these features, certain conditions must be met:

  • The features must match known featureSpec from all known FeatureSpec in defaultKubernetesFeatureGates.
  • It is not allowed to modify the default values of locked FeatureSpec.

There are also special FeatureSpecs:

  • AllAlpha sets the enabled value for all alpha-stage FeatureSpecs that haven’t been explicitly set (not in enabled).
  • AllBeta sets the enabled value for all beta-stage FeatureSpecs that haven’t been explicitly set (not in enabled).

These definitions can typically be found in the staging\src\k8s.io\component-base\featuregate\feature_gate.go file. This function ensures that the feature gates are correctly configured based on the provided map while adhering to certain rules and constraints.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// SetFromMap stores flag gates for known features from a map[string]bool or returns an error
func (f *featureGate) SetFromMap(m map[string]bool) error {
	f.lock.Lock()
	defer f.lock.Unlock()

	// Copy existing state
	known := map[Feature]FeatureSpec{}
	for k, v := range f.known.Load().(map[Feature]FeatureSpec) {
		known[k] = v
	}
	enabled := map[Feature]bool{}
	for k, v := range f.enabled.Load().(map[Feature]bool) {
		enabled[k] = v
	}

	for k, v := range m {
		k := Feature(k)
        // 判断是否是已知的featureSpec,不是已知的,直接报错
		featureSpec, ok := known[k]
		if !ok {
			return fmt.Errorf("unrecognized feature gate: %s", k)
		}
		if featureSpec.LockToDefault && featureSpec.Default != v {
			return fmt.Errorf("cannot set feature gate %v to %v, feature is locked to %v", k, v, featureSpec.Default)
		}
		enabled[k] = v
		// Handle "special" features like "all alpha gates"
		// 遇到AllAlpha或AllBeta将enable值改为v
		if fn, found := f.special[k]; found {
			fn(known, enabled, v)
		}

		if featureSpec.PreRelease == Deprecated {
			klog.Warningf("Setting deprecated feature gate %s=%t. It will be removed in a future release.", k, v)
		} else if featureSpec.PreRelease == GA {
			klog.Warningf("Setting GA feature gate %s=%t. It will be removed in a future release.", k, v)
		}
	}

	// Persist changes
	f.known.Store(known)
	f.enabled.Store(enabled)

	klog.V(1).Infof("feature gates: %v", f.enabled)
	return nil
}

kubeletConfigFlagPrecedence

The kubeletConfigFlagPrecedence process involves re-parsing command line parameters to override the kubeletConfig configuration. Here are the steps involved:

  1. Create a new flagset fs that includes KubeletFlags and KubeletConfigFlags, ignoring Globalsflag options.
  2. Save the original kc.FeatureGates (feature gates from the kubeletConfig).
  3. Re-parse the command line parameters, which will update kc.FeatureGates with the values specified on the command line.
  4. Aggregate the original kc.FeatureGates and the parsed kc.FeatureGates from the command line. If a feature gate from the original kc.FeatureGates (configured in the configuration file) is not present in the command line kc.FeatureGates, it will be added to kc.FeatureGates. However, when there is a conflict between the command line kc.FeatureGates and the configuration file kc.FeatureGates, the values from the command line take precedence.

In summary, this process ensures that the command line parameters can override the feature gate settings specified in the kubeletConfig, while also preserving any feature gates not explicitly specified on the command line. It prioritizes the command line settings in case of conflicts.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// kubeletCosnfigFlagPrecedence re-parses flags over the KubeletConfiguration object.
// We must enforce flag precedence by re-parsing the command line into the new object.
// This is necessary to preserve backwards-compatibility across binary upgrades.
// See issue #56171 for more details.
func kubeletConfigFlagPrecedence(kc *kubeletconfiginternal.KubeletConfiguration, args []string) error {
	// We use a throwaway kubeletFlags and a fake global flagset to avoid double-parses,
	// as some Set implementations accumulate values from multiple flag invocations.
	fs := newFakeFlagSet(newFlagSetWithGlobals())
	// register throwaway KubeletFlags
	options.NewKubeletFlags().AddFlags(fs)
	// register new KubeletConfiguration
	options.AddKubeletConfigFlags(fs, kc)
	// Remember original feature gates, so we can merge with flag gates later
	original := kc.FeatureGates
	// re-parse flags
	if err := fs.Parse(args); err != nil {
		return err
	}
	// Add back feature gates that were set in the original kc, but not in flags
	for k, v := range original {
		if _, ok := kc.FeatureGates[k]; !ok {
			kc.FeatureGates[k] = v
		}
	}
	return nil
}

Taking a code snippet from kubelet as an example, you can use utilfeature.DefaultFeatureGate.Enabled(key Feature) bool to determine whether a specific feature is enabled.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import (
	utilfeature "k8s.io/apiserver/pkg/util/feature"
    features "k8s.io/kubernetes/pkg/features"
    .....
)

.....
// If the kubelet config controller is available, and dynamic config is enabled, start the config and status sync loops
	if utilfeature.DefaultFeatureGate.Enabled(features.DynamicKubeletConfig) && len(s.DynamicConfigDir.Value()) > 0 &&
		kubeDeps.KubeletConfigController != nil && !standaloneMode && !s.RunOnce {
		if err := kubeDeps.KubeletConfigController.StartSync(kubeDeps.KubeClient, kubeDeps.EventClient, string(nodeName)); err != nil {
			return err
		}
	}

The Enabled function is used to check if a specific feature is enabled:

  • First, it checks if the feature is in the enabled list because only explicitly set features are saved there, and all the defaults are not saved there.
  • It then checks if the feature is known. If it is known, it returns its default value; otherwise, it panics.

Defined in cmd\kubelet\app\server.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// Enabled returns true if the key is enabled.  If the key is not known, this call will panic.
func (f *featureGate) Enabled(key Feature) bool {
	if v, ok := f.enabled.Load().(map[Feature]bool)[key]; ok {
		return v
	}
	if v, ok := f.known.Load().(map[Feature]FeatureSpec)[key]; ok {
		return v.Default
	}

	panic(fmt.Errorf("feature %q is not registered in FeatureGate %q", key, f.featureGateName))
}

Related Content