From a282006d2d3b245c2f5cdf444a4dea45d1b27585 Mon Sep 17 00:00:00 2001 From: Qi Wang Date: Mon, 26 Apr 2021 16:28:14 -0400 Subject: [PATCH] Add kubeletconfig to bootstrap mode Follow up PR for https://github.com/openshift/machine-config-operator/pull/2517/files#r611658650 To run KubeletConfig at bootstrap mode. Signed-off-by: Qi Wang --- pkg/controller/bootstrap/bootstrap.go | 11 + pkg/controller/common/constants.go | 3 + pkg/controller/kubelet-config/helpers.go | 57 ++++- .../kubelet_config_bootstrap.go | 111 ++++++++++ .../kubelet_config_bootstrap_test.go | 196 ++++++++++++++++++ .../kubelet_config_controller.go | 47 +---- .../kubelet-config/kubelet_config_features.go | 3 + 7 files changed, 382 insertions(+), 46 deletions(-) create mode 100644 pkg/controller/kubelet-config/kubelet_config_bootstrap.go create mode 100644 pkg/controller/kubelet-config/kubelet_config_bootstrap_test.go diff --git a/pkg/controller/bootstrap/bootstrap.go b/pkg/controller/bootstrap/bootstrap.go index 5f0a0a5b1f..47fd5911e7 100644 --- a/pkg/controller/bootstrap/bootstrap.go +++ b/pkg/controller/bootstrap/bootstrap.go @@ -74,6 +74,7 @@ func (b *Bootstrap) Run(destDir string) error { var cconfig *mcfgv1.ControllerConfig var featureGate *apicfgv1.FeatureGate + var kconfigs []*mcfgv1.KubeletConfig var pools []*mcfgv1.MachineConfigPool var configs []*mcfgv1.MachineConfig var icspRules []*apioperatorsv1alpha1.ImageContentSourcePolicy @@ -112,6 +113,8 @@ func (b *Bootstrap) Run(destDir string) error { configs = append(configs, obj) case *mcfgv1.ControllerConfig: cconfig = obj + case *mcfgv1.KubeletConfig: + kconfigs = append(kconfigs, obj) case *apioperatorsv1alpha1.ImageContentSourcePolicy: icspRules = append(icspRules, obj) case *apicfgv1.Image: @@ -139,6 +142,7 @@ func (b *Bootstrap) Run(destDir string) error { if err != nil { return err } + configs = append(configs, rconfigs...) if featureGate != nil { @@ -148,6 +152,13 @@ func (b *Bootstrap) Run(destDir string) error { } configs = append(configs, kConfigs...) } + if len(kconfigs) > 0 { + kconfigs, err := kubeletconfig.RunKubeletBootstrap(b.templatesDir, kconfigs, cconfig, featureGate, pools) + if err != nil { + return err + } + configs = append(configs, kconfigs...) + } fpools, gconfigs, err := render.RunBootstrap(pools, configs, cconfig) if err != nil { diff --git a/pkg/controller/common/constants.go b/pkg/controller/common/constants.go index f0f7c49ed2..3bb45eac5a 100644 --- a/pkg/controller/common/constants.go +++ b/pkg/controller/common/constants.go @@ -19,6 +19,9 @@ const ( // MCNameSuffixAnnotationKey is used to keep track of the machine config name associated with a CR MCNameSuffixAnnotationKey = "machineconfiguration.openshift.io/mc-name-suffix" + // MaxMCNameSuffix is the maximum value of the name suffix of the machine config associated with kubeletconfig and containerruntime objects + MaxMCNameSuffix int = 9 + // ClusterFeatureInstanceName is a singleton name for featureGate configuration ClusterFeatureInstanceName = "cluster" ) diff --git a/pkg/controller/kubelet-config/helpers.go b/pkg/controller/kubelet-config/helpers.go index 1e3aab5bb0..437318d9e3 100644 --- a/pkg/controller/kubelet-config/helpers.go +++ b/pkg/controller/kubelet-config/helpers.go @@ -8,6 +8,7 @@ import ( "strconv" ign3types "github.com/coreos/ignition/v2/config/v3_2/types" + "github.com/imdario/mergo" osev1 "github.com/openshift/api/config/v1" "github.com/vincent-petithory/dataurl" corev1 "k8s.io/api/core/v1" @@ -206,7 +207,7 @@ func getManagedKubeletConfigKey(pool *mcfgv1.MachineConfigPool, client mcfgclien // then if the user creates a kc-new it will map to mc-3. This is what we want as the latest kubelet config created should be higher in priority // so that those changes can be rolled out to the nodes. But users will have to be mindful of how many kubelet config CRs they create. Don't think // anyone should ever have the need to create 10 when they can simply update an existing kubelet config unless it is to apply to another pool. - if suffixNum+1 > 9 { + if suffixNum+1 > ctrlcommon.MaxMCNameSuffix { return "", fmt.Errorf("max number of supported kubelet config (10) has been reached. Please delete old kubelet configs before retrying") } // Return the default MC name with the suffixNum+1 value appended to it @@ -336,3 +337,57 @@ func kubeletConfigToIgnFile(cfg *kubeletconfigv1beta1.KubeletConfiguration) (*ig cfgIgn := createNewKubeletIgnition(cfgJSON) return cfgIgn, nil } + +// generateKubeletIgnFiles generates the Ignition files from the kubelet config +func generateKubeletIgnFiles(kubeletConfig *mcfgv1.KubeletConfig, originalKubeConfig *kubeletconfigv1beta1.KubeletConfiguration, userDefinedSystemReserved map[string]string) (*ign3types.File, *ign3types.File, *ign3types.File, error) { + var ( + kubeletIgnition *ign3types.File + logLevelIgnition *ign3types.File + autoSizingReservedIgnition *ign3types.File + ) + + if kubeletConfig.Spec.KubeletConfig != nil && kubeletConfig.Spec.KubeletConfig.Raw != nil { + specKubeletConfig, err := decodeKubeletConfig(kubeletConfig.Spec.KubeletConfig.Raw) + if err != nil { + return nil, nil, nil, fmt.Errorf("could not deserialize the new Kubelet config: %v", err) + } + + if val, ok := specKubeletConfig.SystemReserved["memory"]; ok { + userDefinedSystemReserved["memory"] = val + delete(specKubeletConfig.SystemReserved, "memory") + } + + if val, ok := specKubeletConfig.SystemReserved["cpu"]; ok { + userDefinedSystemReserved["cpu"] = val + delete(specKubeletConfig.SystemReserved, "cpu") + } + + // FeatureGates must be set from the FeatureGate. + // Remove them here to prevent the specKubeletConfig merge overwriting them. + specKubeletConfig.FeatureGates = nil + + // Merge the Old and New + err = mergo.Merge(originalKubeConfig, specKubeletConfig, mergo.WithOverride) + if err != nil { + return nil, nil, nil, fmt.Errorf("could not merge original config and new config: %v", err) + } + } + + // Encode the new config into an Ignition File + kubeletIgnition, err := kubeletConfigToIgnFile(originalKubeConfig) + if err != nil { + return nil, nil, nil, fmt.Errorf("could not encode JSON: %v", err) + } + + if kubeletConfig.Spec.LogLevel != nil { + logLevelIgnition = createNewKubeletLogLevelIgnition(*kubeletConfig.Spec.LogLevel) + } + if kubeletConfig.Spec.AutoSizingReserved != nil && len(userDefinedSystemReserved) == 0 { + autoSizingReservedIgnition = createNewKubeletDynamicSystemReservedIgnition(kubeletConfig.Spec.AutoSizingReserved, userDefinedSystemReserved) + } + if len(userDefinedSystemReserved) > 0 { + autoSizingReservedIgnition = createNewKubeletDynamicSystemReservedIgnition(nil, userDefinedSystemReserved) + } + + return kubeletIgnition, logLevelIgnition, autoSizingReservedIgnition, nil +} diff --git a/pkg/controller/kubelet-config/kubelet_config_bootstrap.go b/pkg/controller/kubelet-config/kubelet_config_bootstrap.go new file mode 100644 index 0000000000..90fe3c14fd --- /dev/null +++ b/pkg/controller/kubelet-config/kubelet_config_bootstrap.go @@ -0,0 +1,111 @@ +package kubeletconfig + +import ( + "encoding/json" + "fmt" + + configv1 "github.com/openshift/api/config/v1" + mcfgv1 "github.com/openshift/machine-config-operator/pkg/apis/machineconfiguration.openshift.io/v1" + ctrlcommon "github.com/openshift/machine-config-operator/pkg/controller/common" + "github.com/openshift/machine-config-operator/pkg/version" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" +) + +// RunKubeletBootstrap generates MachineConfig objects for mcpPools that would have been generated by syncKubeletConfig +func RunKubeletBootstrap(templateDir string, kubeletConfigs []*mcfgv1.KubeletConfig, controllerConfig *mcfgv1.ControllerConfig, features *configv1.FeatureGate, mcpPools []*mcfgv1.MachineConfigPool) ([]*mcfgv1.MachineConfig, error) { + var res []*mcfgv1.MachineConfig + managedKeyExist := make(map[string]bool) + userDefinedSystemReserved := make(map[string]string) + // Validate the KubeletConfig CR if exists + for _, kubeletConfig := range kubeletConfigs { + if err := validateUserKubeletConfig(kubeletConfig); err != nil { + return nil, err + } + } + + for _, kubeletConfig := range kubeletConfigs { + // use selector since label matching part of a KubeletConfig is not handled during the bootstrap + selector, err := metav1.LabelSelectorAsSelector(kubeletConfig.Spec.MachineConfigPoolSelector) + if err != nil { + return nil, fmt.Errorf("invalid label selector: %v", err) + } + + for _, pool := range mcpPools { + // If a pool with a nil or empty selector creeps in, it should match nothing, not everything. + // skip the pool if no matched label for kubeletconfig + if selector.Empty() || !selector.Matches(labels.Set(pool.Labels)) { + continue + } + role := pool.Name + + originalKubeConfig, err := generateOriginalKubeletConfigWithFeatureGates(controllerConfig, templateDir, role, features) + if err != nil { + return nil, err + } + if kubeletConfig.Spec.TLSSecurityProfile != nil { + // Inject TLS Options from Spec + observedMinTLSVersion, observedCipherSuites := getSecurityProfileCiphers(kubeletConfig.Spec.TLSSecurityProfile) + originalKubeConfig.TLSMinVersion = observedMinTLSVersion + originalKubeConfig.TLSCipherSuites = observedCipherSuites + } + + kubeletIgnition, logLevelIgnition, autoSizingReservedIgnition, err := generateKubeletIgnFiles(kubeletConfig, originalKubeConfig, userDefinedSystemReserved) + if err != nil { + return nil, err + } + + tempIgnConfig := ctrlcommon.NewIgnConfig() + if autoSizingReservedIgnition != nil { + tempIgnConfig.Storage.Files = append(tempIgnConfig.Storage.Files, *autoSizingReservedIgnition) + } + if logLevelIgnition != nil { + tempIgnConfig.Storage.Files = append(tempIgnConfig.Storage.Files, *logLevelIgnition) + } + if kubeletIgnition != nil { + tempIgnConfig.Storage.Files = append(tempIgnConfig.Storage.Files, *kubeletIgnition) + } + + rawIgn, err := json.Marshal(tempIgnConfig) + if err != nil { + return nil, err + } + managedKey, err := generateBootstrapManagedKeyKubelet(pool, managedKeyExist) + if err != nil { + return nil, err + } + ignConfig := ctrlcommon.NewIgnConfig() + mc, err := ctrlcommon.MachineConfigFromIgnConfig(role, managedKey, ignConfig) + if err != nil { + return nil, fmt.Errorf("could not create MachineConfig from new Ignition config: %v", err) + } + mc.Spec.Config.Raw = rawIgn + mc.SetAnnotations(map[string]string{ + ctrlcommon.GeneratedByControllerVersionAnnotationKey: version.Hash, + }) + oref := metav1.OwnerReference{ + APIVersion: controllerKind.GroupVersion().String(), + Kind: controllerKind.Kind, + } + mc.SetOwnerReferences([]metav1.OwnerReference{oref}) + res = append(res, mc) + } + } + return res, nil +} + +// generateBootstrapManagedKeyKubelet generates the machine config name for a CR during bootstrap, returns error if there's more than 1 kubeletconfigs fir the same pool +// Note: Only one kubeletconfig manifest per pool is allowed for bootstrap mode for the following reason: +// if you provide multiple per pool, they would overwrite each other and not merge, potentially confusing customers post install; +// we can simplify the logic for the bootstrap generation and avoid some edge cases. +func generateBootstrapManagedKeyKubelet(pool *mcfgv1.MachineConfigPool, managedKeyExist map[string]bool) (string, error) { + if _, ok := managedKeyExist[pool.Name]; ok { + return "", fmt.Errorf("Error found multiple KubeletConfigs targeting MachineConfigPool %v. Please apply only one KubeletConfig manifest for each pool during installation", pool.Name) + } + managedKey, err := ctrlcommon.GetManagedKey(pool, nil, "99", "kubelet", "") + if err != nil { + return "", err + } + managedKeyExist[pool.Name] = true + return managedKey, nil +} diff --git a/pkg/controller/kubelet-config/kubelet_config_bootstrap_test.go b/pkg/controller/kubelet-config/kubelet_config_bootstrap_test.go new file mode 100644 index 0000000000..d5aee588da --- /dev/null +++ b/pkg/controller/kubelet-config/kubelet_config_bootstrap_test.go @@ -0,0 +1,196 @@ +package kubeletconfig + +import ( + "fmt" + "testing" + + configv1 "github.com/openshift/api/config/v1" + mcfgv1 "github.com/openshift/machine-config-operator/pkg/apis/machineconfiguration.openshift.io/v1" + ctrlcommon "github.com/openshift/machine-config-operator/pkg/controller/common" + "github.com/openshift/machine-config-operator/test/helpers" + "github.com/stretchr/testify/require" + "github.com/vincent-petithory/dataurl" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + kubeletconfigv1beta1 "k8s.io/kubelet/config/v1beta1" +) + +func TestRunKubeletBootstrap(t *testing.T) { + customSelector := metav1.AddLabelToSelector(&metav1.LabelSelector{}, "node-role/custom", "") + + for _, platform := range []configv1.PlatformType{configv1.AWSPlatformType, configv1.NonePlatformType, "unrecognized"} { + t.Run(string(platform), func(t *testing.T) { + cc := newControllerConfig(ctrlcommon.ControllerConfigName, platform) + pools := []*mcfgv1.MachineConfigPool{ + helpers.NewMachineConfigPool("master", nil, helpers.MasterSelector, "v0"), + helpers.NewMachineConfigPool("worker", nil, helpers.WorkerSelector, "v0"), + helpers.NewMachineConfigPool("custom", nil, customSelector, "v0"), + } + + kcRaw, err := EncodeKubeletConfig(&kubeletconfigv1beta1.KubeletConfiguration{MaxPods: 100}, kubeletconfigv1beta1.SchemeGroupVersion) + if err != nil { + panic(err) + } + // kubeletconfigs for master, worker, custom pool respectively + expectedMCNames := []string{"99-master-generated-kubelet", "99-worker-generated-kubelet", "99-custom-generated-kubelet"} + cfgs := []*mcfgv1.KubeletConfig{ + { + ObjectMeta: metav1.ObjectMeta{Name: "kcfg-master"}, + Spec: mcfgv1.KubeletConfigSpec{ + KubeletConfig: &runtime.RawExtension{ + Raw: kcRaw, + }, + MachineConfigPoolSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "pools.operator.machineconfiguration.openshift.io/master": "", + }, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "kcfg-worker"}, + Spec: mcfgv1.KubeletConfigSpec{ + KubeletConfig: &runtime.RawExtension{ + Raw: kcRaw, + }, + MachineConfigPoolSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "pools.operator.machineconfiguration.openshift.io/worker": "", + }, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "kcfg-custom", Labels: map[string]string{"node-role/custom": ""}}, + Spec: mcfgv1.KubeletConfigSpec{ + KubeletConfig: &runtime.RawExtension{ + Raw: kcRaw, + }, + MachineConfigPoolSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "pools.operator.machineconfiguration.openshift.io/custom": "", + }, + }, + }, + }, + } + + mcs, err := RunKubeletBootstrap("../../../templates", cfgs, cc, nil, pools) + require.NoError(t, err) + require.Len(t, mcs, len(cfgs)) + + for i := range mcs { + require.Equal(t, expectedMCNames[i], mcs[i].Name) + verifyKubeletConfigJSONContents(t, mcs[i], mcs[i].Name, cc.Spec.ReleaseImage) + } + }) + } +} + +func verifyKubeletConfigJSONContents(t *testing.T, mc *mcfgv1.MachineConfig, mcName string, releaseImageReg string) { + ignCfg, err := ctrlcommon.ParseAndConvertConfig(mc.Spec.Config.Raw) + require.NoError(t, err) + regfile := ignCfg.Storage.Files[0] + conf, err := dataurl.DecodeString(*regfile.Contents.Source) + require.NoError(t, err) + require.Contains(t, string(conf.Data), `"maxPods": 100`) +} + +func TestGenerateDefaultManagedKeyKubelet(t *testing.T) { + workerPool := helpers.NewMachineConfigPool("worker", nil, helpers.WorkerSelector, "v0") + masterPool := helpers.NewMachineConfigPool("master", nil, helpers.WorkerSelector, "v0") + kcRaw, err := EncodeKubeletConfig(&kubeletconfigv1beta1.KubeletConfiguration{MaxPods: 100}, kubeletconfigv1beta1.SchemeGroupVersion) + if err != nil { + panic(err) + } + + // valid case, only 1 kubeletconfig per pool + managedKeyExist := make(map[string]bool) + for _, tc := range []struct { + kubeletconfig *mcfgv1.KubeletConfig + pool *mcfgv1.MachineConfigPool + expectedManagedKey string + }{ + { + &mcfgv1.KubeletConfig{ + ObjectMeta: metav1.ObjectMeta{Name: "kcfg-default"}, + Spec: mcfgv1.KubeletConfigSpec{ + KubeletConfig: &runtime.RawExtension{ + Raw: kcRaw, + }, + }, + }, + workerPool, + "99-worker-generated-kubelet", + }, + { + &mcfgv1.KubeletConfig{ + ObjectMeta: metav1.ObjectMeta{Name: "kcfg-default"}, + Spec: mcfgv1.KubeletConfigSpec{ + KubeletConfig: &runtime.RawExtension{ + Raw: kcRaw, + }, + }, + }, + masterPool, + "99-master-generated-kubelet", // kubeletconfig apply to master pool, expected managedKey for master pool + }, + } { + res, err := generateBootstrapManagedKeyKubelet(tc.pool, managedKeyExist) + require.NoError(t, err) + require.Equal(t, tc.expectedManagedKey, res) + } + + // error case, 2 kubeletconfig applied for master pool + managedKeyExist = make(map[string]bool) + for _, tc := range []struct { + kubeletconfig *mcfgv1.KubeletConfig + pool *mcfgv1.MachineConfigPool + expectedManagedKey string + expectedErr error + }{ + { + &mcfgv1.KubeletConfig{ + ObjectMeta: metav1.ObjectMeta{Name: "kcfg-default"}, + Spec: mcfgv1.KubeletConfigSpec{ + KubeletConfig: &runtime.RawExtension{ + Raw: kcRaw, + }, + }, + }, + workerPool, + "99-worker-generated-kubelet", + nil, + }, + { + &mcfgv1.KubeletConfig{ + ObjectMeta: metav1.ObjectMeta{Name: "kcfg-default"}, + Spec: mcfgv1.KubeletConfigSpec{ + KubeletConfig: &runtime.RawExtension{ + Raw: kcRaw, + }, + }, + }, + masterPool, + "99-master-generated-kubelet", // kubeletconfig apply to master pool, expected managedKey for master pool + nil, + }, + { + &mcfgv1.KubeletConfig{ + ObjectMeta: metav1.ObjectMeta{Name: "kcfg-1"}, + Spec: mcfgv1.KubeletConfigSpec{ + KubeletConfig: &runtime.RawExtension{ + Raw: kcRaw, + }, + }, + }, + masterPool, + "", + fmt.Errorf("Error found multiple KubeletConfigs targeting MachineConfigPool master. Please apply only one KubeletConfig manifest for each pool during installation"), + }, + } { + res, err := generateBootstrapManagedKeyKubelet(tc.pool, managedKeyExist) + require.Equal(t, tc.expectedErr, err) + require.Equal(t, tc.expectedManagedKey, res) + } +} diff --git a/pkg/controller/kubelet-config/kubelet_config_controller.go b/pkg/controller/kubelet-config/kubelet_config_controller.go index e69a18cecc..bf4f4dbb07 100644 --- a/pkg/controller/kubelet-config/kubelet_config_controller.go +++ b/pkg/controller/kubelet-config/kubelet_config_controller.go @@ -529,9 +529,6 @@ func (ctrl *Controller) syncKubeletConfig(key string) error { } isNotFound := macherrors.IsNotFound(err) - var kubeletIgnition *ign3types.File - var logLevelIgnition *ign3types.File - var autoSizingReservedIgnition *ign3types.File userDefinedSystemReserved := make(map[string]string, 2) // Generate the original KubeletConfig @@ -562,37 +559,9 @@ func (ctrl *Controller) syncKubeletConfig(key string) error { originalKubeConfig.TLSMinVersion = observedMinTLSVersion originalKubeConfig.TLSCipherSuites = observedCipherSuites - if cfg.Spec.KubeletConfig != nil && cfg.Spec.KubeletConfig.Raw != nil { - specKubeletConfig, err := decodeKubeletConfig(cfg.Spec.KubeletConfig.Raw) - if err != nil { - return ctrl.syncStatusOnly(cfg, err, "could not deserialize the new Kubelet config: %v", err) - } - - if val, ok := specKubeletConfig.SystemReserved["memory"]; ok { - userDefinedSystemReserved["memory"] = val - delete(specKubeletConfig.SystemReserved, "memory") - } - - if val, ok := specKubeletConfig.SystemReserved["cpu"]; ok { - userDefinedSystemReserved["cpu"] = val - delete(specKubeletConfig.SystemReserved, "cpu") - } - - // FeatureGates must be set from the FeatureGate. - // Remove them here to prevent the specKubeletConfig merge overwriting them. - specKubeletConfig.FeatureGates = nil - - // Merge the Old and New - err = mergo.Merge(originalKubeConfig, specKubeletConfig, mergo.WithOverride) - if err != nil { - return ctrl.syncStatusOnly(cfg, err, "could not merge original config and new config: %v", err) - } - } - - // Encode the new config into an Ignition File - kubeletIgnition, err = kubeletConfigToIgnFile(originalKubeConfig) + kubeletIgnition, logLevelIgnition, autoSizingReservedIgnition, err := generateKubeletIgnFiles(cfg, originalKubeConfig, userDefinedSystemReserved) if err != nil { - return ctrl.syncStatusOnly(cfg, err, "could not encode JSON: %v", err) + return ctrl.syncStatusOnly(cfg, err) } if isNotFound { @@ -616,18 +585,6 @@ func (ctrl *Controller) syncKubeletConfig(key string) error { } } - if cfg.Spec.LogLevel != nil { - logLevelIgnition = createNewKubeletLogLevelIgnition(*cfg.Spec.LogLevel) - } - - if cfg.Spec.AutoSizingReserved != nil && len(userDefinedSystemReserved) == 0 { - autoSizingReservedIgnition = createNewKubeletDynamicSystemReservedIgnition(cfg.Spec.AutoSizingReserved, userDefinedSystemReserved) - } - - if len(userDefinedSystemReserved) > 0 { - autoSizingReservedIgnition = createNewKubeletDynamicSystemReservedIgnition(nil, userDefinedSystemReserved) - } - tempIgnConfig := ctrlcommon.NewIgnConfig() if autoSizingReservedIgnition != nil { tempIgnConfig.Storage.Files = append(tempIgnConfig.Storage.Files, *autoSizingReservedIgnition) diff --git a/pkg/controller/kubelet-config/kubelet_config_features.go b/pkg/controller/kubelet-config/kubelet_config_features.go index 9375328251..af83c31868 100644 --- a/pkg/controller/kubelet-config/kubelet_config_features.go +++ b/pkg/controller/kubelet-config/kubelet_config_features.go @@ -175,6 +175,9 @@ func (ctrl *Controller) deleteFeature(obj interface{}) { // generateFeatureMap returns a map of enabled/disabled feature gate selection with exclusion list func generateFeatureMap(features *osev1.FeatureGate, exclusions ...string) (*map[string]bool, error) { rv := make(map[string]bool) + if features == nil { + features = createNewDefaultFeatureGate() + } set, ok := osev1.FeatureSets[features.Spec.FeatureSet] if !ok { return &rv, fmt.Errorf("enabled FeatureSet %v does not have a corresponding config", features.Spec.FeatureSet)