一、引言
最近开始研读 Kubernetes 源码,希望能借此更加深入地理解其运行机理!
由于 Kubernetes 代码量很庞大,我将分模块分组件地去分析和理解,并把自己的发现共享出来。
第一篇博客,先从简单的命令行解析工具 cobra 开始。
二、源码解析
Kubernetes 中的组件(如 kubeadm、kubectl 等)都是命令行方式运行的,命令解析是重要一步
在 Kubernetes 系统中,cobra 被广泛应用,因为其实在是非常的简单易用
关于 cobra 的更多资料,可以参考其 GitHub 官网:https://github.com/spf13/cobra
下面以 kubeadm 为例,看一下 cobra 是怎么被使用的吧
首先,找到 kubeadm 的入口,cmd/kubeadm/kubeadm.go
package main
import (
"fmt"
"os"
"k8s.io/kubernetes/cmd/kubeadm/app"
)
func main() {
if err := app.Run(); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
os.Exit(0)
}
这里,直接调用了 app.Run() 方法,可见这只是个壳子,真正的逻辑在 k8s.io/kubernetes/cmd/kubeadm/app
然后,我们跟踪到 cmd/kubeadm/app/kubeadm.go,找到其 Run 方法
package app
import (
"flag"
"os"
_ "github.com/golang/glog"
"github.com/spf13/pflag"
utilflag "k8s.io/apiserver/pkg/util/flag"
"k8s.io/kubernetes/cmd/kubeadm/app/cmd"
)
// Run creates and executes new kubeadm command
func Run() error {
pflag.CommandLine.SetNormalizeFunc(utilflag.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("google-json-key")
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 方法在设定了一系列的参数属性信息后,创建了一个 cmd 对象,并执行了其 Execute 方法
经过分析,这里的 cmd 对象就是一个 cobra 命令对象,而 Execute 正是 cobra 提供的执行命令的方法
cobra 内部使用了 pflag 库,这里通过设置 pflag 属性,可以对 cobra 的运行产生作用
pflag 也兼容 golang flag 库,这里通过 AddGoFlagSet(flag.CommandLine) 实现了对 golang flag 的兼容
紧接着,我们跟踪到 NewKubeadmCommand 的函数实现, cmd/kubeadm/app/cmd/cmd.go
// 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.
┌──────────────────────────────────────────────────────────┐
│ KUBEADM IS CURRENTLY IN BETA │
│ │
│ But please, try it out and give us feedback at: │
│ https://github.com/kubernetes/kubeadm/issues │
│ and at-mention @kubernetes/sig-cluster-lifecycle-bugs │
│ or @kubernetes/sig-cluster-lifecycle-feature-requests │
└──────────────────────────────────────────────────────────┘
Example usage:
Create a two-machine cluster with one master (which controls the cluster),
and one node (where your workloads, like Pods and Deployments run).
┌──────────────────────────────────────────────────────────┐
│ On the first machine: │
├──────────────────────────────────────────────────────────┤
│ master# kubeadm init │
└──────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────┐
│ On the second machine: │
├──────────────────────────────────────────────────────────┤
│ node# kubeadm join <arguments-returned-from-init> │
└──────────────────────────────────────────────────────────┘
You can then repeat the second step on as many other machines as you like.
`),
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(NewCmdCompletion(out, ""))
cmds.AddCommand(NewCmdConfig(out))
cmds.AddCommand(NewCmdInit(out))
cmds.AddCommand(NewCmdJoin(out))
cmds.AddCommand(NewCmdReset(in, out))
cmds.AddCommand(NewCmdVersion(out))
cmds.AddCommand(NewCmdToken(out, err))
cmds.AddCommand(upgrade.NewCmdUpgrade(out))
// Wrap not yet fully supported commands in an alpha subcommand
experimentalCmd := &cobra.Command{
Use: "alpha",
Short: "Experimental sub-commands not yet fully functional.",
}
experimentalCmd.AddCommand(phases.NewCmdPhase(out))
cmds.AddCommand(experimentalCmd)
AddKubeadmOtherFlags(cmds.PersistentFlags(), &rootfsPath)
return cmds
}
该函数首先构造了 kubeadm 的根命令对象 cmds(也就是 kubeadm 命令)
然后依次将 kubeadm 的子命令(如 init、join)等通过 cmds.AddCommand()方法添加到 cmds 对象
可见,cmd/kubeadm/app/kubeadm.go 中末尾执行的 cmd.Execute() 正是执行的这个 cmds 的 Execute() 方法
各个子命令的构造大同小异,为了简单起见,我们看一下 NewCmdVersion 是如何实现的吧
跟踪到 cmd/kubeadm/app/cmd/version.go
// NewCmdVersion provides the version information of kubeadm.
func NewCmdVersion(out io.Writer) *cobra.Command {
cmd := &cobra.Command{
Use: "version",
Short: "Print the version of kubeadm",
Run: func(cmd *cobra.Command, args []string) {
err := RunVersion(out, cmd)
kubeadmutil.CheckErr(err)
},
}
cmd.Flags().StringP("output", "o", "", "Output format; available options are 'yaml', 'json' and 'short'")
return cmd
}
我们看到,NewCmdVersion 函数同样返回了一个 cobra Command 对象,
而与之前看到的根命令 kubeadm不同的是
这里 version 子命令的定义中包含了运行实体 Run 函数的具体定义,
这表明 version 命令不再是父级命令了,它能真正被执行
至于 version 命令可以识别的参数,可以通过 cmd.Falgs() 设定
这里把输入的参数直接绑定到 output 上了,使用的时候可以通过 cmd.Flags().GetString(“output”) 获取
三、源码分析总结
整体而言,kubernetes 命令的执行是通过组合 cobra command 生成父子命令树,
最终命令的执行是在树的叶节点上通过 Run: func(cmd *cobra.Command, args []string){...} 定义的
命令参数通过 cmd.Flags() 设置
主函数 main 通过调起根命令的 Execute 激活整个命令解析和执行过程
四、实验验证
实验代码 GitHub 地址:https://github.com/SataQiu/k8s-learn/tree/master/cobra-example
package main
import (
"fmt"
"github.com/spf13/cobra"
)
func NewRootCommand() *cobra.Command {
cmds := &cobra.Command{
Use: "cobra-example",
Short: "cobra-example: my first cmd tool based on cobra.",
}
cmds.AddCommand(NewSubCommandOfHello())
cmds.AddCommand(NewSubCommandOfBye())
return cmds
}
func NewSubCommandOfHello() *cobra.Command {
cmd := &cobra.Command{
Use: "hello",
Short: "Say hello.",
Run: func(cmd *cobra.Command, args []string) {
name, _ := cmd.Flags().GetString("name")
fmt.Println("Welcome " + name)
},
}
cmd.Flags().StringP("name", "n", "", "Name: input your name.")
return cmd
}
func NewSubCommandOfBye() *cobra.Command {
cmd := &cobra.Command{
Use: "bye",
Short: "Say bye.",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Bye!")
},
}
return cmd
}
func main() {
cmd := NewRootCommand()
cmd.Execute()
}
这里,我们首先定义一个根命令 cobra-example,然后添加两个子命令 hello 和 bye,其中 hello 可以接受 name 参数
试验一下,执行编译后的二进制文件
执行子命令