kubeadm源码之—Reset

920 阅读5分钟

kubeadm源码部分

最近想学习 k8s 生态的相关源码知识,从kubeadm入手来看看 k8s 的搭建及管理过程,在这里做个记录

这里就是kubeadm的源码位置,可以从这里看到github源码

cobra库

kubeadm源码使用的是cobra库开发的,在Golang中,我们可以使用Cobra很方便的进行命令行工具的开发。可以快速创建应用程序并添加所需的任何命令。

Cobra库文档地址:pkg.go.dev/github.com/…

程序入口

通过上面的链接进入源码库后,可以看到文件 kubeadm.go,这就是kubeadm程序的入口文件:

image.png

package main

import (
   "k8s.io/kubernetes/cmd/kubeadm/app"
   kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util"
)

func main() {
   kubeadmutil.CheckErr(app.Run())
}

调用了 app 包中的 Run() 函数,打开app目录后,可以再找到一个 kubeadm.go 文件,并包含目标 Run() 函数:

image.png

func Run() error {
   klog.InitFlags(nil)
   pflag.CommandLine.SetNormalizeFunc(cliflag.WordSepNormalizeFunc)
   pflag.CommandLine.AddGoFlagSet(flag.CommandLine)

   pflag.Set("logtostderr", "true")
   // We do not want these flags to show up in --help
   // These MarkHidden calls must be after the lines above
   pflag.CommandLine.MarkHidden("version")
   pflag.CommandLine.MarkHidden("log-flush-frequency")
   pflag.CommandLine.MarkHidden("alsologtostderr")
   pflag.CommandLine.MarkHidden("log-backtrace-at")
   pflag.CommandLine.MarkHidden("log-dir")
   pflag.CommandLine.MarkHidden("logtostderr")
   pflag.CommandLine.MarkHidden("stderrthreshold")
   pflag.CommandLine.MarkHidden("vmodule")

   cmd := cmd.NewKubeadmCommand(os.Stdin, os.Stdout, os.Stderr)
   return cmd.Execute()
}

可以看到 Run 函数通过 pflag 包定义了一系列命令行解析的事项(相较于flag包,pflag能够支持更加精细的参数类型、更多的参数类型、对标准flag的兼容、更高级的功能等)

核心功能通过对 cmd.NewKubeadmCommand(os.Stdin, os.Stdout, os.Stderr) 的调用来实现,这个函数真正详细定义了对 kubeadm 的命令行交互操作

打开 app/cmd/cmd.go 文件:

image.png

// NewKubeadmCommand returns cobra.Command to run kubeadm command
func NewKubeadmCommand(in io.Reader, out, err io.Writer) *cobra.Command {
	var rootfsPath string

	cmds := &cobra.Command{
		Use:   "kubeadm",
		Short: "kubeadm: easily bootstrap a secure Kubernetes cluster",
		Long: dedent.Dedent(`

			    ┌──────────────────────────────────────────────────────────┐
			    │ KUBEADM                                                  │
			    │ Easily bootstrap a secure Kubernetes cluster             │
			    │                                                          │
			    │ Please give us feedback at:                              │
			    │ https://github.com/kubernetes/kubeadm/issues             │
			    └──────────────────────────────────────────────────────────┘

			Example usage:

			    Create a two-machine cluster with one control-plane node
			    (which controls the cluster), and one worker node
			    (where your workloads, like Pods and Deployments run).

			    ┌──────────────────────────────────────────────────────────┐
			    │ On the first machine:                                    │
			    ├──────────────────────────────────────────────────────────┤
			    │ control-plane# kubeadm init                              │
			    └──────────────────────────────────────────────────────────┘

			    ┌──────────────────────────────────────────────────────────┐
			    │ On the second machine:                                   │
			    ├──────────────────────────────────────────────────────────┤
			    │ worker# kubeadm join <arguments-returned-from-init>      │
			    └──────────────────────────────────────────────────────────┘

			    You can then repeat the second step on as many other machines as you like.

		`),
		SilenceErrors: true,
		SilenceUsage:  true,
		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
			if rootfsPath != "" {
				if err := kubeadmutil.Chroot(rootfsPath); err != nil {
					return err
				}
			}
			return nil
		},
	}

	cmds.ResetFlags()

	cmds.AddCommand(newCmdCertsUtility(out))
	cmds.AddCommand(newCmdCompletion(out, ""))
	cmds.AddCommand(newCmdConfig(out))
	cmds.AddCommand(newCmdInit(out, nil))
	cmds.AddCommand(newCmdJoin(out, nil))
	cmds.AddCommand(newCmdReset(in, out, nil))
	cmds.AddCommand(newCmdVersion(out))
	cmds.AddCommand(newCmdToken(out, err))
	cmds.AddCommand(upgrade.NewCmdUpgrade(out))
	cmds.AddCommand(alpha.NewCmdAlpha())
	options.AddKubeadmOtherFlags(cmds.PersistentFlags(), &rootfsPath)
	cmds.AddCommand(newCmdKubeConfigUtility(out))

	return cmds
}

