Kubectl 源码分析
kubectl子命令很多,估计很少有人可以全部记住,作为开发,不知道大家有没有试着思考下,怎么样才可以比较灵活的实现这么丰富的功能,此外,命令行工具的用户友好也是很重要的。
1 Kubectl创建
一起看看Kubectl的启动:
// kubernetes\cmd\kubectl\kubectl.go
func main() {
rand.Seed(time.Now().UnixNano())
command := cmd.NewDefaultKubectlCommand()
if err := command.Execute(); err != nil {
os.Exit(1)
}
}
COPY
去掉了一些无关代码,主要操作就是两个:
1. NewDefaultKubectlCommand(): 创建kubectl命令
2. command.Execute() : 执行command命令COPY
重点看下创建的过程:
// 使用默认的初始化参数调用NewDefaultKubectlCommandWithArgs创建kubectl
func NewDefaultKubectlCommand() *cobra.Command {
return NewDefaultKubectlCommandWithArgs(NewDefaultPluginHandler(plugin.ValidPluginFilenamePrefixes), os.Args, os.Stdin, os.Stdout, os.Stderr)
}
// 字面意思
func NewDefaultKubectlCommandWithArgs(pluginHandler PluginHandler, args []string, in io.Reader, out, errout io.Writer) *cobra.Command {
// 实际创建kubectl
cmd := NewKubectlCommand(in, out, errout)
// 检测是否存在插件处理器
if pluginHandler == nil {
return cmd
}
if len(args) > 1 {
cmdPathPieces := args[1:]
// 根据传入的参数判断是否存在这样的子命令,如果不存在,则判断是否存在对应的插件可以调用,有的话就调用插件,调用完随即推出。
if _, _, err := cmd.Find(cmdPathPieces); err != nil {
if err := HandlePluginCommand(pluginHandler, cmdPathPieces); err != nil {
fmt.Fprintf(errout, "Error: %v\n", err)
os.Exit(1)
}
}
}
return cmd
}COPY
NewDefaultKubectlCommandWithArgs
方法里只有第一步是创建命令的,后续都在处理插件,插件的处理后续单独看,继续往调用栈里走,看内部的创建逻辑:
// NewKubectlCommand 创建kubcel命令,并添加相应的子命令
func NewKubectlCommand(in io.Reader, out, err io.Writer) *cobra.Command {
warningHandler := rest.NewWarningWriter(err, rest.WarningWriterOptions{Deduplicate: true, Color: term.AllowsColorOutput(err)})
warningsAsErrors := false
// 所有子命令的父命令
// 这里可以看到kubectl默认的说明
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/overview/`),
// 默认的方法是打印出help信息
Run: runHelp,
// 调用时,各个阶段的 Hook 方法
// 命令运行前回调
PersistentPreRunE: func(*cobra.Command, []string) error {
rest.SetDefaultWarningHandler(warningHandler)
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
},
BashCompletionFunction: bashCompletionFunc,
}
flags := cmds.PersistentFlags()
flags.SetNormalizeFunc(cliflag.WarnWordSepNormalizeFunc)
// 参数的归一化方法,主要作用是将所有 "_" 转化成 "-"
flags.SetNormalizeFunc(cliflag.WordSepNormalizeFunc)
// 为kubectl添加默认的性能测算参数,默认是关闭的
// 如果在参数中设置为开启,则会进行测算,具体可见命令调用前后的回调
addProfilingFlags(flags)
flags.BoolVar(&warningsAsErrors, "warnings-as-errors", warningsAsErrors, "Treat warnings received from the server as errors and exit with a non-zero exit code")
kubeConfigFlags := genericclioptions.NewConfigFlags(true).WithDeprecatedPasswordFlag()
kubeConfigFlags.AddFlags(flags)
matchVersionKubeConfigFlags := cmdutil.NewMatchVersionFlags(kubeConfigFlags)
matchVersionKubeConfigFlags.AddFlags(cmds.PersistentFlags())
cmds.PersistentFlags().AddGoFlagSet(flag.CommandLine)
f := cmdutil.NewFactory(matchVersionKubeConfigFlags)
i18n.LoadTranslations("kubectl", nil)
cmds.SetGlobalNormalizationFunc(cliflag.WarnWordSepNormalizeFunc)
ioStreams := genericclioptions.IOStreams{In: in, Out: out, ErrOut: err}
// 添加当前的子命令组
groups := templates.CommandGroups{
{
Message: "Basic Commands (Beginner):",
Commands: []*cobra.Command{
create.NewCmdCreate(f, ioStreams),
expose.NewCmdExposeService(f, ioStreams),
run.NewCmdRun(f, ioStreams),
set.NewCmdSet(f, ioStreams),
},
},
{
Message: "Basic Commands (Intermediate):",
Commands: []*cobra.Command{
explain.NewCmdExplain("kubectl", f, ioStreams),
get.NewCmdGet("kubectl", f, ioStreams),
edit.NewCmdEdit(f, ioStreams),
delete.NewCmdDelete(f, ioStreams),
},
},
{
Message: "Deploy Commands:",
Commands: []*cobra.Command{
rollout.NewCmdRollout(f, ioStreams),
scale.NewCmdScale(f, ioStreams),
autoscale.NewCmdAutoscale(f, ioStreams),
},
},
{
Message: "Cluster Management Commands:",
Commands: []*cobra.Command{
certificates.NewCmdCertificate(f, ioStreams),
clusterinfo.NewCmdClusterInfo(f, ioStreams),
top.NewCmdTop(f, ioStreams),
drain.NewCmdCordon(f, ioStreams),
drain.NewCmdUncordon(f, ioStreams),
drain.NewCmdDrain(f, ioStreams),
taint.NewCmdTaint(f, ioStreams),
},
},
{
Message: "Troubleshooting and Debugging Commands:",
Commands: []*cobra.Command{
describe.NewCmdDescribe("kubectl", f, ioStreams),
logs.NewCmdLogs(f, ioStreams),
attach.NewCmdAttach(f, ioStreams),
cmdexec.NewCmdExec(f, ioStreams),
portforward.NewCmdPortForward(f, ioStreams),
proxy.NewCmdProxy(f, ioStreams),
cp.NewCmdCp(f, ioStreams),
auth.NewCmdAuth(f, ioStreams),
debug.NewCmdDebug(f, ioStreams),
},
},
{
Message: "Advanced Commands:",
Commands: []*cobra.Command{
diff.NewCmdDiff(f, ioStreams),
apply.NewCmdApply("kubectl", f, ioStreams),
patch.NewCmdPatch(f, ioStreams),
replace.NewCmdReplace(f, ioStreams),
wait.NewCmdWait(f, ioStreams),
kustomize.NewCmdKustomize(ioStreams),
},
},
{
Message: "Settings Commands:",
Commands: []*cobra.Command{
label.NewCmdLabel(f, ioStreams),
annotate.NewCmdAnnotate("kubectl", f, ioStreams),
completion.NewCmdCompletion(ioStreams.Out, ""),
},
},
}
groups.Add(cmds)
filters := []string{"options"}
alpha := NewCmdAlpha(f, ioStreams)
if !alpha.HasSubCommands() {
filters = append(filters, alpha.Name())
}
templates.ActsAsRootCommand(cmds, filters, groups...)
for name, completion := range bashCompletionFlags {
if cmds.Flag(name) != nil {
if cmds.Flag(name).Annotations == nil {
cmds.Flag(name).Annotations = map[string][]string{}
}
cmds.Flag(name).Annotations[cobra.BashCompCustom] = append(
cmds.Flag(name).Annotations[cobra.BashCompCustom],
completion,
)
}
}
// 添加一些不在默认分组内的方法
cmds.AddCommand(alpha)
cmds.AddCommand(cmdconfig.NewCmdConfig(f, clientcmd.NewDefaultPathOptions(), ioStreams))
cmds.AddCommand(plugin.NewCmdPlugin(f, ioStreams))
cmds.AddCommand(version.NewCmdVersion(f, ioStreams))
cmds.AddCommand(apiresources.NewCmdAPIVersions(f, ioStreams))
cmds.AddCommand(apiresources.NewCmdAPIResources(f, ioStreams))
cmds.AddCommand(options.NewCmdOptions(ioStreams.Out))
return cmds
}COPY
kubectl命令的创建过程就是初始化Kubectl命令,明确命令使用前后的回调函数,然后将几组子命令添加到根命令下。
2 Kubectl执行
kubectl大部分命令的执行其实是交给子命令模块去做的。Cobra规定了,子命令与Root命令的基本代码结构,如果要添加子命令,符合一般约束的前提下,在cmd目录下新建一个文件即可。
▾ appName/
▾ cmd/
add.go
your.go
commands.go
here.go
main.goCOPY
可以看一下目前Kubectl下的子命令:
目前kubectl支持的子命令肯定都会在这里,还记得之前kubectl的构造函数么?K8s的代码里,将这些子命令分为了几个种类:
种类 | 命令 |
---|---|
Basic Commands (Beginner) | create\expose\run\set |
Basic Commands (Intermediate) | explain\get\edit\delete |
Deploy Commands | rollout\scale\autoscale |
Cluster Management Commands | certificate\clusterinfo\top\cordon\uncordon\drain\taint |
Troubleshooting and Debugging Commands | describe\logs\attach\exec\portforward\proxy\cp\auth\debug |
Advanced Commands | diff\apply\patch\replace\wait\kustomize |
Settings Commands | label\annotate\completion |
其他 | version\options\config\apiversions\apiresources |
里面有一些命令比较常用,有一些我也是第一次知道,比如debug\wait。
这些子命令都通过AddCommand
方法,添加到Command
结构体的成员变量commands
中了。commands
作为一个数组,主要用于存储程序支持的子命令。
// kubectl的执行逻辑
func (c *Command) ExecuteC() (cmd *Command, err error) {
if c.ctx == nil {
c.ctx = context.Background()
}
// 以根命令的形式运行
if c.HasParent() {
return c.Root().ExecuteC()
}
// 窗口检测回调函数,主要作用是检测当前程序是否是被鼠标点击触发的,如果不是命令行模式,报错然后退出
if preExecHookFn != nil {
preExecHookFn(c)
}
// 初始化帮助信息,覆盖默认的帮助信息
c.InitDefaultHelpCmd()
args := c.args
// 一些异常case的检测
if c.args == nil && filepath.Base(os.Args[0]) != "cobra.test" {
args = os.Args[1:]
}
// 命令补全初始化
c.initCompleteCmd(args)
var flags []string
// 这一步很关键
// 这一步会明确实际执行的子命令,然后赋值给cmd
if c.TraverseChildren {
cmd, flags, err = c.Traverse(args)
} else {
cmd, flags, err = c.Find(args)
}
if err != nil {
// 如果没有找到对应的子命令,提示出错
if cmd != nil {
c = cmd
}
if !c.SilenceErrors {
c.PrintErrln("Error:", err.Error())
c.PrintErrf("Run '%v --help' for usage.\n", c.CommandPath())
}
return c, err
}
// 子命令执行前的准备
cmd.commandCalledAs.called = true
if cmd.commandCalledAs.name == "" {
cmd.commandCalledAs.name = cmd.Name()
}
// context传输
if cmd.ctx == nil {
cmd.ctx = c.ctx
}
// 执行
err = cmd.execute(flags)
if err != nil {
// Always show help if requested, even if SilenceErrors is in
// effect
if err == flag.ErrHelp {
cmd.HelpFunc()(cmd, args)
return cmd, nil
}
// If root command has SilentErrors flagged,
// all subcommands should respect it
if !cmd.SilenceErrors && !c.SilenceErrors {
c.PrintErrln("Error:", err.Error())
}
// If root command has SilentUsage flagged,
// all subcommands should respect it
if !cmd.SilenceUsage && !c.SilenceUsage {
c.Println(cmd.UsageString())
}
}
return cmd, err
}
// command的执行框架
// 按照
// 1. 解析命令行参数
// 2. help参数检测
// 3. 运行前检测
// 4. preRun
// 5. p.PersistentPreRun | p.PersistentPreRunE p是父命令
// 6. PreRunE | PreRun
// 7. RunE | Run
// 8. PostRunE | PostRun
// 9. p.PersistentPostRunE | p.PersistentPostRun
func (c *Command) execute(a []string) (err error) {
if c == nil {
return fmt.Errorf("Called Execute() on a nil Command")
}
if len(c.Deprecated) > 0 {
c.Printf("Command %q is deprecated, %s\n", c.Name(), c.Deprecated)
}
// initialize help and version flag at the last point possible to allow for user
// overriding
c.InitDefaultHelpFlag()
c.InitDefaultVersionFlag()
err = c.ParseFlags(a)
if err != nil {
return c.FlagErrorFunc()(c, err)
}
// If help is called, regardless of other flags, return we want help.
// Also say we need help if the command isn't runnable.
helpVal, err := c.Flags().GetBool("help")
if err != nil {
// should be impossible to get here as we always declare a help
// flag in InitDefaultHelpFlag()
c.Println("\"help\" flag declared as non-bool. Please correct your code")
return err
}
if helpVal {
return flag.ErrHelp
}
// for back-compat, only add version flag behavior if version is defined
if c.Version != "" {
versionVal, err := c.Flags().GetBool("version")
if err != nil {
c.Println("\"version\" flag declared as non-bool. Please correct your code")
return err
}
if versionVal {
err := tmpl(c.OutOrStdout(), c.VersionTemplate(), c)
if err != nil {
c.Println(err)
}
return err
}
}
if !c.Runnable() {
return flag.ErrHelp
}
c.preRun()
argWoFlags := c.Flags().Args()
if c.DisableFlagParsing {
argWoFlags = a
}
if err := c.ValidateArgs(argWoFlags); err != nil {
return err
}
for p := c; p != nil; p = p.Parent() {
if p.PersistentPreRunE != nil {
if err := p.PersistentPreRunE(c, argWoFlags); err != nil {
return err
}
break
} else if p.PersistentPreRun != nil {
p.PersistentPreRun(c, argWoFlags)
break
}
}
if c.PreRunE != nil {
if err := c.PreRunE(c, argWoFlags); err != nil {
return err
}
} else if c.PreRun != nil {
c.PreRun(c, argWoFlags)
}
if err := c.validateRequiredFlags(); err != nil {
return err
}
if c.RunE != nil {
if err := c.RunE(c, argWoFlags); err != nil {
return err
}
} else {
c.Run(c, argWoFlags)
}
if c.PostRunE != nil {
if err := c.PostRunE(c, argWoFlags); err != nil {
return err
}
} else if c.PostRun != nil {
c.PostRun(c, argWoFlags)
}
for p := c; p != nil; p = p.Parent() {
if p.PersistentPostRunE != nil {
if err := p.PersistentPostRunE(c, argWoFlags); err != nil {
return err
}
break
} else if p.PersistentPostRun != nil {
p.PersistentPostRun(c, argWoFlags)
break
}
}
return nil
}COPY
就是这么简单,kubectl的初始化和子命令调用看起来没有什么太大的复杂性。
3 子命令结构设计
Kubectl的子命令,主要基于三种设计模式:建造者模式、访问者模式、装饰器模式。
建造者模式(Builder Pattern)比较简单,就是使用多个简单的对象一步一步构建成一个复杂的对象。
访问者模式(Visitor Pattern)是一种行为型设计模式,可以将算法和操作对象的结构进行分离,遵循开放、封闭原则的一种方法。我们需要重点看下这个模式是如何工作的。
3.1 访问者模式示例
写一个简单的访问者模式的应用,主要元素有访问对象、访问者、调用方
type Visitor func(person Person)
// 基类
// 被访问的对象,通过accept方法接受访问
type Person interface {
accept(visitor Visitor)
}
// 存储学生信息的类型
// 实现了Person接口
type Student struct {
Name string `json:"name"`
Age int `json:"age"`
Score int `json:"score"`
}
func (s Student) accept(visitor Visitor) {
visitor(s)
}
// 存储教师信息
type Teacher struct {
Name string `json:"name"`
Age int `json:"age"`
Course string `json:"course"`
}
func (t Teacher) accept(visitor Visitor) {
visitor(t)
}COPY
定义两个简单的访问器
// 导出json格式数据的访问器
func JsonVisitor(person Person) {
bytes, err := json.Marshal(person)
if err != nil {
panic(err)
}
fmt.Println(string(bytes))
}
// 导出yaml格式信息的访问器
func YamlVisitor(person Person) {
bytes, err := yaml.Marshal(person)
if err != nil {
panic(err)
}
fmt.Println(string(bytes))
}COPY
调用一下
func main() {
s := Student{Age: 10, Name: "小明", Score: 90}
t := Teacher{Name: "李", Age: 35, Course: "数学"}
persons := []Person{s, t}
for _, person := range persons {
person.accept(JsonVisitor)
person.accept(YamlVisitor)
}
}COPY
上面是一个简单的示例,看起来有一点像策略模式。两者没有本质区别,都是针对多态的一种处理。不过访问者模式更侧重对于被访问者的状态的修改,而策略模式更侧重的是处理逻辑的扩展。实际用的时候不用考虑那么多,怎么方便怎么来就好了。上面的例子比较简单,复杂的情况下,一个结构体里会有很多不同的状态,每个访问器负责修改一部分状态。kubectl中就是这样的场景。
3.2 Kubectl对于访问者模式的应用
在k8s中,存在各种各样的资源类型,每个类型都包含复杂的状态信息,有些是公用的,有的是独有的。kubectl的子命令,需要对不同的资源类型做出处理, 处理流程上:1. 读取命令行参数、或者读取指定文件、或者读取url,构建命令 2. 调用k8s的client,向API Server发起请求 3. 完成处理
处理逻辑上,可以抽象为:1. 获取参数 2. Builder模式构建一个资源的集合 3. 使用visitor模式处理这些资源状态的集合,包括本地资源的修改操作、向Api Server的请求操作 3. 完成处理
这是一个比较抽象的过程。下面可以具体看一下实际实现的代码
// Info 封装了一些client调用时所需要的基本信息
type Info struct {
// 只有需要远端调用的时候才会初始化client和mapping
Client RESTClient
Mapping *meta.RESTMapping
// 指定namespace的时候才会设置这个参数
Namespace string
Name string
// 可选参数 url或者文件路径
Source string
Object runtime.Object
ResourceVersion string
}
// 访问器基类,用于修改资源类型的操作
type Visitor interface {
Visit(VisitorFunc) error
}
type VisitorFunc func(*Info, error) errorCOPY
可以看到,Visitor类型里包含方法Visit,Visit的参数是一个VisitorFunc。
为了方便理解Visitor的使用,先举个例子,我们定义几个不同类型的Visitor
// name visitor
// 假设这个visitor主要用于访问 Info 结构中的 Name 和 NameSpace 成员
type NameVisitor struct {
visitor Visitor
}
func (v NameVisitor) Visit(fn VisitorFunc) error {
return v.visitor.Visit(func(info *Info, err error) error {
fmt.Println("NameVisitor() before call function")
err = fn(info, err)
if err == nil {
fmt.Printf("==> Name=%s, NameSpace=%s\n", info.Name, info.Namespace)
}
fmt.Println("NameVisitor() after call function")
return err
})
}
// Other Visitor
// 这个Visitor主要用来访问 Info 结构中的 OtherThings 成员
type OtherThingsVisitor struct {
visitor Visitor
}
func (v OtherThingsVisitor) Visit(fn VisitorFunc) error {
return v.visitor.Visit(func(info *Info, err error) error {
fmt.Println("OtherThingsVisitor() before call function")
err = fn(info, err)
if err == nil {
fmt.Printf("==> OtherThings=%s\n", info.OtherThings)
}
fmt.Println("OtherThingsVisitor() after call function")
return err
})
}
// Log Visitor
type LogVisitor struct {
visitor Visitor
}
func (v LogVisitor) Visit(fn VisitorFunc) error {
return v.visitor.Visit(func(info *Info, err error) error {
fmt.Println("LogVisitor() before call function")
err = fn(info, err)
fmt.Println("LogVisitor() after call function")
return err
})
}
// 调用逻辑
func main() {
info := Info{}
var v Visitor = &info
v = LogVisitor{v}
v = NameVisitor{v}
v = OtherThingsVisitor{v}
loadFile := func(info *Info, err error) error {
info.Name = "Hao Chen"
info.Namespace = "MegaEase"
info.OtherThings = "We are running as remote team."
return nil
}
v.Visit(loadFile)
}
COPY
上面的代码,每个visitor里
有一个 Visitor
接口成员,这里意味着多态。在实现 Visit()
方法时,其调用了自己结构体内的那个Visitor
的Visitor()
方法,这其实是一种修饰器的模式,用另一个Visitor修饰了自己
调用后,输出如下信息
LogVisitor() before call function
NameVisitor() before call function
OtherThingsVisitor() before call function
==> OtherThings=We are running as remote team.
OtherThingsVisitor() after call function
==> Name=Hao Chen, NameSpace=MegaEase
NameVisitor() after call function
LogVisitor() after call functionCOPY
显而易见,上面的代码有以下几种功效:
解耦了数据和程序。 使用了修饰器模式 还做出来pipeline的模式
搞清楚上述逻辑后,在回过来看k8s的实现就会一目了然
// Decorate 就是装饰的意思,显而易见,装饰过的访问器
type DecoratedVisitor struct {
visitor Visitor
// 一个装饰器集合
decorators []VisitorFunc
}
// Visit implements Visitor
// 1. 下潜一层,调用自己的Visitor内的访问器方法
// 2. 调用自己 所有装饰者方法,即自身的访问器
// 3. 最后再调用传入的fn
// 达到一种嵌套调用、链式调用的效果
func (v DecoratedVisitor) Visit(fn VisitorFunc) error {
return v.visitor.Visit(func(info *Info, err error) error {
if err != nil {
return err
}
for i := range v.decorators {
if err := v.decorators[i](info, nil); err != nil {
return err
}
}
return fn(info, nil)
})
}COPY
上面的代码并不复杂,
用一个 DecoratedVisitor
的结构来存放所有的VistorFunc
函数NewDecoratedVisitor
可以把所有的VisitorFunc
转给它,构造DecoratedVisitor
对象。DecoratedVisitor
实现了Visit()
方法,里面就是来做一个for-loop,顺着调用所有的VisitorFunc
用一个非常巧妙的方法,把装饰器模式和访问者模式结合在了一起,又把操作与数据解耦、操作与操作解耦,叹为观止!
那怎么调用呢?
info := Info{}
var v Visitor = &info
v = NewDecoratedVisitor(v, NameVisitor, OtherVisitor)
v.Visit(LoadFile)COPY
举个例子,kubectl的apply方法的实现:
// 路径 vendor/k8s.io/kubectl/pkg/cmd/apply/apply.go
func (o *ApplyOptions) GetObjects() ([]*resource.Info, error) {
var err error = nil
if !o.objectsCached {
// 通过builder模式,逐步构建一个完整的资源对象
r := o.Builder.
Unstructured().
Schema(o.Validator).
ContinueOnError().
NamespaceParam(o.Namespace).DefaultNamespace().
FilenameParam(o.EnforceNamespace, &o.DeleteOptions.FilenameOptions).
LabelSelectorParam(o.Selector).
Flatten().
Do()
o.objects, err = r.Infos()
o.objectsCached = true
}
return o.objects, err
}
// 路径 k8s.io/cli-runtime/pkg/resource/builder.go
func (b *Builder) Do() *Result {
r := b.visitorResult()
r.mapper = b.Mapper()
if r.err != nil {
return r
}
if b.flatten {
r.visitor = NewFlattenListVisitor(r.visitor, b.objectTyper, b.mapper)
}
// 访问器方法集合
helpers := []VisitorFunc{}
if b.defaultNamespace {
// 设置namespace的访问器 SetNamespace
helpers = append(helpers, SetNamespace(b.namespace))
}
if b.requireNamespace {
// 需要namespace的提示操作 RequireNamespace
helpers = append(helpers, RequireNamespace(b.namespace))
}
// 过滤namespace的访问器 FilterNamespace
helpers = append(helpers, FilterNamespace)
if b.requireObject {
// 另一个访问器
helpers = append(helpers, RetrieveLazy)
}
if b.continueOnError {
// 构造被装饰的访问者
r.visitor = NewDecoratedVisitor(ContinueOnErrorVisitor{r.visitor}, helpers...)
} else {
r.visitor = NewDecoratedVisitor(r.visitor, helpers...)
}
return r
}COPY
kubectl的子命令基本都是这样的工作模式。具体的业务逻辑就需要看实际的处理需求了。
原文链接:https://jeffdingzone.com/2021/02/k8s%e6%ba%90%e7%a0%81%e5%88%86%e6%9e%903-kubectl%e5%ae%9e%e7%8e%b0%e5%88%86%e6%9e%90/
K8S 进阶训练营