k8s源码阅读之kubectl的大致流程

347 阅读7分钟

0 前言

上篇文章我们学习了了cobra的基本使用和用法,正式基于他的强大之处,让kubectl在处理命令式参数能够支持各种各样的参数形式,包括基本的文本参数,以及yaml的解析的工作

kubectl是Kubernetes的命令行工具,用于与Kubernetes集群进行交互,帮助你逐步阅读和理解kubectl的源码。

1. 准备工作

1.1 克隆源码

首先,你需要克隆Kubernetes的源码仓库:

git clone https://github.com/kubernetes/kubernetes.git
cd kubernetes
​

然后我们直接在根目录直接make,就可以生成了所有的目录。

2. 了解项目结构

Kubernetes项目的目录结构非常庞大,以下是一些关键目录:

  • cmd/kubectl/: kubectl的主要代码目录。
  • pkg/kubectl/: kubectl的核心功能实现。
  • staging/src/k8s.io/client-go/: Kubernetes的客户端库。
  • staging/src/k8s.io/apimachinery/: Kubernetes的通用API工具和类型。

其实上边主要是cmd为通过corbal生成的cmd为入口文件,最终,代码会进入到

image-20240709231438362.png

这个目录下的kubectl中来实现的

但是本文主要是介绍关键流程。

3. 关键文件和目录

3.1 cmd/kubectl/

这个目录包含了kubectl的入口文件和命令实现。

  • cmd/kubectl/kubectl.go: kubectl的主入口文件。
  • cmd/kubectl/app/: 包含了kubectl的命令行解析和命令注册。

3.2 pkg/kubectl/

这个目录包含了kubectl的核心功能实现。

  • pkg/kubectl/cmd/: 各种kubectl命令的实现。
  • pkg/kubectl/util/: 一些实用工具和辅助函数。

4. 逐步阅读源码

4.1 入口文件

首先,从kubectl的入口文件开始:

// cmd/kubectl/kubectl.go
package main
​
import (
    "os"
​
    "k8s.io/kubectl/pkg/cmd"
    "k8s.io/component-base/cli"
)
​
func main() {
    command := cmd.NewDefaultKubectlCommand()
    code := cli.Run(command)
    os.Exit(code)
}

这个文件非常简洁,主要是创建一个kubectl命令并运行它。

其中, cmd.NewDefaultKubectlCommand 主要是实例和注册所有命令相关的数据和参数。

只有在调用code := cli.Run(command)时候,通过参数来快速查找具体的一个实例来执行和服务端的链接

4.2 命令创建

接下来,查看NewDefaultKubectlCommand的实现:

// pkg/cmd/cmd.go
package cmd
​
import (
    "k8s.io/kubectl/pkg/cmd/util"
    "k8s.io/kubectl/pkg/cmd/version"
    "k8s.io/kubectl/pkg/cmd/apply"
    // 其他命令的导入
    "github.com/spf13/cobra"
)
​
func NewDefaultKubectlCommand() *cobra.Command {
    cmd := &cobra.Command{
        Use:   "kubectl",
        Short: "kubectl controls the Kubernetes cluster manager",
        Long:  "kubectl controls the Kubernetes cluster manager.",
    }
​
    f := util.NewFactory(util.NewMatchVersionFlags())
  
   // 添加其他命令 而且通过
   groups := templates.CommandGroups{
    {
      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),
      },
    },
     。。。。
​
    cmd.AddCommand(version.NewCmdVersion(f))
    cmd.AddCommand(apply.NewCmdApply(f))
   
     
    return cmd
}

所以,目前通过组的方式来注册所有支持的命令实现

最后调用

templates.ActsAsRootCommand(cmds, filters, groups...)

这里使用了cobra库来创建命令行工具,并添加了各种子命令。

4.3 具体命令实现

pkg下的cmd目录,存了所有实现的命令

image-20240709234034013.png

apply命令为例,查看其实现:

