Kubernetes: Kubectl 源码分析
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 交互图如下: