【技术】从POD创建看Kubernetes源码实现(一)- kubectl

✍️ 作者:茶水间Tech

🏷️ 标签:#云计算#云原生#kubernetes#容器

📖 前言

​ kubernetes的模块比较多,架构复杂,代码量更是庞大,看代码比较麻烦,我们从现实场景出发,从创建POD分析在Kubernetes内部的代码流程,本系列文章从POD创建,整体梳理Kubernetes源码实现,其中本节主要分析kubectl 侧的流程实现。

​ 本文基于 Client Version: v1.34.3 , Server Version: v1.34.2

​ 📌 POD创建的整体架构图

在这里插入图片描述

💻 正文

📑 一、关于kubectl

在这里插入图片描述

kubelet 是Kubernetes CLI 版本的瑞士军刀,是用于管理Kubernetes集群资源的主要工具,

官方地址:https://github.com/kubernetes/kubectl

1.1 kubectl 常规用法

在了解创建POD的流程前,先得了解一下kubectl 的常规用法,便于下文代码理解。

a. 普通内置命令
#kubectl get pods
b. 插件命令
#kubectl foo bar
c. 子命令插件命令
#kubectl create customresource
d. 带标志的内置命令
#kubectl create -f file.yaml
1.2 通过kubectl 创建POD
#kubectl create -f nginx_pod.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx
  namespace: default
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginxs
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:latest
        ports:
        - containerPort: 80
📑 二、代码分析
代码逻辑图
kubectl
  |
  |-- 1. CLI 初始化 & 命令解析
  |-- 2. 读取并解析 YAML
  |-- 3. 构建 Kubernetes 对象(runtime.Object)
  |-- 4. REST 请求(POST)
详细流程如下:
2.1 程序入口:main (kubectl.go)

kubectl 是kubernetes的一部分,被整合到kubernetes大项目中,作为staging中的一个库存在

代码路径:kubernetes/cmd/kubectl/kubectl.go

func main() {
	// We need to manually parse the arguments looking for verbosity flag and
	// set appropriate level here, because in the normal flow the flag parsing,
	// including the logging verbosity, happens inside cli.RunNoErrOutput.
	// Doing it here ensures we can continue using klog during kubectl command
	// construction, which includes handling plugins and parsing .kuberc file,
	// for example.
	logs.GlogSetter(cmd.GetLogVerbosity(os.Args)) // nolint:errcheck
	command := cmd.NewDefaultKubectlCommand()
	if err := cli.RunNoErrOutput(command); err != nil {
		// Pretty-print the error and exit with an error.
		util.CheckErr(err)
	}
}
2.2 命令注册:NewDefaultKubectlCommand (cmd.go)

kubectl main() 入口函数调用了 NewDefaultKubectlCommand() => NewDefaultKubectlCommandWithArgs 做cobra的命令初始化。

  1. 预先注册:所有内置命令在 NewKubectlCommand() 中注册到 cobra 命令树
  2. 查找判断:通过 cmd.Find() 在命令树中查找,能找到就是内置命令
  3. 错误处理:找不到的命令才会尝试作为插件处理

代码路径:kubernetes/staging/src/k8s.io/kubectl/pkg/cmd/cmd.go

func NewDefaultKubectlCommand() *cobra.Command {
	ioStreams := genericiooptions.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr}
	return NewDefaultKubectlCommandWithArgs(KubectlOptions{
		PluginHandler: NewDefaultPluginHandler(plugin.ValidPluginFilenamePrefixes),
		Arguments:     os.Args,
		ConfigFlags:   defaultConfigFlags().WithWarningPrinter(ioStreams),
		IOStreams:     ioStreams,
	})
}

// NewDefaultKubectlCommandWithArgs creates the `kubectl` command with arguments
func NewDefaultKubectlCommandWithArgs(o KubectlOptions) *cobra.Command {
    //初始化KubectlCommand
	cmd := NewKubectlCommand(o)

	if o.PluginHandler == nil {
		return cmd
	}
	
	if len(o.Arguments) > 1 {
	    // 这里为传入的参数,即 create -f nginx_pod.yaml 部分
		cmdPathPieces := o.Arguments[1:]

		// only look for suitable extension executables if
		// the specified command does not already exist
		// 调用cobra的Find去匹配args
		if foundCmd, foundArgs, err := cmd.Find(cmdPathPieces); err != nil {
			// Also check the commands that will be added by Cobra.
			// These commands are only added once rootCmd.Execute() is called, so we
			// need to check them explicitly here.
			var cmdName string // first "non-flag" arguments
			for _, arg := range cmdPathPieces {
				if !strings.HasPrefix(arg, "-") {
					cmdName = arg
					break
				}
			}

			switch cmdName {
			case "help", cobra.ShellCompRequestCmd, cobra.ShellCompNoDescRequestCmd:
				// Don't search for a plugin
			default:
				if err := HandlePluginCommand(o.PluginHandler, cmdPathPieces, 1); err != nil {
					fmt.Fprintf(o.IOStreams.ErrOut, "Error: %v\n", err)
					os.Exit(1)
				}
			}
		} else if err == nil {
			// Command exists(e.g. kubectl create), but it is not certain that
			// subcommand also exists (e.g. kubectl create networkpolicy)
			// we also have to eliminate kubectl create -f
			if IsSubcommandPluginAllowed(foundCmd.Name()) && len(foundArgs) >= 1 && !strings.HasPrefix(foundArgs[0], "-") {
				subcommand := foundArgs[0]
				builtinSubcmdExist := false
				for _, subcmd := range foundCmd.Commands() {
					if subcmd.Name() == subcommand {
						builtinSubcmdExist = true
						break
					}
				}

				if !builtinSubcmdExist {
					if err := HandlePluginCommand(o.PluginHandler, cmdPathPieces, len(cmdPathPieces)-len(foundArgs)+1); err != nil {
						fmt.Fprintf(o.IOStreams.ErrOut, "Error: %v\n", err)
						os.Exit(1)
					}
				}
			}
		}
	}

	return cmd
}

**NewDefaultKubectlCommandWithArgs()**中将所有内置命令 在 NewKubectlCommand() 中注册到cobra的命令树中,

并实例化 Factory 对象f ,用于创建与 Kubernetes 交互所需的各种对象和客户端,并通过上下文参数一直传下去。