// pkg/cmd/apply/apply.go
package apply
​
import (
    "k8s.io/kubectl/pkg/cmd/util"
    "github.com/spf13/cobra"
)
​
func NewCmdApply(f util.Factory) *cobra.Command {
    o := NewApplyOptions(f)
​
    cmd := &cobra.Command{
        Use:   "apply -f FILENAME",
        Short: "Apply a configuration to a resource by filename or stdin",
        Run: func(cmd *cobra.Command, args []string) {
            util.CheckErr(o.Complete(cmd, args))
            util.CheckErr(o.Run())
        },
    }
​
    // 添加命令行标志和选项
    return cmd
}

这里定义了apply命令,并在Run函数中调用了CompleteRun方法。

4.4 命令的具体逻辑

查看ApplyOptions的实现:

// pkg/cmd/apply/apply.go
package apply
​
import (
    "k8s.io/kubectl/pkg/cmd/util"
    "k8s.io/cli-runtime/pkg/genericclioptions"
)
​
type ApplyOptions struct {
    // 定义各种选项和参数
}
​
func NewApplyOptions(f util.Factory) *ApplyOptions {
    return &ApplyOptions{
        // 初始化选项
    }
}
​
func (o *ApplyOptions) Complete(cmd *cobra.Command, args []string) error {
    // 解析命令行参数和选项
    return nil
}
​
func (o *ApplyOptions) Run() error {
    // 实现apply命令的具体逻辑
    return nil
}

5. 深入理解

5.1 工厂模式

util.Factory是一个工厂模式,用于创建各种客户端和配置。查看其实现:

util.Factory接口定义了一些方法,用于创建和配置Kubernetes客户端。以下是Factory接口的一部分实现:

// pkg/cmd/util/factory.go
package util
​
import (
    "k8s.io/client-go/rest"
    "k8s.io/client-go/tools/clientcmd"
    "k8s.io/client-go/kubernetes"
    "k8s.io/cli-runtime/pkg/genericclioptions"
)
​
type Factory interface {
    ToRESTConfig() (*rest.Config, error)
    ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error)
    ToRESTMapper() (meta.RESTMapper, error)
    // 其他方法
}
​
type factoryImpl struct {
    clientGetter genericclioptions.RESTClientGetter
}
​
func NewFactory(clientGetter genericclioptions.RESTClientGetter) Factory {
    return &factoryImpl{
        clientGetter: clientGetter,
    }
}
​
func (f *factoryImpl) ToRESTConfig() (*rest.Config, error) {
    return f.clientGetter.ToRESTConfig()
}
​
func (f *factoryImpl) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
    config, err := f.ToRESTConfig()
    if err != nil {
        return nil, err
    }
    return discovery.NewDiscoveryClientForConfig(config)
}
​
func (f *factoryImpl) ToRESTMapper() (meta.RESTMapper, error) {
    config, err := f.ToRESTConfig()
    if err != nil {
        return nil, err
    }
    return restmapper.NewDeferredDiscoveryRESTMapper(discovery.NewDiscoveryClientForConfigOrDie(config)), nil
}

factoryImpl实现了Factory接口,并使用genericclioptions.RESTClientGetter来获取REST配置和客户端。

5.2 genericclioptions.RESTClientGetter

genericclioptions.RESTClientGetter是一个接口,定义了获取REST客户端配置的方法:

// staging/src/k8s.io/cli-runtime/pkg/genericclioptions/config_flags.go
package genericclioptions
​
import (
    "k8s.io/client-go/rest"
    "k8s.io/client-go/tools/clientcmd"
)
​
type RESTClientGetter interface {
    ToRESTConfig() (*rest.Config, error)
    ToRawKubeConfigLoader() clientcmd.ClientConfig
}
​
type ConfigFlags struct {
    // 定义各种配置标志
}
​
func (f *ConfigFlags) ToRESTConfig() (*rest.Config, error) {
    return f.ToRawKubeConfigLoader().ClientConfig()
}
​
func (f *ConfigFlags) ToRawKubeConfigLoader() clientcmd.ClientConfig {
    return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(f.KubeConfig, &clientcmd.ConfigOverrides{})
}

