diff --git a/cmd/kubeadm/app/cmd/cmd.go b/cmd/kubeadm/app/cmd/cmd.go index 17b827f405..602f61fb09 100644 --- a/cmd/kubeadm/app/cmd/cmd.go +++ b/cmd/kubeadm/app/cmd/cmd.go @@ -82,7 +82,7 @@ func NewKubeadmCommand(in io.Reader, out, err io.Writer) *cobra.Command { cmds.AddCommand(NewCmdCompletion(out, "")) cmds.AddCommand(NewCmdConfig(out)) - cmds.AddCommand(NewCmdInit(out)) + cmds.AddCommand(NewCmdInit(out, nil)) cmds.AddCommand(NewCmdJoin(out)) cmds.AddCommand(NewCmdReset(in, out)) cmds.AddCommand(NewCmdVersion(out)) diff --git a/cmd/kubeadm/app/cmd/init.go b/cmd/kubeadm/app/cmd/init.go index 22939087d9..9e6dbc714a 100644 --- a/cmd/kubeadm/app/cmd/init.go +++ b/cmd/kubeadm/app/cmd/init.go @@ -124,8 +124,12 @@ type initData struct { } // NewCmdInit returns "kubeadm init" command. -func NewCmdInit(out io.Writer) *cobra.Command { - initOptions := newInitOptions() +// NB. initOptions is exposed as parameter for allowing unit testing of +// the newInitOptions method, that implements all the command options validation logic +func NewCmdInit(out io.Writer, initOptions *initOptions) *cobra.Command { + if initOptions == nil { + initOptions = newInitOptions() + } initRunner := workflow.NewRunner() cmd := &cobra.Command{ @@ -192,7 +196,7 @@ func NewCmdInit(out io.Writer) *cobra.Command { func AddInitConfigFlags(flagSet *flag.FlagSet, cfg *kubeadmapiv1beta1.InitConfiguration, featureGatesString *string) { flagSet.StringVar( &cfg.LocalAPIEndpoint.AdvertiseAddress, options.APIServerAdvertiseAddress, cfg.LocalAPIEndpoint.AdvertiseAddress, - "The IP address the API Server will advertise it's listening on. Specify '0.0.0.0' to use the address of the default network interface.", + "The IP address the API Server will advertise it's listening on. If not set the default network interface will be used.", ) flagSet.Int32Var( &cfg.LocalAPIEndpoint.BindPort, options.APIServerBindPort, cfg.LocalAPIEndpoint.BindPort, @@ -289,7 +293,9 @@ func newInitData(cmd *cobra.Command, options *initOptions, out io.Writer) (initD } ignorePreflightErrorsSet, err := validation.ValidateIgnorePreflightErrors(options.ignorePreflightErrors) - kubeadmutil.CheckErr(err) + if err != nil { + return initData{}, err + } if err = validation.ValidateMixedArguments(cmd.Flags()); err != nil { return initData{}, err diff --git a/cmd/kubeadm/app/cmd/init_test.go b/cmd/kubeadm/app/cmd/init_test.go new file mode 100644 index 0000000000..4c9e72dfa2 --- /dev/null +++ b/cmd/kubeadm/app/cmd/init_test.go @@ -0,0 +1,159 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "testing" + + "k8s.io/kubernetes/cmd/kubeadm/app/cmd/options" + "k8s.io/kubernetes/cmd/kubeadm/app/features" +) + +const ( + testInitConfig = `--- +apiVersion: kubeadm.k8s.io/v1beta1 +kind: InitConfiguration +localAPIEndpoint: + advertiseAddress: "1.2.3.4" +bootstrapTokens: +- token: "abcdef.0123456789abcdef" +nodeRegistration: + criSocket: /run/containerd/containerd.sock + name: someName +--- +apiVersion: kubeadm.k8s.io/v1beta1 +kind: ClusterConfiguration +controlPlaneEndpoint: "3.4.5.6" +` +) + +func TestNewInitData(t *testing.T) { + // create temp directory + tmpDir, err := ioutil.TempDir("", "kubeadm-init-test") + if err != nil { + t.Errorf("Unable to create temporary directory: %v", err) + } + defer os.RemoveAll(tmpDir) + + // create config file + configFilePath := filepath.Join(tmpDir, "test-config-file") + cfgFile, err := os.Create(configFilePath) + if err != nil { + t.Errorf("Unable to create file %q: %v", configFilePath, err) + } + defer cfgFile.Close() + if _, err = cfgFile.WriteString(testInitConfig); err != nil { + t.Fatalf("Unable to write file %q: %v", configFilePath, err) + } + + testCases := []struct { + name string + args []string + flags map[string]string + validate func(*testing.T, *initData) + expectError bool + }{ + // Init data passed using flags + { + name: "pass without any flag (use defaults)", + }, + { + name: "fail if unknown feature gates flag are passed", + flags: map[string]string{ + options.FeatureGatesString: "unknown=true", + }, + expectError: true, + }, + { + name: "fail if deprecetes feature gates are set", + flags: map[string]string{ + options.FeatureGatesString: fmt.Sprintf("%s=true", features.CoreDNS), + }, + expectError: true, + }, + { + name: "fails if invalid preflight checks are provided", + flags: map[string]string{ + options.IgnorePreflightErrors: "all,something-else", + }, + expectError: true, + }, + + // Init data passed using config file + { + name: "Pass with config from file", + flags: map[string]string{ + options.CfgPath: configFilePath, + }, + }, + { + name: "--cri-socket and --node-name flags override config from file", + flags: map[string]string{ + options.CfgPath: configFilePath, + options.NodeCRISocket: "/var/run/crio/crio.sock", + options.NodeName: "anotherName", + }, + validate: func(t *testing.T, data *initData) { + // validate that cri-socket and node-name are overwritten + if data.cfg.NodeRegistration.CRISocket != "/var/run/crio/crio.sock" { + t.Errorf("Invalid NodeRegistration.CRISocket") + } + if data.cfg.NodeRegistration.Name != "anotherName" { + t.Errorf("Invalid NodeRegistration.Name") + } + }, + }, + { + name: "fail if mixedArguments are passed", + flags: map[string]string{ + options.CfgPath: configFilePath, + options.APIServerAdvertiseAddress: "1.2.3.4", + }, + expectError: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // initialize an external init option and inject it to the init cmd + initOptions := newInitOptions() + cmd := NewCmdInit(nil, initOptions) + + // sets cmd flags (that will be reflected on the init options) + for f, v := range tc.flags { + cmd.Flags().Set(f, v) + } + + // test newInitData method + data, err := newInitData(cmd, initOptions, nil) + if err != nil && !tc.expectError { + t.Fatalf("newInitData returned unexpected error: %v", err) + } + if err == nil && tc.expectError { + t.Fatalf("newInitData didn't return error when expected") + } + + // exec additional validation on the returned value + if tc.validate != nil { + tc.validate(t, &data) + } + }) + } +}