// kubernetes/staging/src/k8s.io/kubectl/pkg/cmd/cmd.go
func NewKubectlCommand(o KubectlOptions) *cobra.Command {
	// ...(略)
	// 创建主命令
	cmds := &cobra.Command{
		Use:   "kubectl",
		Short: i18n.T("kubectl controls the Kubernetes cluster manager"),
		Long: templates.LongDesc(`
      kubectl controls the Kubernetes cluster manager.

      Find more information at:
            https://kubernetes.io/docs/reference/kubectl/`),
		Run: runHelp,
		// Hook before and after Run initialize and write profiles to disk,
		// respectively.
		 // 初始化后,在运行指令前的钩子
		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
			rest.SetDefaultWarningHandler(warningHandler)

			if cmd.Name() == cobra.ShellCompRequestCmd {
				// This is the __complete or __completeNoDesc command which
				// indicates shell completion has been requested.
				plugin.SetupPluginCompletion(cmd, args)
			}

			return initProfiling()
		},
		// 运行指令后的钩子
		PersistentPostRunE: func(*cobra.Command, []string) error {
			if err := flushProfiling(); err != nil {
				return err
			}
			if warningsAsErrors {
				count := warningHandler.WarningCount()
				switch count {
				case 0:
					// no warnings
				case 1:
					return fmt.Errorf("%d warning received", count)
				default:
					return fmt.Errorf("%d warnings received", count)
				}
			}
			return nil
		},
	}
	// ...(略)

    // 实例化Factory接口,工厂模式
	f := cmdutil.NewFactory(matchVersionKubeConfigFlags)

    // kubectl定义了7类命令,结合Message和各个子命令的package名来看
	groups := templates.CommandGroups{
		{
		    // 1. 初级命令,包括 create/expose/run/set
			Message: "Basic Commands (Beginner):",
			Commands: []*cobra.Command{
				create.NewCmdCreate(f, o.IOStreams),
				expose.NewCmdExposeService(f, o.IOStreams),
				run.NewCmdRun(f, o.IOStreams),
				set.NewCmdSet(f, o.IOStreams),
			},
		},
		{
		    // 2. 中级命令,包括explain/get/edit/delete
			Message: "Basic Commands (Intermediate):",
			Commands: []*cobra.Command{
				explain.NewCmdExplain("kubectl", f, o.IOStreams),
				getCmd,
				edit.NewCmdEdit(f, o.IOStreams),
				delete.NewCmdDelete(f, o.IOStreams),
			},
		},
		{
		    // 3. 部署命令,包括 rollout/scale/autoscale
			Message: "Deploy Commands:",
			Commands: []*cobra.Command{
				rollout.NewCmdRollout(f, o.IOStreams),
				scale.NewCmdScale(f, o.IOStreams),
				autoscale.NewCmdAutoscale(f, o.IOStreams),
			},
		},
		{
		    // 4. 集群管理命令,包括 cerfificate/cluster-info/top/cordon/drain/taint
			Message: "Cluster Management Commands:",
			Commands: []*cobra.Command{
				certificates.NewCmdCertificate(f, o.IOStreams),
				clusterinfo.NewCmdClusterInfo(f, o.IOStreams),
				top.NewCmdTop(f, o.IOStreams),
				drain.NewCmdCordon(f, o.IOStreams),
				drain.NewCmdUncordon(f, o.IOStreams),
				drain.NewCmdDrain(f, o.IOStreams),
				taint.NewCmdTaint(f, o.IOStreams),
			},
		},
		{
		    // 5. 故障排查和调试,包括 describe/logs/attach/exec/port-forward/proxy/cp/auth
			Message: "Troubleshooting and Debugging Commands:",
			Commands: []*cobra.Command{
				describe.NewCmdDescribe("kubectl", f, o.IOStreams),
				logs.NewCmdLogs(f, o.IOStreams),
				attach.NewCmdAttach(f, o.IOStreams),
				cmdexec.NewCmdExec(f, o.IOStreams),
				portforward.NewCmdPortForward(f, o.IOStreams),
				proxyCmd,
				cp.NewCmdCp(f, o.IOStreams),
				auth.NewCmdAuth(f, o.IOStreams),
				debugCmd,
				events.NewCmdEvents(f, o.IOStreams),
			},
		},
		{
		    // 6. 高级命令,包括diff/apply/patch/replace/wait/convert/kustomize
			Message: "Advanced Commands:",
			Commands: []*cobra.Command{
				diff.NewCmdDiff(f, o.IOStreams),
				apply.NewCmdApply("kubectl", f, o.IOStreams),
				patch.NewCmdPatch(f, o.IOStreams),
				replace.NewCmdReplace(f, o.IOStreams),
				wait.NewCmdWait(f, o.IOStreams),
				kustomize.NewCmdKustomize(o.IOStreams),
			},
		},
		{
		    // 7. 设置命令,包括label,annotate,completion
			Message: "Settings Commands:",
			Commands: []*cobra.Command{
				label.NewCmdLabel(f, o.IOStreams),
				annotate.NewCmdAnnotate("kubectl", f, o.IOStreams),
				completion.NewCmdCompletion(o.IOStreams.Out, ""),
			},
		},
	}
	groups.Add(cmds)

	// ...(略)
    // 添加其余子命令,包括 alpha/config/plugin/version/api-versions/api-resources/options
	cmds.AddCommand(alpha)
	cmds.AddCommand(cmdconfig.NewCmdConfig(f, clientcmd.NewDefaultPathOptions(), o.IOStreams))
	cmds.AddCommand(plugin.NewCmdPlugin(o.IOStreams))
	cmds.AddCommand(version.NewCmdVersion(f, o.IOStreams))
	cmds.AddCommand(apiresources.NewCmdAPIVersions(f, o.IOStreams))
	cmds.AddCommand(apiresources.NewCmdAPIResources(f, o.IOStreams))
	cmds.AddCommand(options.NewCmdOptions(o.IOStreams.Out))

	// ...(略)

	return cmds
}
关于Factory
type factoryImpl struct {
	clientGetter genericclioptions.RESTClientGetter

	// Caches OpenAPI document and parsed resources
	openAPIParser *openapi.CachedOpenAPIParser
	oapi          *openapi.CachedOpenAPIGetter
	parser        sync.Once
	getter        sync.Once
}

func NewFactory(clientGetter genericclioptions.RESTClientGetter) Factory {
	if clientGetter == nil {
		panic("attempt to instantiate client_access_factory with nil clientGetter")
	}
	f := &factoryImpl{
		clientGetter: clientGetter,
	}

	return f
}

func (f *factoryImpl) ToRESTConfig() (*restclient.Config, error) {
	return f.clientGetter.ToRESTConfig()
}

func (f *factoryImpl) ToRESTMapper() (meta.RESTMapper, error) {
	return f.clientGetter.ToRESTMapper()
}