ConfigFlags实现了RESTClientGetter接口,并提供了从配置文件加载Kubernetes客户端配置的方法。

6. 具体命令的实现

apply命令为例,继续深入了解其具体逻辑。

6.1 ApplyOptions的实现

ApplyOptions结构体包含了apply命令的各种选项和参数:

// pkg/cmd/apply/apply.go
package apply
​
import (
    "k8s.io/kubectl/pkg/cmd/util"
    "k8s.io/cli-runtime/pkg/genericclioptions"
    "k8s.io/apimachinery/pkg/runtime"
    "k8s.io/apimachinery/pkg/runtime/serializer"
    "k8s.io/client-go/kubernetes"
    "k8s.io/client-go/rest"
)
​
type ApplyOptions struct {
    // 定义各种选项和参数
    Factory          util.Factory
    Namespace        string
    EnforceNamespace bool
    FilenameOptions  resource.FilenameOptions
    // 其他选项
}
​
func NewApplyOptions(f util.Factory) *ApplyOptions {
    return &ApplyOptions{
        Factory: f,
    }
}
​
func (o *ApplyOptions) Complete(cmd *cobra.Command, args []string) error {
    // 解析命令行参数和选项
    return nil
}
​
func (o *ApplyOptions) Run() error {
    // 实现apply命令的具体逻辑
    return nil
}

6.2 Complete方法

Complete方法用于解析命令行参数和选项,并进行必要的初始化:

func (o *ApplyOptions) Complete(cmd *cobra.Command, args []string) error {
    var err error
​
    // 解析命名空间
    o.Namespace, o.EnforceNamespace, err = o.Factory.ToRawKubeConfigLoader().Namespace()
    if err != nil {
        return err
    }
​
    // 解析文件名选项
    o.FilenameOptions.Filenames = args
​
    // 其他初始化操作
    return nil
}

Complete方法中,NamespaceEnforceNamespace是通过工厂方法ToRawKubeConfigLoader().Namespace()获取的。FilenameOptions.Filenames则是从命令行参数中解析出来的。

6.3 Run方法

Run方法是apply命令的核心逻辑,负责实际的资源应用操作:

func (o *ApplyOptions) Run() error {
    // 获取REST配置
    config, err := o.Factory.ToRESTConfig()
    if err != nil {
        return err
    }
​
    // 创建Kubernetes客户端
    clientset, err := kubernetes.NewForConfig(config)
    if err != nil {
        return err
    }
​
    // 解析资源文件
    objs, err := o.getObjectsFromFiles()
    if err != nil {
        return err
    }
​
    // 应用资源
    for _, obj := range objs {
        if err := o.applyObject(clientset, obj); err != nil {
            return err
        }
    }
​
    return nil
}

Run方法中,首先通过工厂方法获取REST配置,然后创建Kubernetes客户端。接下来,解析资源文件并应用这些资源。

6.4 解析资源文件

getObjectsFromFiles方法用于解析资源文件:

func (o *ApplyOptions) getObjectsFromFiles() ([]runtime.Object, error) {
    var objs []runtime.Object
​
    for _, filename := range o.FilenameOptions.Filenames {
        data, err := ioutil.ReadFile(filename)
        if err != nil {
            return nil, err
        }
​
        // 解析YAML或JSON文件
        obj, err := runtime.Decode(serializer.NewCodecFactory(runtime.NewScheme()).UniversalDeserializer(), data)
        if err != nil {
            return nil, err
        }
​
        objs = append(objs, obj)
    }
​
    return objs, nil
}

getObjectsFromFiles方法中,首先读取文件内容,然后使用Kubernetes的runtime.Decode方法解析YAML或JSON文件,最终返回解析后的对象列表。

6.5 应用资源