如果有安装kubeadm工具,可以在屏幕输出kubeadm的命令

image.png

从代码中不难看出,kubeadm通过 AddCommand() 函数添加了 Init、Join、Reset等常用功能,将所有子命令都附加到了父命令 kubeadm

Reset

进入 newCmdReset() 函数,可以看到 reset 命令是如何传进去的,以及他的命令子集:

// newCmdReset returns the "kubeadm reset" command
func newCmdReset(in io.Reader, out io.Writer, resetOptions *resetOptions) *cobra.Command {
   if resetOptions == nil {
      resetOptions = newResetOptions()
   }
   resetRunner := workflow.NewRunner()

   cmd := &cobra.Command{
      Use:   "reset",
      Short: "Performs a best effort revert of changes made to this host by 'kubeadm init' or 'kubeadm join'",
      RunE: func(cmd *cobra.Command, args []string) error {
         err := resetRunner.Run(args)
         if err != nil {
            return err
         }

         // output help text instructing user how to remove cni folders
         fmt.Print(cniCleanupInstructions)
         // Output help text instructing user how to remove iptables rules
         fmt.Print(iptablesCleanupInstructions)
         return nil
      },
   }

   AddResetFlags(cmd.Flags(), resetOptions)

   // initialize the workflow runner with the list of phases
   resetRunner.AppendPhase(phases.NewPreflightPhase())
   resetRunner.AppendPhase(phases.NewRemoveETCDMemberPhase())
   resetRunner.AppendPhase(phases.NewCleanupNodePhase())

   // sets the data builder function, that will be used by the runner
   // both when running the entire workflow or single phases
   resetRunner.SetDataInitializer(func(cmd *cobra.Command, args []string) (workflow.RunData, error) {
      if cmd.Flags().Lookup(options.NodeCRISocket) == nil {
         // avoid CRI detection
         // assume that the command execution does not depend on CRISocket when --cri-socket flag is not set
         resetOptions.criSocketPath = kubeadmconstants.UnknownCRISocket
      }
      return newResetData(cmd, resetOptions, in, out)
   })

   // binds the Runner to kubeadm reset command by altering
   // command help, adding --skip-phases flag and by adding phases subcommands
   resetRunner.BindToCommand(cmd)

   return cmd
}

我们在集群内执行reset命令可以看到:

image.png

首先会展示 reset 命令的 short 短描述,并通过 resetRunner.AppendPhase() 函数绑定了三个阶段的功能函数执行:

preflight           Run reset pre-flight checks  (对应preflight运行reset前的检查)
remove-etcd-member  Remove a local etcd member.  (删除本地etcd成员)
cleanup-node        Run cleanup node.  (执行cleanup node命令)
// AppendPhase adds the given phase to the ordered sequence of phases managed by the runner.
func (e *Runner) AppendPhase(t Phase) {
   e.Phases = append(e.Phases, t)
}

Runner.Phases 就是结构体中的一个数组,来保存 Phases

phases.NewPreflightPhase()

执行结果:

[reset] Reading configuration from the cluster...

[reset] FYI: You can look at this config file with 'kubectl -n kube-system get cm kubeadm-config -o yaml'

[reset] WARNING: Changes made to this host by 'kubeadm init' or 'kubeadm join' will be reverted

[reset] Are you sure you want to proceed? [y/N]: y

[preflight] Running pre-flight checks

通过 NewPreflightPhase() 源码可以看出,这里的代码只是做了交互式的判断行为:

// NewPreflightPhase creates a kubeadm workflow phase implements preflight checks for reset
func NewPreflightPhase() workflow.Phase {
   return workflow.Phase{
      Name:    "preflight",
      Aliases: []string{"pre-flight"},
      Short:   "Run reset pre-flight checks",
      Long:    "Run pre-flight checks for kubeadm reset.",
      Run:     runPreflight,
      InheritFlags: []string{
         options.IgnorePreflightErrors,
         options.ForceReset,
         options.DryRun,
      },
   }
}

// runPreflight executes preflight checks logic.
func runPreflight(c workflow.RunData) error {
   r, ok := c.(resetData)
   if !ok {
      return errors.New("preflight phase invoked with an invalid data struct")
   }

   if !r.ForceReset() && !r.DryRun() {
      klog.Warning("[reset] WARNING: Changes made to this host by 'kubeadm init' or 'kubeadm join' will be reverted.")
      if err := util.InteractivelyConfirmAction("reset", "Are you sure you want to proceed?", r.InputReader()); err != nil {
         return err
      }
   }

   fmt.Println("[preflight] Running pre-flight checks")
   return preflight.RunRootCheckOnly(r.IgnorePreflightErrors())
}

创建一个kubeadm工作流阶段,实现预检检查以进行重置,这里没有做检查操作,只做了一个执行预检检查逻辑,如果不小心输入,提供用户交互式的操作以便确认是否进行 reset kubeadm 节点