func (f *factoryImpl) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
	return f.clientGetter.ToDiscoveryClient()
}
2.3 命令执行 : NewCmdCreate (create.go)

因为是内置标志命令,转到了 NewCmdCreate(),通过填充默认值,并校验参数合法性,最终执行创建操作

代码路径:kubernetes/staging/src/k8s.io/kubectl/pkg/cmd/create/create.go

NewCmdCreate调用链
NewCmdCreate()
 └── RunCreate()
      └── o.RunCreate()
           └── resource.Builder.Do()
                └── Result.Visit()
                     └── FileVisitor.Visit()
                          └── StreamVisitor.Visit()
                               └── VisitorFunc
                                     └── resource.Create()

func NewCmdCreate(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command {
    // create子命令的相关选项
	o := NewCreateOptions(ioStreams)

	cmd := &cobra.Command{
		Use:                   "create -f FILENAME",
		DisableFlagsInUseLine: true,
		Short:                 i18n.T("Create a resource from a file or from stdin"),
		Long:                  createLong,
		Example:               createExample,
		Run: func(cmd *cobra.Command, args []string) {
		    // 验证参数并运行
			cmdutil.CheckErr(o.Complete(f, cmd, args))
			cmdutil.CheckErr(o.Validate())
			cmdutil.CheckErr(o.RunCreate(f, cmd))
		},
	}

	// ...(略)
	return cmd
}
2.4 核心逻辑 : NewCmdCreate (create.go)

o.RunCreate(f,cmd) 是kubectl create命令的核心执行逻辑,函数中先绑定kubeconfig,通过 f.NewBuilder() 构造了builder流水线链,其中链中FilenameParam 中遍历生成了File Visitor,builder的Do()函数中,生成了带有visitor的result object, 最终调用builder的Visit函数中 创建resource的Helper助手,进行Create 请求apiserver

代码路径:kubernetes/staging/src/k8s.io/kubectl/pkg/cmd/create/create.go

Visit 函数的处理流程

  1. 添加注解:记录 kubectl 的最后应用配置
  2. 记录命令:用于审计和回滚
  3. 创建资源:调用 API Server 创建
  4. 输出结果:按指定格式输出
func (o *CreateOptions) RunCreate(f cmdutil.Factory, cmd *cobra.Command) error {
	// ...(略)
	if len(o.Raw) > 0 {   //kubectl create --raw /api/v1/xxxx 场景
		restClient, err := f.RESTClient()
		if err != nil {
			return err
		}
		return rawhttp.RawPost(restClient, o.IOStreams, o.Raw, o.FilenameOptions.Filenames[0])
	}

	// ...(略)
	schema, err := f.Validator(o.ValidationDirective)
	if err != nil {
		return err
	}

	cmdNamespace, enforceNamespace, err := f.ToRawKubeConfigLoader().Namespace()  //绑定kubeconfig
	if err != nil {
		return err
	}
	//构造builder,生成带有具体 visitor的result
	r := f.NewBuilder().     
		Unstructured().      // 使用非结构化数据(支持任何资源类型)
		Schema(schema).      //设置验证模式
		ContinueOnError().    // 遇到错误继续处理其他资源
		NamespaceParam(cmdNamespace).DefaultNamespace().    //设置命名空间
		FilenameParam(enforceNamespace, &o.FilenameOptions).  //处理文件输入生成visitor
		LabelSelectorParam(o.Selector).    //应用标签选择器
		Flatten().     //展平嵌套的资源列表
		Do()   
	err = r.Err()
	if err != nil {
		return err
	}

	count := 0
	// 调用visit函数,创建资源
	err = r.Visit(func(info *resource.Info, err error) error {
		if err != nil {
			return err
		}
		// 1. 添加或更新注解
		if err := util.CreateOrUpdateAnnotation(cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag), info.Object, scheme.DefaultJSONEncoder()); err != nil {
			return cmdutil.AddSourceToErr("creating", info.Source, err)
		}
		// 2. 记录命令历史
		if err := o.Recorder.Record(info.Object); err != nil {
			klog.V(4).Infof("error recording current command: %v", err)
		}
		// 3. 执行创建操作
		if o.DryRunStrategy != cmdutil.DryRunClient {    //不是客户端模拟,那就得发送到apiserver
			obj, err := resource.
				NewHelper(info.Client, info.Mapping).
				DryRun(o.DryRunStrategy == cmdutil.DryRunServer).
				WithFieldManager(o.fieldManager).
				WithFieldValidation(o.ValidationDirective).
				Create(info.Namespace, true, info.Object)   //调用apiserver
			if err != nil {
				return cmdutil.AddSourceToErr("creating", info.Source, err)
			}
			info.Refresh(obj, true)
		}

		count++
		// 4. 输出结果
		return o.PrintObj(info.Object)
	})
	if err != nil {
		return err
	}
	if count == 0 {
		return fmt.Errorf("no objects passed to create")
	}
	return nil
}

