diff --git a/cmd/kubeadm/app/cmd/BUILD b/cmd/kubeadm/app/cmd/BUILD index 6c1db7d23f..32f48674e5 100644 --- a/cmd/kubeadm/app/cmd/BUILD +++ b/cmd/kubeadm/app/cmd/BUILD @@ -88,8 +88,6 @@ go_test( ], embed = [":go_default_library"], deps = [ - "//cmd/kubeadm/app/apis/kubeadm:go_default_library", - "//cmd/kubeadm/app/apis/kubeadm/scheme:go_default_library", "//cmd/kubeadm/app/apis/kubeadm/v1beta1:go_default_library", "//cmd/kubeadm/app/apis/kubeadm/validation:go_default_library", "//cmd/kubeadm/app/cmd/options:go_default_library", diff --git a/cmd/kubeadm/app/cmd/cmd.go b/cmd/kubeadm/app/cmd/cmd.go index 602f61fb09..c97f3166b1 100644 --- a/cmd/kubeadm/app/cmd/cmd.go +++ b/cmd/kubeadm/app/cmd/cmd.go @@ -83,7 +83,7 @@ func NewKubeadmCommand(in io.Reader, out, err io.Writer) *cobra.Command { cmds.AddCommand(NewCmdCompletion(out, "")) cmds.AddCommand(NewCmdConfig(out)) cmds.AddCommand(NewCmdInit(out, nil)) - cmds.AddCommand(NewCmdJoin(out)) + cmds.AddCommand(NewCmdJoin(out, nil)) cmds.AddCommand(NewCmdReset(in, out)) cmds.AddCommand(NewCmdVersion(out)) cmds.AddCommand(NewCmdToken(out, err)) diff --git a/cmd/kubeadm/app/cmd/join.go b/cmd/kubeadm/app/cmd/join.go index b80fe5088a..69034ccff4 100644 --- a/cmd/kubeadm/app/cmd/join.go +++ b/cmd/kubeadm/app/cmd/join.go @@ -37,6 +37,7 @@ import ( kubeadmscheme "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/scheme" kubeadmapiv1beta1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta1" "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/validation" + "k8s.io/kubernetes/cmd/kubeadm/app/cmd/options" kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants" "k8s.io/kubernetes/cmd/kubeadm/app/discovery" certsphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs" @@ -152,206 +153,258 @@ var ( `) ) +// joinOptions defines all the options exposed via flags by kubeadm join. +// Please note that this structure includes the public kubeadm config API, but only a subset of the options +// supported by this api will be exposed as a flag. +type joinOptions struct { + cfgPath string + token string + controlPlane bool + ignorePreflightErrors []string + externalcfg *kubeadmapiv1beta1.JoinConfiguration +} + +// joinData defines all the runtime information used when running the kubeadm join worklow; +// this data is shared across all the phases that are included in the workflow. +type joinData struct { + cfg *kubeadmapi.JoinConfiguration + ignorePreflightErrors sets.String + outputWriter io.Writer +} + // NewCmdJoin returns "kubeadm join" command. -func NewCmdJoin(out io.Writer) *cobra.Command { - cfg := &kubeadmapiv1beta1.JoinConfiguration{} - kubeadmscheme.Scheme.Default(cfg) - - fd := &kubeadmapiv1beta1.FileDiscovery{} - btd := &kubeadmapiv1beta1.BootstrapTokenDiscovery{} - - var token string - var cfgPath string - var ignorePreflightErrors []string - var controlPlane bool - var advertiseAddress string - var bindPort int32 = kubeadmapiv1beta1.DefaultAPIBindPort +// NB. joinOptions is exposed as parameter for allowing unit testing of +// the newJoinData method, that implements all the command options validation logic +func NewCmdJoin(out io.Writer, joinOptions *joinOptions) *cobra.Command { + if joinOptions == nil { + joinOptions = newJoinOptions() + } cmd := &cobra.Command{ Use: "join", Short: "Run this on any machine you wish to join an existing cluster", Long: joinLongDescription, Run: func(cmd *cobra.Command, args []string) { - - if len(fd.KubeConfigPath) != 0 { - cfg.Discovery.File = fd - } else { - cfg.Discovery.BootstrapToken = btd - if len(cfg.Discovery.BootstrapToken.Token) == 0 { - cfg.Discovery.BootstrapToken.Token = token - } - if len(args) > 0 { - if len(cfgPath) == 0 && len(args) > 1 { - klog.Warningf("[join] WARNING: More than one API server endpoint supplied on command line %v. Using the first one.", args) - } - cfg.Discovery.BootstrapToken.APIServerEndpoint = args[0] - } - } - - if len(cfg.Discovery.TLSBootstrapToken) == 0 { - cfg.Discovery.TLSBootstrapToken = token - } - - if controlPlane { - cfg.ControlPlane = &kubeadmapiv1beta1.JoinControlPlane{ - LocalAPIEndpoint: kubeadmapiv1beta1.APIEndpoint{ - AdvertiseAddress: advertiseAddress, - BindPort: bindPort, - }, - } - } - - j, err := NewValidJoin(cmd.PersistentFlags(), cfg, cfgPath, ignorePreflightErrors) + joinData, err := newJoinData(cmd, args, joinOptions, out) + kubeadmutil.CheckErr(err) + + err = joinData.Run() kubeadmutil.CheckErr(err) - kubeadmutil.CheckErr(j.Run(out)) }, } - AddJoinConfigFlags(cmd.PersistentFlags(), cfg, &token) - AddJoinBootstrapTokenDiscoveryFlags(cmd.PersistentFlags(), btd) - AddJoinFileDiscoveryFlags(cmd.PersistentFlags(), fd) - AddJoinOtherFlags(cmd.PersistentFlags(), &cfgPath, &ignorePreflightErrors, &controlPlane, &advertiseAddress, &bindPort) + AddJoinConfigFlags(cmd.Flags(), joinOptions.externalcfg) + AddJoinOtherFlags(cmd.Flags(), &joinOptions.cfgPath, &joinOptions.ignorePreflightErrors, &joinOptions.controlPlane, &joinOptions.token) return cmd } -// NewValidJoin validates the command line that are passed to the cobra command -func NewValidJoin(flagSet *flag.FlagSet, cfg *kubeadmapiv1beta1.JoinConfiguration, cfgPath string, ignorePreflightErrors []string) (*Join, error) { - var err error - - if err = validation.ValidateMixedArguments(flagSet); err != nil { - return nil, err - } - - ignorePreflightErrorsSet, err := validation.ValidateIgnorePreflightErrors(ignorePreflightErrors) - if err != nil { - return nil, err - } - - return NewJoin(cfgPath, cfg, ignorePreflightErrorsSet) -} - // AddJoinConfigFlags adds join flags bound to the config to the specified flagset -func AddJoinConfigFlags(flagSet *flag.FlagSet, cfg *kubeadmapiv1beta1.JoinConfiguration, token *string) { +func AddJoinConfigFlags(flagSet *flag.FlagSet, cfg *kubeadmapiv1beta1.JoinConfiguration) { flagSet.StringVar( - &cfg.NodeRegistration.Name, "node-name", cfg.NodeRegistration.Name, - "Specify the node name.") + &cfg.NodeRegistration.Name, options.NodeName, cfg.NodeRegistration.Name, + `Specify the node name.`, + ) flagSet.StringVar( - token, "token", "", - "Use this token for both discovery-token and tls-bootstrap-token when those values are not provided.") - flagSet.StringVar( - &cfg.NodeRegistration.CRISocket, "cri-socket", cfg.NodeRegistration.CRISocket, + &cfg.NodeRegistration.CRISocket, options.NodeCRISocket, cfg.NodeRegistration.CRISocket, `Specify the CRI socket to connect to.`, ) + flagSet.StringVar( + &cfg.Discovery.TLSBootstrapToken, options.TLSBootstrapToken, cfg.Discovery.TLSBootstrapToken, + `Specify the token used to temporarily authenticate with the Kubernetes Master while joining the node.`, + ) + AddControlPlaneFlags(flagSet, cfg.ControlPlane) + AddJoinBootstrapTokenDiscoveryFlags(flagSet, cfg.Discovery.BootstrapToken) + AddJoinFileDiscoveryFlags(flagSet, cfg.Discovery.File) } // AddJoinBootstrapTokenDiscoveryFlags adds bootstrap token specific discovery flags to the specified flagset func AddJoinBootstrapTokenDiscoveryFlags(flagSet *flag.FlagSet, btd *kubeadmapiv1beta1.BootstrapTokenDiscovery) { flagSet.StringVar( - &btd.Token, "discovery-token", "", - "A token used to validate cluster information fetched from the API server.") + &btd.Token, options.TokenDiscovery, "", + "For token-based discovery, the token used to validate cluster information fetched from the API server.", + ) flagSet.StringSliceVar( - &btd.CACertHashes, "discovery-token-ca-cert-hash", []string{}, - "For token-based discovery, validate that the root CA public key matches this hash (format: \":\").") + &btd.CACertHashes, options.TokenDiscoveryCAHash, []string{}, + "For token-based discovery, validate that the root CA public key matches this hash (format: \":\").", + ) flagSet.BoolVar( - &btd.UnsafeSkipCAVerification, "discovery-token-unsafe-skip-ca-verification", false, - "For token-based discovery, allow joining without --discovery-token-ca-cert-hash pinning.") + &btd.UnsafeSkipCAVerification, options.TokenDiscoverySkipCAHash, false, + "For token-based discovery, allow joining without --discovery-token-ca-cert-hash pinning.", + ) } // AddJoinFileDiscoveryFlags adds file discovery flags to the specified flagset func AddJoinFileDiscoveryFlags(flagSet *flag.FlagSet, fd *kubeadmapiv1beta1.FileDiscovery) { flagSet.StringVar( - &fd.KubeConfigPath, "discovery-file", "", - "A file or URL from which to load cluster information.") + &fd.KubeConfigPath, options.FileDiscovery, "", + "For file-based discovery, a file or URL from which to load cluster information.", + ) +} + +// AddControlPlaneFlags adds file control plane flags to the specified flagset +func AddControlPlaneFlags(flagSet *flag.FlagSet, cp *kubeadmapiv1beta1.JoinControlPlane) { + flagSet.StringVar( + &cp.LocalAPIEndpoint.AdvertiseAddress, options.APIServerAdvertiseAddress, cp.LocalAPIEndpoint.AdvertiseAddress, + "If the node should host a new control plane instance, the IP address the API Server will advertise it's listening on. If not set the default network interface will be used.", + ) + flagSet.Int32Var( + &cp.LocalAPIEndpoint.BindPort, options.APIServerBindPort, cp.LocalAPIEndpoint.BindPort, + "If the node should host a new control plane instance, the port for the API Server to bind to.", + ) } // AddJoinOtherFlags adds join flags that are not bound to a configuration file to the given flagset -func AddJoinOtherFlags(flagSet *flag.FlagSet, cfgPath *string, ignorePreflightErrors *[]string, controlPlane *bool, advertiseAddress *string, bindPort *int32) { +func AddJoinOtherFlags(flagSet *flag.FlagSet, cfgPath *string, ignorePreflightErrors *[]string, controlPlane *bool, token *string) { flagSet.StringVar( - cfgPath, "config", *cfgPath, - "Path to kubeadm config file.") + cfgPath, options.CfgPath, *cfgPath, + "Path to kubeadm config file.", + ) flagSet.StringSliceVar( - ignorePreflightErrors, "ignore-preflight-errors", *ignorePreflightErrors, - "A list of checks whose errors will be shown as warnings. Example: 'IsPrivilegedUser,Swap'. Value 'all' ignores errors from all checks.") - flagSet.BoolVar( - controlPlane, "experimental-control-plane", *controlPlane, - "Create a new control plane instance on this node") + ignorePreflightErrors, options.IgnorePreflightErrors, *ignorePreflightErrors, + "A list of checks whose errors will be shown as warnings. Example: 'IsPrivilegedUser,Swap'. Value 'all' ignores errors from all checks.", + ) flagSet.StringVar( - advertiseAddress, "apiserver-advertise-address", *advertiseAddress, - "If the node should host a new control plane instance, the IP address the API Server will advertise it's listening on.") - flagSet.Int32Var( - bindPort, "apiserver-bind-port", *bindPort, - "If the node should host a new control plane instance, the port for the API Server to bind to.") + token, options.TokenStr, "", + "Use this token for both discovery-token and tls-bootstrap-token when those values are not provided.", + ) + flagSet.BoolVar( + controlPlane, options.ControlPlane, *controlPlane, + "Create a new control plane instance on this node", + ) } -// Join defines struct used by kubeadm join command -type Join struct { - cfg *kubeadmapi.JoinConfiguration - initCfg *kubeadmapi.InitConfiguration - tlsBootstrapCfg *clientcmdapi.Config - ignorePreflightErrors sets.String +// newJoinOptions returns a struct ready for being used for creating cmd join flags. +func newJoinOptions() *joinOptions { + // initialize the public kubeadm config API by appling defaults + externalcfg := &kubeadmapiv1beta1.JoinConfiguration{} + + // Add optional config objects to host flags. + // un-set objects will be cleaned up afterwards (into newJoinData func) + externalcfg.Discovery.File = &kubeadmapiv1beta1.FileDiscovery{} + externalcfg.Discovery.BootstrapToken = &kubeadmapiv1beta1.BootstrapTokenDiscovery{} + externalcfg.ControlPlane = &kubeadmapiv1beta1.JoinControlPlane{} + + // Apply defaults + kubeadmscheme.Scheme.Default(externalcfg) + + return &joinOptions{ + externalcfg: externalcfg, + } } -// NewJoin instantiates Join struct with given arguments -func NewJoin(cfgPath string, defaultcfg *kubeadmapiv1beta1.JoinConfiguration, ignorePreflightErrors sets.String) (*Join, error) { +// 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, options *joinOptions, out io.Writer) (joinData, error) { + // Re-apply defaults to the public kubeadm API (this will set only values not exposed/not set as a flags) + kubeadmscheme.Scheme.Default(options.externalcfg) - if defaultcfg.NodeRegistration.Name == "" { - klog.V(1).Infoln("[join] found NodeName empty; using OS hostname as NodeName") - } + // Validate standalone flags values and/or combination of flags and then assigns + // validated values to the public kubeadm config API when applicable - if defaultcfg.ControlPlane != nil && defaultcfg.ControlPlane.LocalAPIEndpoint.AdvertiseAddress == "" { - klog.V(1).Infoln("[join] found advertiseAddress empty; using default interface's IP address as advertiseAddress") - } - - internalCfg, err := configutil.JoinConfigFileAndDefaultsToInternalConfig(cfgPath, defaultcfg) - if err != nil { - return nil, err - } - - // override node name and CRI socket from the command line options - if defaultcfg.NodeRegistration.Name != "" { - internalCfg.NodeRegistration.Name = defaultcfg.NodeRegistration.Name - } - if defaultcfg.NodeRegistration.CRISocket != kubeadmapiv1beta1.DefaultCRISocket { - internalCfg.NodeRegistration.CRISocket = defaultcfg.NodeRegistration.CRISocket - } - - if internalCfg.ControlPlane != nil { - if err := configutil.VerifyAPIServerBindAddress(internalCfg.ControlPlane.LocalAPIEndpoint.AdvertiseAddress); err != nil { - return nil, err + // if a token is provided, use this value for both discovery-token and tls-bootstrap-token when those values are not provided + if len(options.token) > 0 { + if len(options.externalcfg.Discovery.TLSBootstrapToken) == 0 { + options.externalcfg.Discovery.TLSBootstrapToken = options.token + } + if len(options.externalcfg.Discovery.BootstrapToken.Token) == 0 { + options.externalcfg.Discovery.BootstrapToken.Token = options.token } } + // if a file or URL from which to load cluster information was not provided, unset the Discovery.File object + if len(options.externalcfg.Discovery.File.KubeConfigPath) == 0 { + options.externalcfg.Discovery.File = nil + } + + // if an APIServerEndpoint from which to retrive cluster information was not provided, unset the Discovery.BootstrapToken object + if len(args) == 0 { + options.externalcfg.Discovery.BootstrapToken = nil + } else { + if len(options.cfgPath) == 0 && len(args) > 1 { + klog.Warningf("[join] WARNING: More than one API server endpoint supplied on command line %v. Using the first one.", args) + } + options.externalcfg.Discovery.BootstrapToken.APIServerEndpoint = args[0] + } + + // if not joining a control plane, unset the ControlPlane object + if !options.controlPlane { + options.externalcfg.ControlPlane = nil + } + + ignorePreflightErrorsSet, err := validation.ValidateIgnorePreflightErrors(options.ignorePreflightErrors) + if err != nil { + return joinData{}, err + } + + if err = validation.ValidateMixedArguments(cmd.Flags()); err != nil { + return joinData{}, err + } + + // Either use the config file if specified, or convert public kubeadm API to the internal JoinConfiguration + // and validates JoinConfiguration + if options.externalcfg.NodeRegistration.Name == "" { + klog.V(1).Infoln("[join] found NodeName empty; using OS hostname as NodeName") + } + + if options.externalcfg.ControlPlane != nil && options.externalcfg.ControlPlane.LocalAPIEndpoint.AdvertiseAddress == "" { + klog.V(1).Infoln("[join] found advertiseAddress empty; using default interface's IP address as advertiseAddress") + } + + cfg, err := configutil.JoinConfigFileAndDefaultsToInternalConfig(options.cfgPath, options.externalcfg) + if err != nil { + return joinData{}, err + } + + // override node name and CRI socket from the command line options + if options.externalcfg.NodeRegistration.Name != "" { + cfg.NodeRegistration.Name = options.externalcfg.NodeRegistration.Name + } + if options.externalcfg.NodeRegistration.CRISocket != kubeadmapiv1beta1.DefaultCRISocket { + cfg.NodeRegistration.CRISocket = options.externalcfg.NodeRegistration.CRISocket + } + + if cfg.ControlPlane != nil { + if err := configutil.VerifyAPIServerBindAddress(cfg.ControlPlane.LocalAPIEndpoint.AdvertiseAddress); err != nil { + return joinData{}, err + } + } + + return joinData{ + cfg: cfg, + ignorePreflightErrors: ignorePreflightErrorsSet, + outputWriter: out, + }, nil +} + +// Run executes worker node provisioning and tries to join an existing cluster. +func (j *joinData) Run() error { fmt.Println("[preflight] Running pre-flight checks") // Start with general checks klog.V(1).Infoln("[preflight] Running general checks") - if err := preflight.RunJoinNodeChecks(utilsexec.New(), internalCfg, ignorePreflightErrors); err != nil { - return nil, err + if err := preflight.RunJoinNodeChecks(utilsexec.New(), j.cfg, j.ignorePreflightErrors); err != nil { + return err } // Fetch the init configuration based on the join configuration klog.V(1).Infoln("[preflight] Fetching init configuration") - initCfg, tlsBootstrapCfg, err := fetchInitConfigurationFromJoinConfiguration(internalCfg) + initCfg, tlsBootstrapCfg, err := fetchInitConfigurationFromJoinConfiguration(j.cfg) if err != nil { - return nil, err + return err } // Continue with more specific checks based on the init configuration klog.V(1).Infoln("[preflight] Running configuration dependant checks") - if err := preflight.RunOptionalJoinNodeChecks(utilsexec.New(), initCfg, ignorePreflightErrors); err != nil { - return nil, err + if err := preflight.RunOptionalJoinNodeChecks(utilsexec.New(), initCfg, j.ignorePreflightErrors); err != nil { + return err } - return &Join{cfg: internalCfg, initCfg: initCfg, tlsBootstrapCfg: tlsBootstrapCfg, ignorePreflightErrors: ignorePreflightErrors}, nil -} - -// Run executes worker node provisioning and tries to join an existing cluster. -func (j *Join) Run(out io.Writer) error { if j.cfg.ControlPlane != nil { // Checks if the cluster configuration supports // joining a new control plane instance and if all the necessary certificates are provided - if err := j.CheckIfReadyForAdditionalControlPlane(j.initCfg); err != nil { + if err := j.CheckIfReadyForAdditionalControlPlane(initCfg); err != nil { // outputs the not ready for hosting a new control plane instance message ctx := map[string]string{ "Error": err.Error(), @@ -364,11 +417,11 @@ func (j *Join) Run(out io.Writer) error { // run kubeadm init preflight checks for checking all the prequisites fmt.Printf("[join] Running pre-flight checks before initializing the new control plane instance\n") - preflight.RunInitMasterChecks(utilsexec.New(), j.initCfg, j.ignorePreflightErrors) + preflight.RunInitMasterChecks(utilsexec.New(), initCfg, j.ignorePreflightErrors) // Prepares the node for hosting a new control plane instance by writing necessary // kubeconfig files, and static pod manifests - if err := j.PrepareForHostingControlPlane(j.initCfg); err != nil { + if err := j.PrepareForHostingControlPlane(initCfg); err != nil { return err } } @@ -379,21 +432,21 @@ func (j *Join) Run(out io.Writer) error { // if the node is hosting a new control plane instance, since it uses static pods for the control plane, // as soon as the kubelet starts it will take charge of creating control plane // components on the node. - if err := j.BootstrapKubelet(); err != nil { + if err := j.BootstrapKubelet(tlsBootstrapCfg, initCfg); err != nil { return err } // if the node is hosting a new control plane instance if j.cfg.ControlPlane != nil { // Completes the control plane setup - if err := j.PostInstallControlPlane(j.initCfg); err != nil { + if err := j.PostInstallControlPlane(initCfg); err != nil { return err } // outputs the join control plane done template and exits etcdMessage := "" // in case of local etcd - if j.initCfg.Etcd.External == nil { + if initCfg.Etcd.External == nil { etcdMessage = "* A new etcd member was added to the local/stacked etcd cluster." } @@ -401,19 +454,19 @@ func (j *Join) Run(out io.Writer) error { "KubeConfigPath": kubeadmconstants.GetAdminKubeConfigPath(), "etcdMessage": etcdMessage, } - joinControPlaneDoneTemp.Execute(out, ctx) + joinControPlaneDoneTemp.Execute(j.outputWriter, ctx) return nil } // otherwise, if the node joined as a worker node; // outputs the join done message and exits - fmt.Fprintf(out, joinWorkerNodeDoneMsg) + fmt.Fprintf(j.outputWriter, joinWorkerNodeDoneMsg) return nil } // CheckIfReadyForAdditionalControlPlane ensures that the cluster is in a state that supports // joining an additional control plane instance and if the node is ready to join -func (j *Join) CheckIfReadyForAdditionalControlPlane(initConfiguration *kubeadmapi.InitConfiguration) error { +func (j *joinData) CheckIfReadyForAdditionalControlPlane(initConfiguration *kubeadmapi.InitConfiguration) error { // blocks if the cluster was created without a stable control plane endpoint if initConfiguration.ControlPlaneEndpoint == "" { return errors.New("unable to add a new control plane instance a cluster that doesn't have a stable controlPlaneEndpoint address") @@ -428,7 +481,7 @@ func (j *Join) CheckIfReadyForAdditionalControlPlane(initConfiguration *kubeadma } // PrepareForHostingControlPlane makes all preparation activities require for a node hosting a new control plane instance -func (j *Join) PrepareForHostingControlPlane(initConfiguration *kubeadmapi.InitConfiguration) error { +func (j *joinData) PrepareForHostingControlPlane(initConfiguration *kubeadmapi.InitConfiguration) error { // Generate missing certificates (if any) if err := certsphase.CreatePKIAssets(initConfiguration); err != nil { @@ -471,19 +524,19 @@ func (j *Join) PrepareForHostingControlPlane(initConfiguration *kubeadmapi.InitC // BootstrapKubelet executes the kubelet TLS bootstrap process. // This process is executed by the kubelet and completes with the node joining the cluster // with a dedicates set of credentials as required by the node authorizer -func (j *Join) BootstrapKubelet() error { +func (j *joinData) BootstrapKubelet(tlsBootstrapCfg *clientcmdapi.Config, initConfiguration *kubeadmapi.InitConfiguration) error { bootstrapKubeConfigFile := kubeadmconstants.GetBootstrapKubeletKubeConfigPath() // Write the bootstrap kubelet config file or the TLS-Boostrapped kubelet config file down to disk klog.V(1).Infoln("[join] writing bootstrap kubelet config file at", bootstrapKubeConfigFile) - if err := kubeconfigutil.WriteToDisk(bootstrapKubeConfigFile, j.tlsBootstrapCfg); err != nil { + if err := kubeconfigutil.WriteToDisk(bootstrapKubeConfigFile, tlsBootstrapCfg); err != nil { return errors.Wrap(err, "couldn't save bootstrap-kubelet.conf to disk") } // Write the ca certificate to disk so kubelet can use it for authentication - cluster := j.tlsBootstrapCfg.Contexts[j.tlsBootstrapCfg.CurrentContext].Cluster + cluster := tlsBootstrapCfg.Contexts[tlsBootstrapCfg.CurrentContext].Cluster if _, err := os.Stat(j.cfg.CACertPath); os.IsNotExist(err) { - if err := certutil.WriteCert(j.cfg.CACertPath, j.tlsBootstrapCfg.Clusters[cluster].CertificateAuthorityData); err != nil { + if err := certutil.WriteCert(j.cfg.CACertPath, tlsBootstrapCfg.Clusters[cluster].CertificateAuthorityData); err != nil { return errors.Wrap(err, "couldn't save the CA certificate to disk") } } @@ -512,7 +565,7 @@ func (j *Join) BootstrapKubelet() error { // register the joining node with the specified taints if the node // is not a master. The markmaster phase will register the taints otherwise. registerTaintsUsingFlags := j.cfg.ControlPlane == nil - if err := kubeletphase.WriteKubeletDynamicEnvFile(j.initCfg, registerTaintsUsingFlags, kubeadmconstants.KubeletRunDirectory); err != nil { + if err := kubeletphase.WriteKubeletDynamicEnvFile(initConfiguration, registerTaintsUsingFlags, kubeadmconstants.KubeletRunDirectory); err != nil { return err } @@ -544,7 +597,7 @@ func (j *Join) BootstrapKubelet() error { } // PostInstallControlPlane marks the new node as master and update the cluster status with information about current node -func (j *Join) PostInstallControlPlane(initConfiguration *kubeadmapi.InitConfiguration) error { +func (j *joinData) PostInstallControlPlane(initConfiguration *kubeadmapi.InitConfiguration) error { kubeConfigFile := filepath.Join(kubeadmconstants.KubernetesDir, kubeadmconstants.AdminKubeConfigFileName) client, err := kubeconfigutil.ClientSetFromFile(kubeConfigFile) diff --git a/cmd/kubeadm/app/cmd/join_test.go b/cmd/kubeadm/app/cmd/join_test.go index 1ea7ba9462..cfce81eeba 100644 --- a/cmd/kubeadm/app/cmd/join_test.go +++ b/cmd/kubeadm/app/cmd/join_test.go @@ -17,42 +17,29 @@ limitations under the License. package cmd import ( - "bytes" "io/ioutil" "os" "path/filepath" "testing" - "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" - kubeadmscheme "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/scheme" - kubeadmapiv1beta1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta1" + "k8s.io/kubernetes/cmd/kubeadm/app/cmd/options" ) const ( - testConfig = `apiVersion: v1 -clusters: -- cluster: - certificate-authority-data: - server: localhost:9008 - name: prod -contexts: -- context: - cluster: prod - namespace: default - user: default-service-account - name: default -current-context: default -kind: Config -preferences: {} -users: -- name: kubernetes-admin - user: - client-certificate-data: - client-key-data: + testJoinConfig = `apiVersion: kubeadm.k8s.io/v1beta1 +kind: JoinConfiguration +discovery: + bootstrapToken: + token: abcdef.0123456789abcdef + apiServerEndpoint: 1.2.3.4:6443 + unsafeSkipCAVerification: true +nodeRegistration: + criSocket: /run/containerd/containerd.sock + name: someName ` ) -func TestNewValidJoin(t *testing.T) { +func TestNewJoinData(t *testing.T) { // create temp directory tmpDir, err := ioutil.TempDir("", "kubeadm-join-test") if err != nil { @@ -67,110 +54,192 @@ func TestNewValidJoin(t *testing.T) { t.Errorf("Unable to create file %q: %v", configFilePath, err) } defer cfgFile.Close() + if _, err = cfgFile.WriteString(testJoinConfig); err != nil { + t.Fatalf("Unable to write file %q: %v", configFilePath, err) + } testCases := []struct { - name string - skipPreFlight bool - cfgPath string - configToWrite string - ignorePreflightErrors []string - testJoinValidate bool - testJoinRun bool - cmdPersistentFlags map[string]string - nodeConfig *kubeadm.JoinConfiguration - expectedError bool + name string + args []string + flags map[string]string + validate func(*testing.T, *joinData) + expectError bool }{ + // Join data passed using flags { - name: "invalid: missing config file", - skipPreFlight: true, - cfgPath: "missing-path-to-a-config", - expectedError: true, + name: "fails if no discovery method set", + expectError: true, }, { - name: "invalid: incorrect config file", - skipPreFlight: true, - cfgPath: configFilePath, - configToWrite: "bad-config-contents", - expectedError: true, - }, - { - name: "invalid: fail at preflight.RunJoinNodeChecks()", - skipPreFlight: false, - cfgPath: configFilePath, - configToWrite: testConfig, - expectedError: true, - }, - { - name: "invalid: incorrect ignorePreflight argument", - skipPreFlight: true, - cfgPath: configFilePath, - configToWrite: testConfig, - ignorePreflightErrors: []string{"some-unsupported-preflight-arg"}, - expectedError: true, - }, - { - name: "invalid: fail Join.Validate() with wrong flags", - skipPreFlight: true, - cfgPath: configFilePath, - configToWrite: testConfig, - testJoinValidate: true, - cmdPersistentFlags: map[string]string{ - "config": "some-config", - "node-name": "some-node-name", + name: "fails if both file and bootstrap discovery methods set", + args: []string{"1.2.3.4:6443"}, + flags: map[string]string{ + options.FileDiscovery: "https://foo", + options.TokenDiscovery: "abcdef.0123456789abcdef", + options.TokenDiscoverySkipCAHash: "true", }, - expectedError: true, + expectError: true, }, { - name: "invalid: fail Join.Validate() with wrong node configuration", - skipPreFlight: true, - cfgPath: configFilePath, - configToWrite: testConfig, - testJoinValidate: true, - expectedError: true, + name: "pass if file discovery is set", + flags: map[string]string{ + options.FileDiscovery: "https://foo", + }, + validate: func(t *testing.T, data *joinData) { + // validate that file discovery settings are set into join data + if data.cfg.Discovery.File == nil || data.cfg.Discovery.File.KubeConfigPath != "https://foo" { + t.Errorf("Invalid data.cfg.Discovery.File") + } + }, }, { - name: "invalid: fail Join.Run() with invalid node config", - skipPreFlight: true, - cfgPath: configFilePath, - configToWrite: testConfig, - testJoinRun: true, - expectedError: true, + name: "pass if bootstrap discovery is set", + args: []string{"1.2.3.4:6443", "5.6.7.8:6443"}, + flags: map[string]string{ + options.TokenDiscovery: "abcdef.0123456789abcdef", + options.TokenDiscoverySkipCAHash: "true", + }, + validate: func(t *testing.T, data *joinData) { + // validate that bootstrap discovery settings are set into join data + if data.cfg.Discovery.BootstrapToken == nil || + data.cfg.Discovery.BootstrapToken.APIServerEndpoint != "1.2.3.4:6443" || //only first arg should be kept as APIServerEndpoint + data.cfg.Discovery.BootstrapToken.Token != "abcdef.0123456789abcdef" || + data.cfg.Discovery.BootstrapToken.UnsafeSkipCAVerification != true { + t.Errorf("Invalid data.cfg.Discovery.BootstrapToken") + } + }, + }, + { + name: "--token sets TLSBootstrapToken and BootstrapToken.Token if unset", + args: []string{"1.2.3.4:6443"}, + flags: map[string]string{ + options.TokenStr: "abcdef.0123456789abcdef", + options.TokenDiscoverySkipCAHash: "true", + }, + validate: func(t *testing.T, data *joinData) { + // validate that token sets both TLSBootstrapToken and BootstrapToken.Token into join data + if data.cfg.Discovery.TLSBootstrapToken != "abcdef.0123456789abcdef" || + data.cfg.Discovery.BootstrapToken == nil || + data.cfg.Discovery.BootstrapToken.Token != "abcdef.0123456789abcdef" { + t.Errorf("Invalid TLSBootstrapToken or BootstrapToken.Token") + } + }, + }, + { + name: "--token doesn't override TLSBootstrapToken and BootstrapToken.Token if set", + args: []string{"1.2.3.4:6443"}, + flags: map[string]string{ + options.TokenStr: "aaaaaa.0123456789aaaaaa", + options.TLSBootstrapToken: "abcdef.0123456789abcdef", + options.TokenDiscovery: "defghi.0123456789defghi", + options.TokenDiscoverySkipCAHash: "true", + }, + validate: func(t *testing.T, data *joinData) { + // validate that TLSBootstrapToken and BootstrapToken.Token values are preserved into join data + if data.cfg.Discovery.TLSBootstrapToken != "abcdef.0123456789abcdef" || + data.cfg.Discovery.BootstrapToken == nil || + data.cfg.Discovery.BootstrapToken.Token != "defghi.0123456789defghi" { + t.Errorf("Invalid TLSBootstrapToken or BootstrapToken.Token") + } + }, + }, + { + name: "control plane setting are preserved if --control-plane flag is set", + flags: map[string]string{ + options.ControlPlane: "true", + options.APIServerAdvertiseAddress: "1.2.3.4", + options.APIServerBindPort: "1234", + options.FileDiscovery: "https://foo", //required only to pass discovery validation + }, + validate: func(t *testing.T, data *joinData) { + // validate that control plane attributes are set in join data + if data.cfg.ControlPlane == nil || + data.cfg.ControlPlane.LocalAPIEndpoint.AdvertiseAddress != "1.2.3.4" || + data.cfg.ControlPlane.LocalAPIEndpoint.BindPort != 1234 { + t.Errorf("Invalid ControlPlane") + } + }, + }, + { + name: "control plane setting are cleaned up if --control-plane flag is not set", + flags: map[string]string{ + options.ControlPlane: "false", + options.APIServerAdvertiseAddress: "1.2.3.4", + options.APIServerBindPort: "1.2.3.4", + options.FileDiscovery: "https://foo", //required only to pass discovery validation + }, + validate: func(t *testing.T, data *joinData) { + // validate that control plane attributes are unset in join data + if data.cfg.ControlPlane != nil { + t.Errorf("Invalid ControlPlane") + } + }, + }, + { + name: "fails if invalid preflight checks are provided", + flags: map[string]string{ + options.IgnorePreflightErrors: "all,something-else", + }, + expectError: true, + }, + + // Join 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 *joinData) { + // 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, }, } - - var out bytes.Buffer - cfg := &kubeadmapiv1beta1.JoinConfiguration{} - kubeadmscheme.Scheme.Default(cfg) - - errorFormat := "Test case %q: NewValidJoin expected error: %v, saw: %v, error: %v" - for _, tc := range testCases { - if _, err = cfgFile.WriteString(tc.configToWrite); err != nil { - t.Fatalf("Unable to write file %q: %v", tc.cfgPath, err) - } + t.Run(tc.name, func(t *testing.T) { + // initialize an external join option and inject it to the join cmd + joinOptions := newJoinOptions() + cmd := NewCmdJoin(nil, joinOptions) - cmd := NewCmdJoin(&out) - if tc.cmdPersistentFlags != nil { - for key, value := range tc.cmdPersistentFlags { - cmd.PersistentFlags().Set(key, value) + // sets cmd flags (that will be reflected on the join options) + for f, v := range tc.flags { + cmd.Flags().Set(f, v) } - } - join, err := NewValidJoin(cmd.PersistentFlags(), cfg, tc.cfgPath, tc.ignorePreflightErrors) - - if tc.nodeConfig != nil { - join.cfg = tc.nodeConfig - } - - // test Join.Run() - if err == nil && tc.testJoinRun { - err = join.Run(&out) - if (err != nil) != tc.expectedError { - t.Fatalf(errorFormat, tc.name, tc.expectedError, (err != nil), err) + // test newJoinData method + data, err := newJoinData(cmd, tc.args, joinOptions, nil) + if err != nil && !tc.expectError { + t.Fatalf("newJoinData returned unexpected error: %v", err) } - // check error for NewValidJoin() - } else if (err != nil) != tc.expectedError { - t.Fatalf(errorFormat, tc.name, tc.expectedError, (err != nil), err) - } + if err == nil && tc.expectError { + t.Fatalf("newJoinData didn't return error when expected") + } + + // exec additional validation on the returned value + if tc.validate != nil { + tc.validate(t, &data) + } + }) } } diff --git a/cmd/kubeadm/app/cmd/options/constant.go b/cmd/kubeadm/app/cmd/options/constant.go index bd74e94c07..dd47f66f17 100644 --- a/cmd/kubeadm/app/cmd/options/constant.go +++ b/cmd/kubeadm/app/cmd/options/constant.go @@ -85,7 +85,7 @@ const CSROnly = "csr-only" // CSRDir flag sets the location for CSRs and flags to be output const CSRDir = "csr-dir" -// TokenStr flag sets the token +// TokenStr flags sets both the discovery-token and the tls-bootstrap-token when those values are not provided const TokenStr = "token" // TokenTTL flag sets the time to live for token @@ -99,3 +99,21 @@ const TokenGroups = "groups" // TokenDescription flag sets the description of the token const TokenDescription = "description" + +// TLSBootstrapToken flag sets the token used to temporarily authenticate with the Kubernetes Master to submit a certificate signing request (CSR) for a locally created key pair +const TLSBootstrapToken = "tls-bootstrap-token" + +// TokenDiscovery flag sets the token used to validate cluster information fetched from the API server (for token-based discovery) +const TokenDiscovery = "discovery-token" + +// TokenDiscoveryCAHash flag instruct kubeadm to validate that the root CA public key matches this hash (for token-based discovery) +const TokenDiscoveryCAHash = "discovery-token-ca-cert-hash" + +// TokenDiscoverySkipCAHash flag instruct kubeadm to skip CA hash verification (for token-based discovery) +const TokenDiscoverySkipCAHash = "discovery-token-unsafe-skip-ca-verification" + +// FileDiscovery flag sets the file or URL from which to load cluster information (for file-based discovery) +const FileDiscovery = "discovery-file" + +// ControlPlane flag instruct kubeadm to create a new control plane instance on this node +const ControlPlane = "experimental-control-plane"