其中 (c workflow.RunData) 断言为 resetData 类型,通过 r.InputReader() 获取到用户的输入结果,通过封装的函数 util.InteractivelyConfirmAction() 来判断用户的行为:

// InteractivelyConfirmAction asks the user whether they _really_ want to take the action.
func InteractivelyConfirmAction(action, question string, r io.Reader) error {
   fmt.Printf("[%s] %s [y/N]: ", action, question)
   scanner := bufio.NewScanner(r)
   scanner.Scan()
   if err := scanner.Err(); err != nil {
      return errors.Wrap(err, "couldn't read from standard input")
   }
   answer := scanner.Text()
   if strings.EqualFold(answer, "y") || strings.EqualFold(answer, "yes") {
      return nil
   }

   return errors.New("won't proceed; the user didn't answer (Y|y) in order to continue")
}

最后调用 preflight.RunRootCheckOnly() 函数进行预检查

phases.NewRemoveETCDMemberPhase()

获取数据目录的位置,将本地的etcd数据目录进行删除,核心代码:

func runRemoveETCDMemberPhase(c workflow.RunData) error {
   r, ok := c.(resetData)
   if !ok {
      return errors.New("remove-etcd-member-phase phase invoked with an invalid data struct")
   }
   cfg := r.Cfg()

   // Only clear etcd data when using local etcd.
   klog.V(1).Infoln("[reset] Checking for etcd config")
   etcdManifestPath := filepath.Join(kubeadmconstants.KubernetesDir, kubeadmconstants.ManifestsSubDirName, "etcd.yaml")
   etcdDataDir, err := getEtcdDataDir(etcdManifestPath, cfg)
   
   ...
   CleanDir(etcdDataDir)
   ...

   return nil
}

func getEtcdDataDir(manifestPath string, cfg *kubeadmapi.InitConfiguration) (string, error) {
   const etcdVolumeName = "etcd-data"
   var dataDir string

   if cfg != nil && cfg.Etcd.Local != nil {
      return cfg.Etcd.Local.DataDir, nil
   }
   klog.Warningln("[reset] No kubeadm config, using etcd pod spec to get data directory")

   if _, err := os.Stat(manifestPath); os.IsNotExist(err) {
      // Fall back to use the default cluster config if etcd.yaml doesn't exist, this could happen that
      // etcd.yaml is removed by other reset phases, e.g. cleanup-node.
      cfg := &v1beta3.ClusterConfiguration{}
      scheme.Scheme.Default(cfg)
      return cfg.Etcd.Local.DataDir, nil
   }
   etcdPod, err := utilstaticpod.ReadStaticPodFromDisk(manifestPath)
   if err != nil {
      return "", err
   }
   for _, volumeMount := range etcdPod.Spec.Volumes {
      if volumeMount.Name == etcdVolumeName {
         dataDir = volumeMount.HostPath.Path
         break
      }
   }
   if dataDir == "" {
      return dataDir, errors.New("invalid etcd pod manifest")
   }
   return dataDir, nil
}

通过代码可以看出,根据路径 /etc/kubernetes/manifests/etcd.yaml 的配置文件来删除本地数据

phases.NewCleanupNodePhase()

用来清理节点,比如 stop kubelet,应用了 GetInitSystem() 接口,进行一系列操作,包括删除一些创建的目录等,核心代码:

func runCleanupNode(c workflow.RunData) error {
   dirsToClean := []string{filepath.Join(kubeadmconstants.KubernetesDir, kubeadmconstants.ManifestsSubDirName)}
   r, ok := c.(resetData)
   if !ok {
      return errors.New("cleanup-node phase invoked with an invalid data struct")
   }
   certsDir := r.CertificatesDir()

   // Try to stop the kubelet service
   klog.V(1).Infoln("[reset] Getting init system")
   initSystem, err := initsystem.GetInitSystem()
   ...
   initSystem.ServiceStop("kubelet")
   ...

   // Remove contents from the config and pki directories
   if certsDir != kubeadmapiv1.DefaultCertificatesDir {
      klog.Warningf("[reset] WARNING: Cleaning a non-default certificates directory: %q\n", certsDir)
   }

   dirsToClean = append(dirsToClean, certsDir)
   if r.CleanupTmpDir() {
      tempDir := path.Join(kubeadmconstants.KubernetesDir, kubeadmconstants.TempDirForKubeadm)
      dirsToClean = append(dirsToClean, tempDir)
   }
   resetConfigDir(kubeadmconstants.KubernetesDir, dirsToClean, r.DryRun())

   if r.Cfg() != nil && features.Enabled(r.Cfg().FeatureGates, features.RootlessControlPlane) {
      if !r.DryRun() {
         klog.V(1).Infoln("[reset] Removing users and groups created for rootless control-plane")
         if err := users.RemoveUsersAndGroups(); err != nil {
            klog.Warningf("[reset] Failed to remove users and groups: %v\n", err)
         }
      } else {
         fmt.Println("[reset] Would remove users and groups created for rootless control-plane")
      }
   }

   return nil
}