其中,builder 生成的visitor 在FileVisitor的Visit中读取文件并做编码转换,最终转给了StreamVisitor的Visit

// kubernetes/staging/src/k8s.io/cli-runtime/pkg/resource/visitor.go
func (v *FileVisitor) Visit(fn VisitorFunc) error {
	var f *os.File
	if v.Path == constSTDINstr {
		f = os.Stdin
	} else {
		var err error
		f, err = os.Open(v.Path)
		if err != nil {
			return err
		}
		defer f.Close()
	}

	// TODO: Consider adding a flag to force to UTF16, apparently some
	// Windows tools don't write the BOM
	utf16bom := unicode.BOMOverride(unicode.UTF8.NewDecoder())
	v.StreamVisitor.Reader = transform.NewReader(f, utf16bom)

	return v.StreamVisitor.Visit(fn)
}

StreamVisitor的Visit中解析yaml/json 最终调用VisitorFunc 处理

// kubernetes/staging/src/k8s.io/cli-runtime/pkg/resource/visitor.go
func (v *StreamVisitor) Visit(fn VisitorFunc) error {
    d := yaml.NewYAMLOrJSONDecoder(v.Reader, 4096)
    for {
       ext := runtime.RawExtension{}
       if err := d.Decode(&ext); err != nil {
          if err == io.EOF {
             return nil
          }
          return fmt.Errorf("error parsing %s: %v", v.Source, err)
       }
       // TODO: This needs to be able to handle object in other encodings and schemas.
       ext.Raw = bytes.TrimSpace(ext.Raw)
       if len(ext.Raw) == 0 || bytes.Equal(ext.Raw, []byte("null")) {
          continue
       }
       if err := ValidateSchema(ext.Raw, v.Schema); err != nil {
          return fmt.Errorf("error validating %q: %v", v.Source, err)
       }
       info, err := v.infoForData(ext.Raw, v.Source)  //反序列化为Info object
       if err != nil {
          if fnErr := fn(info, err); fnErr != nil {
             return fnErr
          }
          continue
       }
       if err := fn(info, nil); err != nil {   //调用VisitorFunc 处理资源
          return err
       }
    }
}
2.5 请求Api Server:Create (helper.go)

Helper.Create() 通过调用restclient 封装HTTP 进行 POST请求 apiserver

请求地址:https://:/apis/apps/v1/namespaces//deployments

代码路径:kubernetes/staging/src/k8s.io/cli-runtime/pkg/resource/helper.go

func (m *Helper) Create(namespace string, modify bool, obj runtime.Object) (runtime.Object, error) {
	return m.CreateWithOptions(namespace, modify, obj, nil)
}

func (m *Helper) CreateWithOptions(namespace string, modify bool, obj runtime.Object, options *metav1.CreateOptions) (runtime.Object, error) {
	// ...(略)

	return m.createResource(m.RESTClient, m.Resource, namespace, obj, options)
}
func (m *Helper) createResource(c RESTClient, resource, namespace string, obj runtime.Object, options *metav1.CreateOptions) (runtime.Object, error) {
	return c.Post().
		NamespaceIfScoped(namespace, m.NamespaceScoped).
		Resource(resource).
		VersionedParams(options, metav1.ParameterCodec).
		Body(obj).
		Do(context.TODO()).
		Get()
}

📝 总结与展望

​ 概括kubectl create -f *.yaml 的过程中,逻辑比较简单,通过cobra解析kubctl 传参,构造kubectl 完整命令树,进而解析YAML/JSON文档,最终执行VisitorFunc 封装了HTTP POST请求,最终请求转给了Kube-apiserver。

📚 参考资料

https://blog.huweihuang.com/kubernetes-notes/principle/flow/pod-flow/

https://www.cnblogs.com/liweiboy/p/16100586.html

更多推荐