applyObject方法用于将解析后的资源对象应用到Kubernetes集群:

func (o *ApplyOptions) applyObject(clientset *kubernetes.Clientset, obj runtime.Object) error {
    // 获取对象的元数据
    accessor := meta.NewAccessor()
    namespace, err := accessor.Namespace(obj)
    if err != nil {
        return err
    }
    name, err := accessor.Name(obj)
    if err != nil {
        return err
    }
​
    // 获取对象的GVR(GroupVersionResource)
    gvk := obj.GetObjectKind().GroupVersionKind()
    mapping, err := o.Factory.ToRESTMapper().RESTMapping(gvk.GroupKind(), gvk.Version)
    if err != nil {
        return err
    }
​
    // 创建或更新资源
    resourceClient := clientset.Resource(mapping.Resource).Namespace(namespace)
    _, err = resourceClient.Create(context.TODO(), obj, metav1.CreateOptions{})
    if err != nil {
        if apierrors.IsAlreadyExists(err) {
            _, err = resourceClient.Update(context.TODO(), obj, metav1.UpdateOptions{})
        }
    }
​
    return err
}

applyObject方法中,首先获取对象的元数据(如命名空间和名称),然后通过工厂方法获取对象的GVR(GroupVersionResource)。接下来,使用Kubernetes客户端创建或更新资源。

7. 总结

通过以上步骤,我们详细了解了kubectlapply命令的实现过程。以下是关键点总结:

  1. 入口文件cmd/kubectl/kubectl.gokubectl的主入口文件。
  2. 命令创建NewDefaultKubectlCommand方法
  1. 入口文件cmd/kubectl/kubectl.gokubectl的主入口文件。
  2. 命令创建NewDefaultKubectlCommand方法使用cobra库创建kubectl命令,并添加各种子命令。
  3. 具体命令实现:以apply命令为例,NewCmdApply方法定义了apply命令,并在Run函数中调用了CompleteRun方法。
  4. 工厂模式util.Factory接口和其实现类factoryImpl用于创建和配置Kubernetes客户端。
  5. RESTClientGettergenericclioptions.RESTClientGetter接口定义了获取REST客户端配置的方法,ConfigFlags实现了该接口。
  6. 命令的具体逻辑ApplyOptions结构体包含了apply命令的各种选项和参数,Complete方法用于解析命令行参数和选项,Run方法负责实际的资源应用操作。

8. 进一步阅读和探索

8.1 其他命令

除了apply命令,kubectl还包含许多其他命令,如getcreatedelete等。你可以按照类似的方式阅读和理解这些命令的实现。

例如,get命令的实现:

// pkg/cmd/get/get.go
package get
​
import (
    "k8s.io/kubectl/pkg/cmd/util"
    "github.com/spf13/cobra"
)
​
func NewCmdGet(f util.Factory) *cobra.Command {
    o := NewGetOptions(f)
​
    cmd := &cobra.Command{
        Use:   "get [(-o|--output=)json|yaml|wide|...] (TYPE [NAME | -l label] | TYPE/NAME ...) [flags]",
        Short: "Display one or many resources",
        Run: func(cmd *cobra.Command, args []string) {
            util.CheckErr(o.Complete(cmd, args))
            util.CheckErr(o.Run())
        },
    }
​
    // 添加命令行标志和选项
    return cmd
}

8.2 测试和调试

为了更好地理解kubectl的实现,你可以编写一些测试用例,或者使用调试工具进行调试。

例如,编写一个简单的测试用例:

// pkg/cmd/apply/apply_test.go
package apply
​
import (
    "testing"
    "k8s.io/kubectl/pkg/cmd/util"
)
​
func TestApply(t *testing.T) {
    f := util.NewFactory(util.NewMatchVersionFlags())
    cmd := NewCmdApply(f)
​
    // 模拟命令行参数
    args := []string{"-f", "test.yaml"}
    cmd.SetArgs(args)
​
    if err := cmd.Execute(); err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
}