diff --git a/cmd/kubeadm/app/apis/kubeadm/validation/validation.go b/cmd/kubeadm/app/apis/kubeadm/validation/validation.go index 23c0590d7c3..9713461c4cc 100644 --- a/cmd/kubeadm/app/apis/kubeadm/validation/validation.go +++ b/cmd/kubeadm/app/apis/kubeadm/validation/validation.go @@ -638,15 +638,21 @@ func ValidateCertificateKey(certificateKey string, fldPath *field.Path) field.Er // ValidateIgnorePreflightErrors validates duplicates in: // - ignore-preflight-errors flag and // - ignorePreflightErrors field in {Init,Join}Configuration files. -func ValidateIgnorePreflightErrors(ignorePreflightErrorsFromCLI, ignorePreflightErrorsFromConfigFile []string) (sets.Set[string], error) { +// and make sure ignore "all" cannot configured with individual checks. +func ValidateIgnorePreflightErrors(featureList map[string]bool, ignorePreflightErrorsFromCLI, ignorePreflightErrorsFromConfigFile []string) (sets.Set[string], error) { ignoreErrors := sets.New[string]() allErrs := field.ErrorList{} + ignorePreflightErr := ignorePreflightErrorsFromConfigFile - for _, item := range ignorePreflightErrorsFromConfigFile { - ignoreErrors.Insert(strings.ToLower(item)) // parameters are case insensitive + if ignorePreflightErrorsFromCLI != nil { + if features.Enabled(featureList, features.MergeCLIArgumentsWithConfig) { + ignorePreflightErr = append(ignorePreflightErr, ignorePreflightErrorsFromCLI...) + } else { + ignorePreflightErr = ignorePreflightErrorsFromCLI + } } - for _, item := range ignorePreflightErrorsFromCLI { + for _, item := range ignorePreflightErr { ignoreErrors.Insert(strings.ToLower(item)) // parameters are case insensitive } diff --git a/cmd/kubeadm/app/apis/kubeadm/validation/validation_test.go b/cmd/kubeadm/app/apis/kubeadm/validation/validation_test.go index c6ddf38e72b..ca72a8ddbf5 100644 --- a/cmd/kubeadm/app/apis/kubeadm/validation/validation_test.go +++ b/cmd/kubeadm/app/apis/kubeadm/validation/validation_test.go @@ -29,6 +29,7 @@ import ( kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" kubeadmapiv1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta3" + "k8s.io/kubernetes/cmd/kubeadm/app/features" ) func TestValidateToken(t *testing.T) { @@ -799,17 +800,14 @@ func TestValidateIgnorePreflightErrors(t *testing.T) { ignorePreflightErrorsFromConfigFile []string expectedSet sets.Set[string] expectedError bool + mergeCLIArgumentsWithConfig bool }{ + // FG is off { // empty lists in CLI and config file []string{}, []string{}, sets.New[string](), false, - }, - { // empty list in CLI only - []string{}, - []string{"a"}, - sets.New("a"), false, }, { // empty list in config file only @@ -817,64 +815,103 @@ func TestValidateIgnorePreflightErrors(t *testing.T) { []string{}, sets.New("a"), false, + false, }, { // no duplicates, no overlap []string{"a", "b"}, []string{"c", "d"}, - sets.New("a", "b", "c", "d"), + sets.New("a", "b"), + false, false, }, { // some duplicates, with some overlapping duplicates []string{"a", "b", "a"}, []string{"c", "b"}, - sets.New("a", "b", "c"), + sets.New("a", "b"), + false, false, }, - { // empty list in CLI, but 'all' present in config file - []string{}, + { // CLI is not set, but 'all' present in config file + nil, []string{"all"}, sets.New("all"), false, + false, }, { // empty list in config file, but 'all' present in CLI []string{"all"}, []string{}, sets.New("all"), false, + false, }, { // some duplicates, only 'all' present in CLI and config file []string{"all"}, []string{"all"}, sets.New("all"), false, + false, }, { // non-duplicate, but 'all' present together with individual checks in CLI []string{"a", "b", "all"}, []string{}, sets.New[string](), true, + false, }, { // non-duplicate, but 'all' present together with individual checks in config file - []string{}, + nil, []string{"a", "b", "all"}, sets.New[string](), true, + false, }, - { // non-duplicate, but 'all' present in config file, while values are in CLI, which is forbidden + { // non-duplicate, but 'all' present in config file, while values are in CLI, "all" from config file will be ignored []string{"a", "b"}, []string{"all"}, - sets.New[string](), - true, + sets.New[string]("a", "b"), + false, + false, }, - { // non-duplicate, but 'all' present in CLI, while values are in config file, which is forbidden + { // non-duplicate, but 'all' present in CLI, while values are in config file, values from config file will be ignored []string{"all"}, []string{"a", "b"}, + sets.New[string]("all"), + false, + false, + }, + { // set from CLI will take precedence of the config file + []string{"a", "b"}, + []string{"c", "d"}, + sets.New[string]("a", "b"), + false, + false, + }, + { // empty list in CLI only + []string{}, + []string{"a"}, sets.New[string](), + false, + false, + }, + { // flag from CLI is not set + nil, + []string{"c", "d"}, + sets.New[string]("c", "d"), + false, + false, + }, + // FG is on + { + []string{"a", "b"}, + []string{"c", "d"}, + sets.New[string]("a", "b", "c", "d"), + false, true, }, } for _, rt := range tests { - result, err := ValidateIgnorePreflightErrors(rt.ignorePreflightErrorsFromCLI, rt.ignorePreflightErrorsFromConfigFile) + result, err := ValidateIgnorePreflightErrors(map[string]bool{features.MergeCLIArgumentsWithConfig: rt.mergeCLIArgumentsWithConfig}, rt.ignorePreflightErrorsFromCLI, rt.ignorePreflightErrorsFromConfigFile) switch { case err != nil && !rt.expectedError: t.Errorf("ValidateIgnorePreflightErrors: unexpected error for input (%s, %s), error: %v", rt.ignorePreflightErrorsFromCLI, rt.ignorePreflightErrorsFromConfigFile, err) diff --git a/cmd/kubeadm/app/cmd/init.go b/cmd/kubeadm/app/cmd/init.go index 1b3940cd38e..68feae87bda 100644 --- a/cmd/kubeadm/app/cmd/init.go +++ b/cmd/kubeadm/app/cmd/init.go @@ -306,7 +306,7 @@ func newInitData(cmd *cobra.Command, args []string, initOptions *initOptions, ou return nil, err } - ignorePreflightErrorsSet, err := validation.ValidateIgnorePreflightErrors(initOptions.ignorePreflightErrors, cfg.NodeRegistration.IgnorePreflightErrors) + ignorePreflightErrorsSet, err := validation.ValidateIgnorePreflightErrors(cfg.FeatureGates, initOptions.ignorePreflightErrors, cfg.NodeRegistration.IgnorePreflightErrors) if err != nil { return nil, err } diff --git a/cmd/kubeadm/app/cmd/init_test.go b/cmd/kubeadm/app/cmd/init_test.go index ef380388af2..65427881199 100644 --- a/cmd/kubeadm/app/cmd/init_test.go +++ b/cmd/kubeadm/app/cmd/init_test.go @@ -181,7 +181,7 @@ func TestNewInitData(t *testing.T) { options.CfgPath: configFilePath, options.IgnorePreflightErrors: "a,b", }, - validate: expectedInitIgnorePreflightErrors("a", "b", "c", "d"), + validate: expectedInitIgnorePreflightErrors("a", "b"), }, } for _, tc := range testCases { diff --git a/cmd/kubeadm/app/cmd/join.go b/cmd/kubeadm/app/cmd/join.go index efde491ad64..3ee4d55493e 100644 --- a/cmd/kubeadm/app/cmd/join.go +++ b/cmd/kubeadm/app/cmd/join.go @@ -225,7 +225,7 @@ func newCmdJoin(out io.Writer, joinOptions *joinOptions) *cobra.Command { // assume that the command execution does not depend on CRISocket when --cri-socket flag is not set joinOptions.externalcfg.NodeRegistration.CRISocket = kubeadmconstants.UnknownCRISocket } - data, err := newJoinData(cmd, args, joinOptions, out, kubeadmconstants.GetAdminKubeConfigPath()) + data, err := newJoinData(cmd, args, joinOptions, out, kubeadmconstants.GetAdminKubeConfigPath(), false) if err != nil { return nil, err } @@ -334,8 +334,10 @@ func newJoinOptions() *joinOptions { // newJoinData returns a new joinData struct to be used for the execution of the kubeadm join workflow. // This func takes care of validating joinOptions passed to the command, and then it converts -// options into the internal JoinConfiguration type that is used as input all the phases in the kubeadm join workflow -func newJoinData(cmd *cobra.Command, args []string, opt *joinOptions, out io.Writer, adminKubeConfigPath string) (*joinData, error) { +// options into the internal JoinConfiguration type that is used as input all the phases in the kubeadm join workflow. +// Set the lazilyFetchInitCfg to true to lazily fetch the InitConfiguration from cluster, e.g. unittest to test joinData +// without the interaction with cluster. +func newJoinData(cmd *cobra.Command, args []string, opt *joinOptions, out io.Writer, adminKubeConfigPath string, lazilyFetchInitCfg bool) (*joinData, error) { // Validate the mixed arguments with --config and return early on errors if err := validation.ValidateMixedArguments(cmd.Flags()); err != nil { @@ -431,7 +433,18 @@ func newJoinData(cmd *cobra.Command, args []string, opt *joinOptions, out io.Wri return nil, err } - ignorePreflightErrorsSet, err := validation.ValidateIgnorePreflightErrors(opt.ignorePreflightErrors, cfg.NodeRegistration.IgnorePreflightErrors) + initCfg := &kubeadmapi.InitConfiguration{} + if !lazilyFetchInitCfg { + if tlsBootstrapCfg == nil { + if tlsBootstrapCfg, err = discovery.For(cfg); err != nil { + return nil, err + } + } + if initCfg, err = fetchInitConfigurationFromJoinConfiguration(cfg, tlsBootstrapCfg); err != nil { + return nil, err + } + } + ignorePreflightErrorsSet, err := validation.ValidateIgnorePreflightErrors(initCfg.FeatureGates, opt.ignorePreflightErrors, cfg.NodeRegistration.IgnorePreflightErrors) if err != nil { return nil, err } @@ -463,6 +476,7 @@ func newJoinData(cmd *cobra.Command, args []string, opt *joinOptions, out io.Wri return &joinData{ cfg: cfg, dryRun: cmdutil.ValueFromFlagsOrConfig(cmd.Flags(), options.DryRun, cfg.DryRun, opt.dryRun).(bool), + initCfg: initCfg, tlsBootstrapCfg: tlsBootstrapCfg, ignorePreflightErrors: ignorePreflightErrorsSet, outputWriter: out, diff --git a/cmd/kubeadm/app/cmd/join_test.go b/cmd/kubeadm/app/cmd/join_test.go index 2487193d42b..d7f37ddae58 100644 --- a/cmd/kubeadm/app/cmd/join_test.go +++ b/cmd/kubeadm/app/cmd/join_test.go @@ -289,7 +289,7 @@ func TestNewJoinData(t *testing.T) { options.CfgPath: configFilePath, options.IgnorePreflightErrors: "a,b", }, - validate: expectedJoinIgnorePreflightErrors(sets.New("a", "b", "c", "d")), + validate: expectedJoinIgnorePreflightErrors(sets.New("a", "b")), }, { name: "warn if --control-plane flag is not set", @@ -335,7 +335,7 @@ func TestNewJoinData(t *testing.T) { } // test newJoinData method - data, err := newJoinData(cmd, tc.args, joinOptions, nil, kubeconfigFilePath) + data, err := newJoinData(cmd, tc.args, joinOptions, nil, kubeconfigFilePath, true) klog.Flush() msg := "WARNING: --control-plane is also required when passing control-plane" if tc.expectWarn { diff --git a/cmd/kubeadm/app/cmd/reset.go b/cmd/kubeadm/app/cmd/reset.go index d70fc1ddfdc..835d15bea61 100644 --- a/cmd/kubeadm/app/cmd/reset.go +++ b/cmd/kubeadm/app/cmd/reset.go @@ -104,7 +104,7 @@ func newResetData(cmd *cobra.Command, opts *resetOptions, in io.Reader, out io.W return nil, err } - var initCfg *kubeadmapi.InitConfiguration + initCfg := &kubeadmapi.InitConfiguration{} // Either use the config file if specified, or convert public kubeadm API to the internal ResetConfiguration and validates cfg. resetCfg, err := configutil.LoadOrDefaultResetConfiguration(opts.cfgPath, opts.externalcfg, allowExperimental) @@ -123,7 +123,7 @@ func newResetData(cmd *cobra.Command, opts *resetOptions, in io.Reader, out io.W klog.V(1).Infof("[reset] Could not obtain a client set from the kubeconfig file: %s", opts.kubeconfigPath) } - ignorePreflightErrorsSet, err := validation.ValidateIgnorePreflightErrors(opts.ignorePreflightErrors, resetCfg.IgnorePreflightErrors) + ignorePreflightErrorsSet, err := validation.ValidateIgnorePreflightErrors(initCfg.FeatureGates, opts.ignorePreflightErrors, resetCfg.IgnorePreflightErrors) if err != nil { return nil, err } diff --git a/cmd/kubeadm/app/cmd/reset_test.go b/cmd/kubeadm/app/cmd/reset_test.go index d2b8a1ec8d0..898eeb1f159 100644 --- a/cmd/kubeadm/app/cmd/reset_test.go +++ b/cmd/kubeadm/app/cmd/reset_test.go @@ -209,7 +209,7 @@ func TestNewResetData(t *testing.T) { options.CfgPath: configFilePath, options.IgnorePreflightErrors: "c,d", }, - validate: expectedResetIgnorePreflightErrors(sets.New("a", "b", "c", "d")), + validate: expectedResetIgnorePreflightErrors(sets.New("c", "d")), }, } for _, tc := range testCases { diff --git a/cmd/kubeadm/app/cmd/upgrade/common.go b/cmd/kubeadm/app/cmd/upgrade/common.go index eec3652021e..d639a07baaa 100644 --- a/cmd/kubeadm/app/cmd/upgrade/common.go +++ b/cmd/kubeadm/app/cmd/upgrade/common.go @@ -175,7 +175,7 @@ func enforceRequirements(flags *applyPlanFlags, args []string, dryRun bool, upgr } } - ignorePreflightErrorsSet, err := validation.ValidateIgnorePreflightErrors(flags.ignorePreflightErrors, cfg.NodeRegistration.IgnorePreflightErrors) + ignorePreflightErrorsSet, err := validation.ValidateIgnorePreflightErrors(cfg.FeatureGates, flags.ignorePreflightErrors, cfg.NodeRegistration.IgnorePreflightErrors) if err != nil { return nil, nil, nil, err } diff --git a/cmd/kubeadm/app/cmd/upgrade/node.go b/cmd/kubeadm/app/cmd/upgrade/node.go index 0523172d339..50b8ca4e22a 100644 --- a/cmd/kubeadm/app/cmd/upgrade/node.go +++ b/cmd/kubeadm/app/cmd/upgrade/node.go @@ -153,7 +153,7 @@ func newNodeData(cmd *cobra.Command, args []string, options *nodeOptions, out io return nil, errors.Wrap(err, "unable to fetch the kubeadm-config ConfigMap") } - ignorePreflightErrorsSet, err := validation.ValidateIgnorePreflightErrors(options.ignorePreflightErrors, cfg.NodeRegistration.IgnorePreflightErrors) + ignorePreflightErrorsSet, err := validation.ValidateIgnorePreflightErrors(cfg.FeatureGates, options.ignorePreflightErrors, cfg.NodeRegistration.IgnorePreflightErrors) if err != nil { return nil, err } diff --git a/cmd/kubeadm/app/features/features.go b/cmd/kubeadm/app/features/features.go index 5c007a708d4..b2a02776629 100644 --- a/cmd/kubeadm/app/features/features.go +++ b/cmd/kubeadm/app/features/features.go @@ -38,6 +38,8 @@ const ( EtcdLearnerMode = "EtcdLearnerMode" // UpgradeAddonsBeforeControlPlane is expected to be in deprecated in v1.28 and will be removed in future release UpgradeAddonsBeforeControlPlane = "UpgradeAddonsBeforeControlPlane" + // MergeCLIArgumentsWithConfig is considered to be in deprecated in v1.29 and is expected to be removed in 1.30 + MergeCLIArgumentsWithConfig = "MergeCLIArgumentsWithConfig" ) // InitFeatureGates are the default feature gates for the init command @@ -45,6 +47,10 @@ var InitFeatureGates = FeatureList{ PublicKeysECDSA: {FeatureSpec: featuregate.FeatureSpec{Default: false, PreRelease: featuregate.Alpha}}, RootlessControlPlane: {FeatureSpec: featuregate.FeatureSpec{Default: false, PreRelease: featuregate.Alpha}}, EtcdLearnerMode: {FeatureSpec: featuregate.FeatureSpec{Default: true, PreRelease: featuregate.Beta}}, + MergeCLIArgumentsWithConfig: { + FeatureSpec: featuregate.FeatureSpec{Default: false, PreRelease: featuregate.Deprecated}, + DeprecationMessage: "The MergeCLIArgumentsWithConfig feature gate is deprecated and will be removed in a future release.", + }, UpgradeAddonsBeforeControlPlane: { FeatureSpec: featuregate.FeatureSpec{Default: false, PreRelease: featuregate.Deprecated}, DeprecationMessage: "The UpgradeAddonsBeforeControlPlane feature gate is deprecated and will be removed in a future release.",