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.
1 Module Initialization
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.
1.1 Loading k8s.io/apiserver/pkg/util/feature
DefaultMutableFeatureGate
and DefaultFeatureGate
are essentially the same, with DefaultMutableFeatureGate
providing the ability to read and write externally, while DefaultFeatureGate
is read-only.
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.
&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
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
}
1.2 Loading k8s.io/kubernetes/pkg/features
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.
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.
// 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
}
2 Setting Feature Gates
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:
featureGates:
SupportNodePidsLimit: true
The --feature-gates
command line option defines the configuration as follows:
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 inKubeletConfigure.FeatureGates
. - Executing
utilfeature.DefaultMutableFeatureGate.SetFromMap(kubeletConfig.FeatureGates)
saveskubeletConfig.FeatureGates
inDefaultFeatureGate
. - 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.
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 knownFeatureSpec
indefaultKubernetesFeatureGates
. - It is not allowed to modify the default values of locked FeatureSpec.
There are also special FeatureSpecs:
AllAlpha
sets theenabled
value for all alpha-stage FeatureSpecs that haven’t been explicitly set (not inenabled
).AllBeta
sets theenabled
value for all beta-stage FeatureSpecs that haven’t been explicitly set (not inenabled
).
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.
// 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:
- Create a new flagset
fs
that includesKubeletFlags
andKubeletConfigFlags
, ignoringGlobalsflag
options. - Save the original
kc.FeatureGates
(feature gates from thekubeletConfig
). - Re-parse the command line parameters, which will update
kc.FeatureGates
with the values specified on the command line. - Aggregate the original
kc.FeatureGates
and the parsedkc.FeatureGates
from the command line. If a feature gate from the originalkc.FeatureGates
(configured in the configuration file) is not present in the command linekc.FeatureGates
, it will be added tokc.FeatureGates
. However, when there is a conflict between the command linekc.FeatureGates
and the configuration filekc.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.
// 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
}
3 Using Configuration
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.
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:
// 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))
}