0. 前言
kubectl 看了也有一段时间,期间写了两篇设计模式的文章,是时候对 kubectl 做个回顾了。
1. kubectl 入口:Cobra
kubectl 是 kubernetes 的命令行工具,通过 kubectl 实现资源的增删改查。kubectl 通过 client-go 和 kube-apiserver 进行交互,其背后封装了 https,配置文件为 kubeconfig。
kubectl 的命令行框架为 Cobra。首先,将外部参数,配置统统赋给 KubectlOptions 对象:
// NewDefaultKubectlCommand creates the `kubectl` command with default arguments func NewDefaultKubectlCommand() *cobra.Command { return NewDefaultKubectlCommandWithArgs(KubectlOptions{ PluginHandler: NewDefaultPluginHandler(plugin.ValidPluginFilenamePrefixes), Arguments: os.Args, ConfigFlags: defaultConfigFlags, IOStreams: genericiooptions.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr}, }) }
该对象包含四个属性:
- PluginHandler: PluginHandler is capable of parsing command line arguments and performing executable filename lookups to search for valid plugin files, and execute found plugins.
- Arguments: os.Args;
- ConfigFlags: ConfigFlags composes the set of values necessary for obtaining a REST client config;
- IOStreams: IOStreams provides the standard names for iostreams. This is useful for embedding and for unit testing. Inconsistent and different names make it hard to read and review code;
接着通过 ConfigFlags 属性创建工厂,工厂提供了与 kube-apiserver 的交互方式,以及验证资源对象等方法:
kubeConfigFlags := o.ConfigFlags if kubeConfigFlags == nil { kubeConfigFlags = defaultConfigFlags } kubeConfigFlags.AddFlags(flags) matchVersionKubeConfigFlags := cmdutil.NewMatchVersionFlags(kubeConfigFlags) matchVersionKubeConfigFlags.AddFlags(flags) f := cmdutil.NewFactory(matchVersionKubeConfigFlags)
1.1 创建命令
这里以创建 get 为例 getCmd := get.NewCmdGet("kubectl", f, o.IOStreams),工厂 f 和 IOStreams 作为参数传给 get 包的 NewCmdGet 函数,在函数内实现 get 命令的创建。
创建 GetOptions 对象,该对象包含和 get 命令相关的输入。
func NewCmdGet(parent string, f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { o := NewGetOptions(parent, streams) cmd := &cobra.Command{ ... Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.Run(f, args)) }, SuggestFor: []string{"list", "ps"}, } ...
Cobra 的 Run 函数实现运行 get 命令的行为。
首先,o.Complete(f, cmd, args) 补全 GetOptions 对象的输入:
func (o *GetOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { var err error o.Namespace, o.ExplicitNamespace, err = f.ToRawKubeConfigLoader().Namespace() if err != nil { return err } ...
需要注意的是,f.ToRawKubeConfigLoader().Namespace() 调用工厂的 ToRawKubeConfigLoader() 方法解析 kubeconfig 中的配置,然后调用 Namespace() 方法将 kubeconfig 中定义的 namespace 解析出来,解析 kubeconfig 的过程是反序列化 kubeconfig 文件的过程。这一过程太长,这里就不多做介绍了。
完成了输入补全,在 o.Validate() 中对输入做验证。最后,通过 o.Run(f, args) 运行命令:
func (o *GetOptions) Run(f cmdutil.Factory, args []string) error { ... r := f.NewBuilder(). Unstructured(). NamespaceParam(o.Namespace).DefaultNamespace().AllNamespaces(o.AllNamespaces). FilenameParam(o.ExplicitNamespace, &o.FilenameOptions). LabelSelectorParam(o.LabelSelector). FieldSelectorParam(o.FieldSelector). Subresource(o.Subresource). RequestChunksOf(chunkSize). ResourceTypeOrNameArgs(true, args...). ContinueOnError(). Latest(). Flatten(). TransformRequests(o.transformRequests). Do() ...
这里涉及到 建造者设计模式。通过 f 创建建造者,建造者通过一系列方法补全自身属性,在 Do 方法中根据这些属性建造 resource.Result 对象:
func (b *Builder) Do() *Result { r := b.visitorResult() ... return r }
Do 方法值得重点关注,其实现了 访问者设计模式,且是嵌套的访问者,访问的对象为 info 结构体。
首先,b.visitorResult() 方法通过 visit 多个 item 创建 resource.Result。这里以 visit resource name 为例:
func (b *Builder) visitByName() *Result { result := &Result{ singleItemImplied: len(b.names) == 1, targetsSingleItems: true, } client, err := b.getClient(mapping.GroupVersionKind.GroupVersion()) if err != nil { result.err = err return result } ... visitors := []Visitor{} for _, name := range b.names { info := &Info{ Client: client, Mapping: mapping, Namespace: selectorNamespace, Name: name, Subresource: b.subresource, } visitors = append(visitors, info) } result.visitor = VisitorList(visitors) result.sources = visitors return result }
visitByName() 方法内创建了一组 info 对象,其中保存了 resource 的信息。该对象保存在存储访问者 Visitor 的 visitors 列表,并赋值给 result.visitor 和 result.sources。
关于 result.visitor 要注意的一点是,其中的 VisitorList 也实现了 Visit 方法,它是横向的调用 info, info 是主体,fn 是这里的访问者:
type VisitorList []Visitor // Visit implements Visitor func (l VisitorList) Visit(fn VisitorFunc) error { for i := range l { if err := l[i].Visit(fn); err != nil { return err } } return nil }
得到 resource.Result 之后,通过各个访问者访问 info 资源:
func (b *Builder) Do() *Result { r := b.visitorResult() if b.flatten { r.visitor = NewFlattenListVisitor(r.visitor, b.objectTyper, b.mapper) } helpers := []VisitorFunc{} if b.defaultNamespace { helpers = append(helpers, SetNamespace(b.namespace)) } if b.requireNamespace { helpers = append(helpers, RequireNamespace(b.namespace)) } helpers = append(helpers, FilterNamespace) if b.requireObject { helpers = append(helpers, RetrieveLazy) } if b.continueOnError { r.visitor = ContinueOnErrorVisitor{Visitor: r.visitor} } r.visitor = NewDecoratedVisitor(r.visitor, helpers...) return r }
其中,FlattenListVisitor, ContinueOnErrorVisitor 和 DecoratedVisitor 是纵向的访问者嵌套关系,SetNamespace, RequireNamespace 和 RetrieveLazy 是横向的嵌套关系。
这里关于访问者模式和访问者嵌套的调用顺序就不过多介绍,有兴趣的话可以参考 浅析访问者模式。
Do 方法返回 Result,接着调用 infos, err := r.Infos() 方法实现 resource 的访问:
func (r *Result) Infos() ([]*Info, error) { ... infos := []*Info{} err := r.visitor.Visit(func(info *Info, err error) error { if err != nil { return err } infos = append(infos, info) return nil }) return infos, err }
这里 infos 是一组 info 对象访问 kube-apiserver 获得的返回结果集合。那么,哪里有定义访问 kube-apiserver 的地方呢?
答案在 RetrieveLazy 访问者:
func RetrieveLazy(info *Info, err error) error { if err != nil { return err } if info.Object == nil { return info.Get() } return nil } // Get retrieves the object from the Namespace and Name fields func (i *Info) Get() (err error) { obj, err := NewHelper(i.Client, i.Mapping).WithSubresource(i.Subresource).Get(i.Namespace, i.Name) if err != nil { if errors.IsNotFound(err) && len(i.Namespace) > 0 && i.Namespace != metav1.NamespaceDefault && i.Namespace != metav1.NamespaceAll { err2 := i.Client.Get().AbsPath("api", "v1", "namespaces", i.Namespace).Do(context.TODO()).Error() if err2 != nil && errors.IsNotFound(err2) { return err2 } } return err } i.Object = obj i.ResourceVersion, _ = metadataAccessor.ResourceVersion(obj) return nil } func (m *Helper) Get(namespace, name string) (runtime.Object, error) { req := m.RESTClient.Get(). NamespaceIfScoped(namespace, m.NamespaceScoped). Resource(m.Resource). Name(name). SubResource(m.Subresource) return req.Do(context.TODO()).Get() }
RetrieveLazy 中定义如果 info.Object 没有信息,则调用 info 的 Get 方法,在 Get 方法中根据 i.Client 和 i.Mapping 创建 Helper,通过 Helper 的 Get 方法通过 client-go 实现同 kube-apiserver 的交互,获得 info 的资源信息。
1.2 UML 交互图
通过上例分析给出 UML 交互图如下:
