github.com/alibaba/sealer@v0.8.6-0.20220430115802-37a2bdaa8173/pkg/runtime/masters.go (about)

     1  // Copyright © 2021 Alibaba Group Holding Ltd.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package runtime
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"path/filepath"
    21  	"strconv"
    22  	"strings"
    23  	"sync"
    24  
    25  	"golang.org/x/sync/errgroup"
    26  
    27  	"github.com/pkg/errors"
    28  
    29  	"github.com/alibaba/sealer/logger"
    30  	"github.com/alibaba/sealer/pkg/cert"
    31  	"github.com/alibaba/sealer/pkg/command"
    32  	"github.com/alibaba/sealer/pkg/ipvs"
    33  	"github.com/alibaba/sealer/utils"
    34  )
    35  
    36  const (
    37  	V1991 = "v1.19.1"
    38  	V1992 = "v1.19.2"
    39  	V1150 = "v1.15.0"
    40  	V1200 = "v1.20.0"
    41  	V1230 = "v1.23.0"
    42  )
    43  
    44  const (
    45  	RemoteAddEtcHosts       = "cat /etc/hosts |grep '%s' || echo '%s' >> /etc/hosts"
    46  	RemoteUpdateEtcHosts    = `sed "s/%s/%s/g" < /etc/hosts > hosts && cp -f hosts /etc/hosts`
    47  	RemoteCopyKubeConfig    = `rm -rf .kube/config && mkdir -p /root/.kube && cp /etc/kubernetes/admin.conf /root/.kube/config`
    48  	RemoteReplaceKubeConfig = `grep -qF "apiserver.cluster.local" %s  && sed -i 's/apiserver.cluster.local/%s/' %s && sed -i 's/apiserver.cluster.local/%s/' %s`
    49  	RemoteJoinMasterConfig  = `echo "%s" > %s/etc/kubeadm.yml`
    50  	InitMaster115Lower      = `kubeadm init --config=%s/etc/kubeadm.yml --experimental-upload-certs`
    51  	JoinMaster115Lower      = "kubeadm join %s:6443 --token %s --discovery-token-ca-cert-hash %s --experimental-control-plane --certificate-key %s"
    52  	JoinNode115Lower        = "kubeadm join %s:6443 --token %s --discovery-token-ca-cert-hash %s"
    53  	InitMaser115Upper       = `kubeadm init --config=%s/etc/kubeadm.yml --upload-certs`
    54  	JoinMaster115Upper      = "kubeadm join --config=%s/etc/kubeadm.yml"
    55  	JoinNode115Upper        = "kubeadm join --config=%s/etc/kubeadm.yml"
    56  	RemoveKubeConfig        = "rm -rf /usr/bin/kube* && rm -rf ~/.kube/"
    57  	RemoteCleanMasterOrNode = `if which kubeadm;then kubeadm reset -f %s;fi && \
    58  modprobe -r ipip  && lsmod && \
    59  rm -rf /etc/kubernetes/ && \
    60  rm -rf /etc/systemd/system/kubelet.service.d && rm -rf /etc/systemd/system/kubelet.service && \
    61  rm -rf /usr/bin/kubeadm && rm -rf /usr/bin/kubelet-pre-start.sh && \
    62  rm -rf /usr/bin/kubelet && rm -rf /usr/bin/crictl && \
    63  rm -rf /etc/cni && rm -rf /opt/cni && \
    64  rm -rf /var/lib/etcd && rm -rf /var/etcd 
    65  `
    66  	RemoteRemoveAPIServerEtcHost = "sed -i \"/%s/d\" /etc/hosts"
    67  	RemoteRemoveRegistryCerts    = "rm -rf " + DockerCertDir + "/%s*"
    68  	RemoveLvscareStaticPod       = "rm -rf  /etc/kubernetes/manifests/kube-sealyun-lvscare*"
    69  	CreateLvscareStaticPod       = "mkdir -p /etc/kubernetes/manifests && echo '%s' > /etc/kubernetes/manifests/kube-sealyun-lvscare.yaml"
    70  	KubeDeleteNode               = "kubectl delete node %s"
    71  	// TODO check kubernetes certs
    72  	RemoteCheckCerts = "kubeadm alpha certs check-expiration"
    73  )
    74  
    75  const (
    76  	AdminConf      = "admin.conf"
    77  	ControllerConf = "controller-manager.conf"
    78  	SchedulerConf  = "scheduler.conf"
    79  	KubeletConf    = "kubelet.conf"
    80  
    81  	// kube file
    82  	KUBECONTROLLERCONFIGFILE = "/etc/kubernetes/controller-manager.conf"
    83  	KUBESCHEDULERCONFIGFILE  = "/etc/kubernetes/scheduler.conf"
    84  
    85  	// CriSocket
    86  	DefaultDockerCRISocket     = "/var/run/dockershim.sock"
    87  	DefaultContainerdCRISocket = "/run/containerd/containerd.sock"
    88  	DefaultSystemdCgroupDriver = "systemd"
    89  	DefaultCgroupDriver        = "cgroupfs"
    90  
    91  	// kubeadm api version
    92  	KubeadmV1beta1 = "kubeadm.k8s.io/v1beta1"
    93  	KubeadmV1beta2 = "kubeadm.k8s.io/v1beta2"
    94  	KubeadmV1beta3 = "kubeadm.k8s.io/v1beta3"
    95  )
    96  
    97  const (
    98  	Master0              = "Master0"
    99  	Master               = "Master"
   100  	Masters              = "Masters"
   101  	TokenDiscovery       = "TokenDiscovery"
   102  	VIP                  = "VIP"
   103  	Version              = "Version"
   104  	APIServer            = "ApiServer"
   105  	PodCIDR              = "PodCIDR"
   106  	SvcCIDR              = "SvcCIDR"
   107  	Repo                 = "Repo"
   108  	CertSANS             = "CertSANS"
   109  	EtcdServers          = "etcd-servers"
   110  	CriSocket            = "CriSocket"
   111  	CriCGroupDriver      = "CriCGroupDriver"
   112  	KubeadmAPI           = "KubeadmAPI"
   113  	TokenDiscoveryCAHash = "TokenDiscoveryCAHash"
   114  )
   115  
   116  type CommandType string
   117  
   118  //command type
   119  const InitMaster CommandType = "initMaster"
   120  const JoinMaster CommandType = "joinMaster"
   121  const JoinNode CommandType = "joinNode"
   122  
   123  func getAPIServerHost(ipAddr, APIServer string) (host string) {
   124  	return fmt.Sprintf("%s %s", ipAddr, APIServer)
   125  }
   126  
   127  func (k *KubeadmRuntime) JoinMasterCommands(master, joinCmd, hostname string) []string {
   128  	apiServerHost := getAPIServerHost(k.GetMaster0IP(), k.getAPIServerDomain())
   129  	cmdAddRegistryHosts := fmt.Sprintf(RemoteAddEtcHosts, k.getRegistryHost(), k.getRegistryHost())
   130  	certCMD := command.RemoteCerts(k.getCertSANS(), master, hostname, k.getSvcCIDR(), "")
   131  	cmdAddHosts := fmt.Sprintf(RemoteAddEtcHosts, apiServerHost, apiServerHost)
   132  	if k.RegConfig.Domain != SeaHub {
   133  		cmdAddSeaHubHosts := fmt.Sprintf(RemoteAddEtcHosts, k.RegConfig.IP+" "+SeaHub, k.RegConfig.IP+" "+SeaHub)
   134  		cmdAddRegistryHosts = fmt.Sprintf("%s && %s", cmdAddRegistryHosts, cmdAddSeaHubHosts)
   135  	}
   136  	joinCommands := []string{cmdAddRegistryHosts, certCMD, cmdAddHosts}
   137  	if k.RegConfig.Username != "" && k.RegConfig.Password != "" {
   138  		joinCommands = append(joinCommands, k.GerLoginCommand())
   139  	}
   140  	cmdUpdateHosts := fmt.Sprintf(RemoteUpdateEtcHosts, apiServerHost,
   141  		getAPIServerHost(master, k.getAPIServerDomain()))
   142  
   143  	return append(joinCommands, joinCmd, cmdUpdateHosts, RemoteCopyKubeConfig)
   144  }
   145  
   146  func (k *KubeadmRuntime) sendKubeConfigFile(hosts []string, kubeFile string) error {
   147  	absKubeFile := fmt.Sprintf("%s/%s", cert.KubernetesDir, kubeFile)
   148  	sealerKubeFile := fmt.Sprintf("%s/%s", k.getBasePath(), kubeFile)
   149  	return k.sendFileToHosts(hosts, sealerKubeFile, absKubeFile)
   150  }
   151  
   152  func (k *KubeadmRuntime) sendNewCertAndKey(hosts []string) error {
   153  	return k.sendFileToHosts(hosts, k.getPKIPath(), cert.KubeDefaultCertPath)
   154  }
   155  
   156  func (k *KubeadmRuntime) sendRegistryCertAndKey() error {
   157  	return k.sendFileToHosts(k.GetMasterIPList()[:1], k.getCertsDir(), filepath.Join(k.getRootfs(), "certs"))
   158  }
   159  
   160  func (k *KubeadmRuntime) sendRegistryCert(host []string) error {
   161  	cf := k.RegConfig
   162  	err := k.sendFileToHosts(host, fmt.Sprintf("%s/%s.crt", k.getCertsDir(), cf.Domain), fmt.Sprintf("%s/%s:%s/%s.crt", DockerCertDir, cf.Domain, cf.Port, cf.Domain))
   163  	if err != nil {
   164  		return err
   165  	}
   166  	return k.sendFileToHosts(host, fmt.Sprintf("%s/%s.crt", k.getCertsDir(), cf.Domain), fmt.Sprintf("%s/%s:%s/%s.crt", DockerCertDir, SeaHub, cf.Port, cf.Domain))
   167  }
   168  
   169  func (k *KubeadmRuntime) sendFileToHosts(Hosts []string, src, dst string) error {
   170  	eg, _ := errgroup.WithContext(context.Background())
   171  	for _, node := range Hosts {
   172  		node := node
   173  		eg.Go(func() error {
   174  			ssh, err := k.getHostSSHClient(node)
   175  			if err != nil {
   176  				return fmt.Errorf("send file failed %v", err)
   177  			}
   178  			if err := ssh.Copy(node, src, dst); err != nil {
   179  				return fmt.Errorf("send file failed %v", err)
   180  			}
   181  			return err
   182  		})
   183  	}
   184  	return eg.Wait()
   185  }
   186  
   187  func (k *KubeadmRuntime) ReplaceKubeConfigV1991V1992(masters []string) bool {
   188  	// fix > 1.19.1 kube-controller-manager and kube-scheduler use the LocalAPIEndpoint instead of the ControlPlaneEndpoint.
   189  	if k.getKubeVersion() == V1991 || k.getKubeVersion() == V1992 {
   190  		for _, v := range masters {
   191  			cmd := fmt.Sprintf(RemoteReplaceKubeConfig, KUBESCHEDULERCONFIGFILE, v, KUBECONTROLLERCONFIGFILE, v, KUBESCHEDULERCONFIGFILE)
   192  			ssh, err := k.getHostSSHClient(v)
   193  			if err != nil {
   194  				logger.Info("failed to replace kube config on %s:%v ", v, err)
   195  				return false
   196  			}
   197  			if err := ssh.CmdAsync(v, cmd); err != nil {
   198  				logger.Info("failed to replace kube config on %s:%v ", v, err)
   199  				return false
   200  			}
   201  		}
   202  		return true
   203  	}
   204  	return false
   205  }
   206  
   207  func (k *KubeadmRuntime) SendJoinMasterKubeConfigs(masters []string, files ...string) error {
   208  	for _, f := range files {
   209  		if err := k.sendKubeConfigFile(masters, f); err != nil {
   210  			return err
   211  		}
   212  	}
   213  	if k.ReplaceKubeConfigV1991V1992(masters) {
   214  		logger.Info("set kubernetes v1.19.1 v1.19.2 kube config")
   215  	}
   216  	return nil
   217  }
   218  
   219  // joinMasterConfig is generated JoinCP nodes configuration by master ip.
   220  func (k *KubeadmRuntime) joinMasterConfig(masterIP string) ([]byte, error) {
   221  	k.Lock()
   222  	defer k.Unlock()
   223  	// TODO Using join file instead template
   224  	k.setAPIServerEndpoint(fmt.Sprintf("%s:6443", k.GetMaster0IP()))
   225  	k.setJoinAdvertiseAddress(masterIP)
   226  	cGroupDriver, err := k.getCgroupDriverFromShell(masterIP)
   227  	if err != nil {
   228  		return nil, err
   229  	}
   230  	k.setCgroupDriver(cGroupDriver)
   231  	return utils.MarshalYamlConfigs(k.JoinConfiguration, k.KubeletConfiguration)
   232  }
   233  
   234  // sendJoinCPConfig send join CP nodes configuration
   235  func (k *KubeadmRuntime) sendJoinCPConfig(joinMaster []string) error {
   236  	k.Mutex = &sync.Mutex{}
   237  	eg, _ := errgroup.WithContext(context.Background())
   238  	for _, master := range joinMaster {
   239  		ip := master
   240  		eg.Go(func() error {
   241  			joinConfig, err := k.joinMasterConfig(ip)
   242  			if err != nil {
   243  				return fmt.Errorf("get join %s config failed: %v", ip, err)
   244  			}
   245  			cmd := fmt.Sprintf(RemoteJoinMasterConfig, joinConfig, k.getRootfs())
   246  			ssh, err := k.getHostSSHClient(ip)
   247  			if err != nil {
   248  				return fmt.Errorf("set join kubeadm config failed %s %s %v", ip, cmd, err)
   249  			}
   250  			if err := ssh.CmdAsync(ip, cmd); err != nil {
   251  				return fmt.Errorf("set join kubeadm config failed %s %s %v", ip, cmd, err)
   252  			}
   253  			return err
   254  		})
   255  	}
   256  	return eg.Wait()
   257  }
   258  
   259  func (k *KubeadmRuntime) CmdAsyncHosts(hosts []string, cmd string) error {
   260  	eg, _ := errgroup.WithContext(context.Background())
   261  	for _, host := range hosts {
   262  		ip := host
   263  		eg.Go(func() error {
   264  			ssh, err := k.getHostSSHClient(ip)
   265  			if err != nil {
   266  				logger.Error("exec command failed %s %s %v", ip, cmd, err)
   267  			}
   268  			if err := ssh.CmdAsync(ip, cmd); err != nil {
   269  				logger.Error("exec command failed %s %s %v", ip, cmd, err)
   270  			}
   271  			return err
   272  		})
   273  	}
   274  	return eg.Wait()
   275  }
   276  
   277  func vlogToStr(vlog int) string {
   278  	str := strconv.Itoa(vlog)
   279  	return " -v " + str
   280  }
   281  
   282  func (k *KubeadmRuntime) Command(version string, name CommandType) (cmd string) {
   283  	//cmds := make(map[CommandType]string)
   284  	// Please convert your v1beta1 configuration files to v1beta2 using the
   285  	// "kubeadm config migrate" command of kubeadm v1.15.x, so v1.14 not support multi network interface.
   286  	cmds := map[CommandType]string{
   287  		InitMaster: fmt.Sprintf(InitMaster115Lower, k.getRootfs()),
   288  		JoinMaster: fmt.Sprintf(JoinMaster115Lower, k.GetMaster0IP(), k.getJoinToken(), k.getTokenCaCertHash(), k.getCertificateKey()),
   289  		JoinNode:   fmt.Sprintf(JoinNode115Lower, k.getVIP(), k.getJoinToken(), k.getTokenCaCertHash()),
   290  	}
   291  	//other version >= 1.15.x
   292  	if VersionCompare(version, V1150) {
   293  		cmds[InitMaster] = fmt.Sprintf(InitMaser115Upper, k.getRootfs())
   294  		cmds[JoinMaster] = fmt.Sprintf(JoinMaster115Upper, k.getRootfs())
   295  		cmds[JoinNode] = fmt.Sprintf(JoinNode115Upper, k.getRootfs())
   296  	}
   297  
   298  	v, ok := cmds[name]
   299  	if !ok {
   300  		logger.Error("get kubeadm command failed %v", cmds)
   301  		return ""
   302  	}
   303  
   304  	if utils.IsInContainer() {
   305  		return fmt.Sprintf("%s%s%s", v, vlogToStr(k.Vlog), " --ignore-preflight-errors=all")
   306  	}
   307  	if name == InitMaster || name == JoinMaster {
   308  		return fmt.Sprintf("%s%s%s", v, vlogToStr(k.Vlog), " --ignore-preflight-errors=SystemVerification")
   309  	}
   310  
   311  	return fmt.Sprintf("%s%s", v, vlogToStr(k.Vlog))
   312  }
   313  
   314  func (k *KubeadmRuntime) joinMasters(masters []string) error {
   315  	if len(masters) == 0 {
   316  		return nil
   317  	}
   318  	// if its do not Load and Merge kubeadm config via init, need to redo it
   319  	if err := k.MergeKubeadmConfig(); err != nil {
   320  		return err
   321  	}
   322  	if err := k.WaitSSHReady(6, masters...); err != nil {
   323  		return errors.Wrap(err, "join masters wait for ssh ready time out")
   324  	}
   325  	if err := k.GetJoinTokenHashAndKey(); err != nil {
   326  		return err
   327  	}
   328  	if err := k.CopyStaticFiles(masters); err != nil {
   329  		return err
   330  	}
   331  	if err := k.SendJoinMasterKubeConfigs(masters, AdminConf, ControllerConf, SchedulerConf); err != nil {
   332  		return err
   333  	}
   334  	if err := k.sendRegistryCert(masters); err != nil {
   335  		return err
   336  	}
   337  	// TODO only needs send ca?
   338  	if err := k.sendNewCertAndKey(masters); err != nil {
   339  		return err
   340  	}
   341  	if err := k.sendJoinCPConfig(masters); err != nil {
   342  		return err
   343  	}
   344  	cmd := k.Command(k.getKubeVersion(), JoinMaster)
   345  	// TODO for test skip dockerd dev version
   346  	if cmd == "" {
   347  		return fmt.Errorf("get join master command failed, kubernetes version is %s", k.getKubeVersion())
   348  	}
   349  
   350  	for _, master := range masters {
   351  		logger.Info("Start to join %s as master", master)
   352  
   353  		hostname, err := k.getRemoteHostName(master)
   354  		if err != nil {
   355  			return err
   356  		}
   357  		cmds := k.JoinMasterCommands(master, cmd, hostname)
   358  		ssh, err := k.getHostSSHClient(master)
   359  		if err != nil {
   360  			return err
   361  		}
   362  
   363  		if err := ssh.CmdAsync(master, cmds...); err != nil {
   364  			return fmt.Errorf("exec command failed %s %v %v", master, cmds, err)
   365  		}
   366  
   367  		logger.Info("Succeeded in joining %s as master", master)
   368  	}
   369  	return nil
   370  }
   371  
   372  func (k *KubeadmRuntime) deleteMasters(masters []string) error {
   373  	if len(masters) == 0 {
   374  		return nil
   375  	}
   376  	eg, _ := errgroup.WithContext(context.Background())
   377  	for _, master := range masters {
   378  		master := master
   379  		eg.Go(func() error {
   380  			master := master
   381  			logger.Info("Start to delete master %s", master)
   382  			if err := k.deleteMaster(master); err != nil {
   383  				logger.Error("delete master %s failed %v", master, err)
   384  			} else {
   385  				logger.Info("Succeeded in deleting master %s", master)
   386  			}
   387  			return nil
   388  		})
   389  	}
   390  	return eg.Wait()
   391  }
   392  
   393  func SliceRemoveStr(ss []string, s string) (result []string) {
   394  	for _, v := range ss {
   395  		if v != s {
   396  			result = append(result, v)
   397  		}
   398  	}
   399  	return
   400  }
   401  
   402  func (k *KubeadmRuntime) isHostName(master, host string) (string, error) {
   403  	hostString, err := k.CmdToString(master, "kubectl get nodes | grep -v NAME  | awk '{print $1}'", ",")
   404  	if err != nil {
   405  		return "", err
   406  	}
   407  	hostName, err := k.CmdToString(host, "hostname", "")
   408  	if err != nil {
   409  		return "", err
   410  	}
   411  	hosts := strings.Split(hostString, ",")
   412  	var name string
   413  	for _, h := range hosts {
   414  		if strings.TrimSpace(h) == "" {
   415  			continue
   416  		} else {
   417  			hh := strings.ToLower(h)
   418  			fromH := strings.ToLower(hostName)
   419  			if hh == fromH {
   420  				name = h
   421  				break
   422  			}
   423  		}
   424  	}
   425  	return name, nil
   426  }
   427  
   428  func (k *KubeadmRuntime) deleteMaster(master string) error {
   429  	ssh, err := k.getHostSSHClient(master)
   430  	if err != nil {
   431  		return fmt.Errorf("failed to delete master: %v", err)
   432  	}
   433  	remoteCleanCmd := []string{fmt.Sprintf(RemoteCleanMasterOrNode, vlogToStr(k.Vlog)),
   434  		fmt.Sprintf(RemoteRemoveAPIServerEtcHost, k.RegConfig.Domain),
   435  		fmt.Sprintf(RemoteRemoveAPIServerEtcHost, SeaHub),
   436  		fmt.Sprintf(RemoteRemoveRegistryCerts, k.RegConfig.Domain),
   437  		fmt.Sprintf(RemoteRemoveRegistryCerts, SeaHub),
   438  		fmt.Sprintf(RemoteRemoveAPIServerEtcHost, k.getAPIServerDomain())}
   439  
   440  	//if the master to be removed is the execution machine, kubelet and ~./kube will not be removed and ApiServer host will be added.
   441  	address, err := utils.GetLocalHostAddresses()
   442  	if err != nil || !utils.IsLocalIP(master, address) {
   443  		remoteCleanCmd = append(remoteCleanCmd, RemoveKubeConfig)
   444  	} else {
   445  		apiServerHost := getAPIServerHost(k.GetMaster0IP(), k.getAPIServerDomain())
   446  		remoteCleanCmd = append(remoteCleanCmd,
   447  			fmt.Sprintf(RemoteAddEtcHosts, apiServerHost, apiServerHost))
   448  	}
   449  	if err := ssh.CmdAsync(master, remoteCleanCmd...); err != nil {
   450  		return err
   451  	}
   452  
   453  	//remove master
   454  	masterIPs := SliceRemoveStr(k.GetMasterIPList(), master)
   455  	if len(masterIPs) > 0 {
   456  		hostname, err := k.isHostName(k.GetMaster0IP(), master)
   457  		if err != nil {
   458  			return err
   459  		}
   460  		master0SSH, err := k.getHostSSHClient(k.GetMaster0IP())
   461  		if err != nil {
   462  			return fmt.Errorf("failed to remove master ip: %v", err)
   463  		}
   464  
   465  		if err := master0SSH.CmdAsync(k.GetMaster0IP(), fmt.Sprintf(KubeDeleteNode, strings.TrimSpace(hostname))); err != nil {
   466  			return fmt.Errorf("delete node %s failed %v", hostname, err)
   467  		}
   468  	}
   469  	lvsImage := k.RegConfig.Repo() + "/fanux/lvscare:latest"
   470  	yaml := ipvs.LvsStaticPodYaml(k.getVIP(), masterIPs, lvsImage)
   471  	eg, _ := errgroup.WithContext(context.Background())
   472  	for _, node := range k.GetNodeIPList() {
   473  		node := node
   474  		eg.Go(func() error {
   475  			ssh, err := k.getHostSSHClient(node)
   476  			if err != nil {
   477  				logger.Error("update lvscare static pod failed %s %v", node, err)
   478  			}
   479  			if err := ssh.CmdAsync(node, RemoveLvscareStaticPod, fmt.Sprintf(CreateLvscareStaticPod, yaml)); err != nil {
   480  				logger.Error("update lvscare static pod failed %s %v", node, err)
   481  			}
   482  			return err
   483  		})
   484  	}
   485  	return eg.Wait()
   486  }
   487  
   488  func (k *KubeadmRuntime) GetJoinTokenHashAndKey() error {
   489  	cmd := fmt.Sprintf(`kubeadm init phase upload-certs --upload-certs -v %d`, k.Vlog)
   490  	/*
   491  		I0415 11:45:06.653868   14520 version.go:251] remote version is much newer: v1.21.0; falling back to: stable-1.16
   492  		[upload-certs] Storing the certificates in Secret "kubeadm-certs" in the "kube-system" Namespace
   493  		[upload-certs] Using certificate key:
   494  		8376c70aaaf285b764b3c1a588740728aff493d7c2239684e84a7367c6a437cf
   495  	*/
   496  	output, err := k.CmdToString(k.GetMaster0IP(), cmd, "\r\n")
   497  	if err != nil {
   498  		return err
   499  	}
   500  	logger.Debug("[globals]decodeCertCmd: %s", output)
   501  	slice := strings.Split(output, "Using certificate key:")
   502  	if len(slice) != 2 {
   503  		return fmt.Errorf("get certifacate key failed %s", slice)
   504  	}
   505  	key := strings.Replace(slice[1], "\r\n", "", -1)
   506  	k.CertificateKey = strings.Replace(key, "\n", "", -1)
   507  	cmd = fmt.Sprintf("kubeadm token create --print-join-command -v %d", k.Vlog)
   508  
   509  	ssh, err := k.getHostSSHClient(k.GetMaster0IP())
   510  	if err != nil {
   511  		return fmt.Errorf("failed to get join token hash and key: %v", err)
   512  	}
   513  	out, err := ssh.Cmd(k.GetMaster0IP(), cmd)
   514  	if err != nil {
   515  		return fmt.Errorf("create kubeadm join token failed %v", err)
   516  	}
   517  
   518  	k.decodeMaster0Output(out)
   519  
   520  	logger.Info("join token: %s hash: %s certifacate key: %s", k.getJoinToken(), k.getTokenCaCertHash(), k.getCertificateKey())
   521  	return nil
   522  }