kubernetes CRD 开发入门指南

2020/07/05 posted in  kubernetes

CRD是什么?

CRD全称为Custom Resource Definition,是kubernetes提供的开放的扩展api方式。

下面是我的理解:

通常我们说的Operator指的是CRD + Controller。CRD也是kubernetes提供的一种资源类型,我们通过CRD向kubernetes注册自定义的资源类型。空有定义好的CRD没有任何作用,要让自定义的资源类型像kubernetes中的资源一样工作,需要开发一个Controller来控制、调度、实现该资源中定义的状态。而我们真正使用的则是CR(Custom Resource)。

举个例子

现在很多项目都大量使用到CRD, 为了能有更清楚的理解,下面以matrix项目为例:

[root@centos01 ~]# kubectl get crd | grep crd.cxwen.com
dns.crd.cxwen.com                                     2020-06-30T06:21:09Z
etcdclusters.crd.cxwen.com                            2020-06-30T06:21:09Z
masters.crd.cxwen.com                                 2020-06-30T06:21:09Z
matrices.crd.cxwen.com                                2020-06-30T06:21:09Z
networkplugins.crd.cxwen.com                          2020-06-30T06:21:09Z

可以看到matrix定义了许多的CRD。以masters.crd.cxwen.com为例,来看看CRD里面到底定义了哪些东西。

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: masters.crd.cxwen.com
spec:
  conversion:
    strategy: None
  group: crd.cxwen.com
  names:
    kind: Master
    listKind: MasterList
    plural: masters
    singular: master
  preserveUnknownFields: true
  scope: Namespaced
  versions:
  - additionalPrinterColumns:       # 定义kubectl get时打印出的字段
    - description: version
      jsonPath: .spec.version
      name: VERSION
      type: string
    - description: pod replicas
      jsonPath: .spec.replicas
      name: REPLICAS
      type: string
    - description: etcdcluster name
      jsonPath: .spec.etcdCluster
      name: ETCD
      type: string
    - description: expose type
      jsonPath: .spec.expose.method
      name: EXPOSETYPE
      type: string
    - description: expose node
      jsonPath: .spec.expose.node
      name: EXPOSENODE
      type: string
    - description: expose port
      jsonPath: .spec.expose.port
      name: EXPOSEPORT
      type: string
    - description: phase
      jsonPath: .status.phase
      name: PHASE
      type: string
    - jsonPath: .metadata.creationTimestamp
      name: AGE
      type: date
    name: v1
    schema:                       # 定义CRD的schema
      openAPIV3Schema:
        description: Master is the Schema for the masters API
        properties:
          apiVersion:
            type: string
          kind:
            type: string
          metadata:
            type: object
          spec:
            description: MasterSpec defines the desired state of Master
            properties:
              etcdCluster:
                type: string
              expose:
                properties:
                  method:
                    type: string
                  node:
                    items:
                      type: string
                    type: array
                  port:
                    type: string
                type: object
              imageRegistry:
                type: string
              imageRepo:
                properties:
                  apiserver:
                    type: string
                  controllerManager:
                    type: string
                  proxy:
                    type: string
                  scheduler:
                    type: string
                type: object
              replicas:
                type: integer
              version:
                description: Foo is an example field of Master. Edit Master_types.go
                  to remove/update
                type: string
            type: object
          status:
            description: MasterStatus defines the observed state of Master
            properties:
              adminKubeconfig:
                type: string
              exposeUrl:
                items:
                  type: string
                type: array
              phase:
                description: 'INSERT ADDITIONAL STATUS FIELD - define observed state
                  of cluster Important: Run "make" to regenerate code after modifying
                  this file'
                type: string
            type: object
        type: object
    served: true
    storage: true
    subresources:
      status: {}

CRD里面主要定义了两部分内容:

1、additionalPrinterColumns

顾名思意,additional Printer Columns就是额外的打印列的意思,即设置使用kubectl get命令去查看自定义资源时会打印哪些字段,例如:

[root@centos01 ~]# kubectl get master
NAME         VERSION    REPLICAS   ETCD         EXPOSETYPE   EXPOSENODE         EXPOSEPORT   PHASE   AGE
example-km   v1.15.12   1          example-ec   NodePort     [192.168.83.128]   31299        Ready   12m

这里就打印出了上面CRD中定义的字段。

2、schema

定义Custom Resource的模式或者说规范,这里面定义一个CR的各个属性的数据类型,CR一定要遵循CRD里面的定义才能创建成功。

有以下两种情况:

  • 属性是一个基本数据类型

type直接指定属性的类型,如apiserverstring类型:

apiserver:
  type: string
  • 属性是一个结构体类型

type值为object, properties中描述其它属性的类型,如imageRepo属性:

  imageRepo:
    type: object
    properties:
      apiserver:
        type: string
      controllerManager:
        type: string
      proxy:
        type: string
      scheduler:
        type: string

总结一下:

类型 说明
Operator CRD + Controller
CRD (Custom Resource Definition) 定义自定义资源的各种属性并向kubernetes中注册
CR (Custom Resource) 通过CRD定义好的真正可以在k8s中使用的资源,类似于pod,deployment这样的k8s中定义好的资源
Controller 监听CRD的CRUD事件并添加自定义业务逻辑,负责确保其追踪的资源对象的当前状态接近期望状态

如何开发?

了解了CRD的是什么,那如何来开发一个CRD呢?

从上文看起来CRD的定义文件这么长,似乎很复杂。不要怕,这些都可以用工具生成,不需要咱们手动编写的。正所谓工欲善其事,必先利其器。下面就来了解下CRD的开发利器: kubebuilder。有时间的也可以研究下kubebuilder的文档, 里面有详细的介绍。

使用kubebuilder构建CRD基本代码框架

环境安装

可以直接在机器上进行安装,或者使用构建好kubebuilder镜像运行一个容器在容器执行操作:推荐使用容器方式,方便很多。

机器直接安装

安装go环境

首先机器上得安装go环境,不会安装的可以看这个教程:Go语言环境安装。 如果因为墙的原因无法从go官网下载安装包,可以访问go语言中文网进行下载。

安装kubebuilder

接着需要在机器上安装kubebuilder, linux可以使用下面命令安装:

wget https://github.com/kubernetes-sigs/kubebuilder/releases/download/v2.3.1/kubebuilder_2.3.1_linux_amd64.tar.gz
tar -zxvf kubebuilder_2.3.1_linux_amd64.tar.gz
cp kubebuilder_2.3.1_linux_amd64/bin/kubebuilder /usr/local/bin/

其它版本和架构的机器安装方法一样,可以根据需要下载相应的安装包。

安装kustomize

kustomize是一个yaml渲染工具,kubebuilder依赖它进行yaml文件的渲染。

curl https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2Fv3.6.1/kustomize_v3.6.1_linux_amd64.tar.gz | tar -zxv -C /usr/local/bin/

容器中运行

拉取镜像 xwcheng/kubebuilder:2.3.1

docker pull xwcheng/kubebuilder:2.3.1

运行容器

docker run -d --name kubebuilder -v /go/src/github.com/cxwen/:/go/src/github.com/cxwen -e GOPATH=/go -e GOROOT=/usr/local/go xwcheng/kubebuilder:2.3.1 sh -c "while true; do sleep 1000000000; done"

可以将宿主机的目录挂载到容器中。

进入容器

docker exec -ti kubebuilder bash

构建代码框架

1、初始化代码框架

国内因为墙的关系,最好执行 export GOPROXY=https://goproxy.io

mkdir -p crd/
cd crd/
kubebuilder init --domain cxwen.com --license apache2 --owner "cxwen"
Writing scaffold for you to edit...
Get controller runtime:
$ go get sigs.k8s.io/controller-runtime@v0.5.0
go: finding sigs.k8s.io/controller-runtime v0.5.0
......
Update go.mod:
$ go mod tidy
go: downloading github.com/go-logr/zapr v0.1.0
......
Running make:
$ make
go: creating new go.mod: module tmp
go: finding sigs.k8s.io v0.2.5
......
/go/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
go fmt ./...
go vet ./...
go build -o bin/manager main.go
Next: define a resource with:
$ kubebuilder create api

生成完之后,会出现如下这些文件机目录:

[root@centos01 ~]# tree
.
├── bin
│   └── manager
├── config
│   ├── certmanager
│   │   ├── certificate.yaml
│   │   ├── kustomization.yaml
│   │   └── kustomizeconfig.yaml
│   ├── default
│   │   ├── kustomization.yaml
│   │   ├── manager_auth_proxy_patch.yaml
│   │   ├── manager_webhook_patch.yaml
│   │   └── webhookcainjection_patch.yaml
│   ├── manager
│   │   ├── kustomization.yaml
│   │   └── manager.yaml
│   ├── prometheus
│   │   ├── kustomization.yaml
│   │   └── monitor.yaml
│   ├── rbac
│   │   ├── auth_proxy_client_clusterrole.yaml
│   │   ├── auth_proxy_role_binding.yaml
│   │   ├── auth_proxy_role.yaml
│   │   ├── auth_proxy_service.yaml
│   │   ├── kustomization.yaml
│   │   ├── leader_election_role_binding.yaml
│   │   ├── leader_election_role.yaml
│   │   └── role_binding.yaml
│   └── webhook
│       ├── kustomization.yaml
│       ├── kustomizeconfig.yaml
│       └── service.yaml
├── Dockerfile
├── go.mod
├── go.sum
├── hack
│   └── boilerplate.go.txt
├── main.go
├── Makefile
└── PROJECT

9 directories, 30 files

2、创建CRD

[root@centos01 ~]# kubebuilder create api --group crd --version v1 --kind TestCrd
Create Resource [y/n]
y
Create Controller [y/n]
y
Writing scaffold for you to edit...
api/v1/testcrd_types.go
controllers/testcrd_controller.go
Running make:
$ make
/go/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
go fmt ./...
go vet ./...
go build -o bin/manager main.go

执行完后可以看到目录下出现api和controllers这两个新的目录。

[root@centos01 ~]# tree api/
api/
└── v1
    ├── groupversion_info.go
    ├── testcrd_types.go
    └── zz_generated.deepcopy.go

1 directory, 3 files

[root@centos01 ~]# tree controllers/
controllers/
├── suite_test.go
└── testcrd_controller.go

0 directories, 2 files

代码解析

结构体: CRD的血肉

api目录中存放的是Custom Resource的结构体。 如下所示,TestCrdSpec结构体中可以自定义yaml文件spec属性下需要的字段。TestCrdStatus结构体中可以自定义yaml文件status属性下需要的字段。

// TestCrdSpec defines the desired state of TestCrd
type TestCrdSpec struct {
    // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
    // Important: Run "make" to regenerate code after modifying this file

    // Foo is an example field of TestCrd. Edit TestCrd_types.go to remove/update
    Foo string `json:"foo,omitempty"`
}

// TestCrdStatus defines the observed state of TestCrd
type TestCrdStatus struct {
    // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
    // Important: Run "make" to regenerate code after modifying this file
}

// +kubebuilder:object:root=true

// TestCrd is the Schema for the testcrds API
type TestCrd struct {
    metav1.TypeMeta   `json:",inline"`
    metav1.ObjectMeta `json:"metadata,omitempty"`

    Spec   TestCrdSpec   `json:"spec,omitempty"`
    Status TestCrdStatus `json:"status,omitempty"`
}

spec和status有什么区别呢?

可以查看kubernetes的官方文档对象规约(Spec)与状态(Status)

每个 Kubernetes 对象包含两个嵌套的对象字段,它们负责管理对象的配置:对象 spec 和 对象 status 。 spec 是必需的,它描述了对象的 期望状态(Desired State) —— 希望对象所具有的特征。 status 描述了对象的 实际状态(Actual State) ,它是由 Kubernetes 系统提供和更新的。在任何时刻,Kubernetes 控制面一直努力地管理着对象的实际状态以与期望状态相匹配。

对于我们开发的CRD来说,可以像这样理解。

  • spec是预先定义的期望状态,也就是控制器 (controller) 里面可以获取到的预置信息,并根据这些信息进行处理调度以达到这个期望状态,并且spec里面的信息不能被控制器里面的代码更改的。

  • status里面的字段里面的信息是标志当前实际状态。比如,一个Custom Resource生命周期有三个状态:initializing、ready、teminating, 可以在status中加一个phase字段来表示;当这个CR刚创建时,控制器将phase字段值更新为initializin;CR初始化完成,健康检查等通过,已经可以正常提供服务了,控制器就可以将phase字段置为ready;当这个CR使命已经完成,进入结束阶段,控制器就将phase字段置为teminating, 然后再执行资源清理操作。当然,这些状态的转换,都是需要在控制器代码里来实现的。

Reconcile:CRD的大脑

如果说结构体是CRD的血肉,那么controller里面的Reconcile方法就是CRD的大脑,因为CRD的所有行为,都是通过这个方法来控制的,这个方法也是我们代码实现的关键所在。每一个CRD都会在controllers目录中生成一个以{CRD名称}_controller.go格式命名的代码文件, Reconcile方法即在这个文件中。

func (r *TestCrdReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
    _ = context.Background()
    _ = r.Log.WithValues("testcrd", req.NamespacedName)

    // your logic here

    return ctrl.Result{}, nil
}

下面是Reconcile实现的一个模板:

func (r *TestCrdReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
    var err error
    ctx := context.Background() // 获取context
    log := r.Log.WithValues("master", req.NamespacedName) // 获取日志对象

    log.V(1).Info("TestCrd reconcile triggering")
    
    // 从kubernetes中获取crd对象
    testCrd := crdv1.TestCrd{}
    if err = r.Get(ctx, req.NamespacedName, &testCrd); err != nil { 
        if IgnoreNotFound(err) != nil {
            log.Error(err, "unable to fetch testCrd")
            return ctrl.Result{}, err
        }

        return ctrl.Result{}, nil
    }

    testCrdFinalizer := "crd.cxw.com"
    // 通过DeletionTimestamp字段来判断是删除还是创建更新操作
    if testCrd.ObjectMeta.DeletionTimestamp.IsZero() {
        // 判断是否是创建
        if ! ContainsString(master.ObjectMeta.Finalizers, testCrdFinalizer) {
            testCrd.ObjectMeta.Finalizers = append(testCrd.ObjectMeta.Finalizers, testCrdFinalizer)
            
            // 你的创建处理代码
            
        } else {
            // 你的更新处理代码
        }
    } else {
        if ContainsString(master.ObjectMeta.Finalizers, masterFinalizer) {
            
            // 你的删除处理代码

            testCrd.ObjectMeta.Finalizers = RemoveString(testCrd.ObjectMeta.Finalizers, testCrdFinalizer)
            if err = r.Update(ctx, &testCrd); err != nil {
                return ctrl.Result{}, err
            }
        }
    }

    return ctrl.Result{}, nil
}
注释也是代码

可以直接在代码里通过格式化的注释来实现授权、添加额外打印字段功能。具体使用可以参考matrix

1、给控制器授权

在Reconcile方法前面添加, 例如:

// +kubebuilder:rbac:groups=crd.cxwen.com,resources=testcrds,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=crd.cxwen.com,resources=testcrds/status,verbs=get;update;patch

func (r *TestCrdReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
    _ = context.Background()
    _ = r.Log.WithValues("testcrd", req.NamespacedName)

    // your logic here

    return ctrl.Result{}, nil
}

2、添加额外的打印字段

在CRD结构体前面添加,例如:

// +kubebuilder:printcolumn:name="VERSION",type="string",JSONPath=".spec.version",description="version"
// +kubebuilder:printcolumn:name="REPLICAS",type="string",JSONPath=".spec.replicas",description="pod replicas"
// +kubebuilder:printcolumn:name="ETCD",type="string",JSONPath=".spec.etcdCluster",description="etcdcluster name"
// +kubebuilder:printcolumn:name="EXPOSETYPE",type="string",JSONPath=".spec.expose.method",description="expose type"
// +kubebuilder:printcolumn:name="EXPOSENODE",type="string",JSONPath=".spec.expose.node",description="expose node"
// +kubebuilder:printcolumn:name="EXPOSEPORT",type="string",JSONPath=".spec.expose.port",description="expose port"
// +kubebuilder:printcolumn:name="PHASE",type="string",JSONPath=".status.phase",description="phase"
// +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp"

// Master is the Schema for the masters API
type Master struct {
    metav1.TypeMeta   `json:",inline"`
    metav1.ObjectMeta `json:"metadata,omitempty"`

    Spec   MasterSpec   `json:"spec,omitempty"`
    Status MasterStatus `json:"status,omitempty"`
}
Finalizer

Finalizer的作用是基于kubernetes的处理机制。

当api接收到一个资源对象删除操作,Finalizer为空时,kubernetes会直接将这个资源对象删掉,删掉之后,etcd中就不存在改资源对象了,通过api也无法查询到了。

当Finalizer不为空时,kubernetes不会直接删除改资源对象,而是在对象ObjectMeta中添加DeletionTimestamp这么个字段,一直要等到Finalizer为空时才把资源对象删除。

所以,当我们的CRD在删除时有需要进行资源清理操作时,就可以在创建时添加上Finalizer,当检测到DeletionTimestamp不为空时,就知道该资源对象处于删除状态了,然后执行资源清理操作,最后移除Finalizer即可。

Finalizer的添加很简单,它的值可以是任何字符串。当然,为了起到一些标志作用,可以使用有意义的字符串。

OwnerReference

kubernetes GC在删除一个对象时,任何 ownerReference 是该对象的对象都会被清除。

下面是kubernetes官方文档中的描述。

某些 Kubernetes 对象是其它一些对象的所有者。例如,一个 ReplicaSet 是一组 Pod 的所有者。 具有所有者的对象被称为是所有者的 附属 。 每个附属对象具有一个指向其所属对象的 metadata.ownerReferences 字段。
有时,Kubernetes 会自动设置 ownerReference 的值。 例如,当创建一个 ReplicaSet 时,Kubernetes 自动设置 ReplicaSet 中每个 Pod 的 ownerReference 字段值。 在 Kubernetes 1.8 版本,Kubernetes 会自动为某些对象设置 ownerReference 的值,这些对象是由 ReplicationController、ReplicaSet、StatefulSet、DaemonSet、Deployment、Job 和 CronJob 所创建或管理。 也可以通过手动设置 ownerReference 的值,来指定所有者和附属之间的关系。

添加OwnerReference, 以deployment为例,在ObjectMeta下面添加OwnerReferences即可:

Deployment{
    TypeMeta: metav1.TypeMeta{
        APIVersion: "apps/v1",
        Kind:       "Deployment",
    },
    ObjectMeta: metav1.ObjectMeta{
        Name:      test,
        Namespace: test,
        OwnerReferences: []metav1.OwnerReference{
            *metav1.NewControllerRef(app, schema.GroupVersionKind{
                Group: v1.SchemeGroupVersion.Group,
                Version: v1.SchemeGroupVersion.Version,
                Kind: "TestCrd",
            }),
        },
    },
    ......
}

小结

进行CRD开发时

  • 使用注释的方法来为controller配置权限,以及添加额外打印字段
  • 使用Finalizer来做资源的清理
  • 使用OwnerReference进行对象之间依赖关系的管理

其它代码开发技巧可以研究下kubebuilder的文档

源码小窥

kubebuilder生成的源码架构,主要是基于controller-runtime这个go代码库。这个代码库对controller操作做了很好的封装,基于它开发CRD非常方便。

main.go

var (
    scheme   = runtime.NewScheme()
    setupLog = ctrl.Log.WithName("setup")
)

// 注册scheme
func init() {
    _ = clientgoscheme.AddToScheme(scheme)

    _ = crdv1.AddToScheme(scheme)
    // +kubebuilder:scaffold:scheme
}

func main() {
    ......
    
    // 初始化manager
    mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
        Scheme:             scheme,
        MetricsBindAddress: metricsAddr,
        Port:               9443,
        LeaderElection:     enableLeaderElection,
        LeaderElectionID:   "6ed5364d.cxwen.com",
    })
    if err != nil {
        setupLog.Error(err, "unable to start manager")
        os.Exit(1)
    }

    // 初始化controller
    if err = (&controllers.TestCrdReconciler{
        Client: mgr.GetClient(),
        Log:    ctrl.Log.WithName("controllers").WithName("TestCrd"),
        Scheme: mgr.GetScheme(),
    }).SetupWithManager(mgr); err != nil {
        setupLog.Error(err, "unable to create controller", "controller", "TestCrd")
        os.Exit(1)
    }
    // +kubebuilder:scaffold:builder

    // 启动manager
    setupLog.Info("starting manager")
    if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
        setupLog.Error(err, "problem running manager")
        os.Exit(1)
    }
}

main.go中主要做了下面两件事:

1、注册scheme

在controller中,如果需要使用manager提供的client操作某种类型的资源,需要将资源类型注册到scheme中。从代码中的init函数可以看到,crdv1中的类型被注册到了scheme中。

2、创建并启动manager

这个manager就是管理controller的manager, 一个manager中可以管理多个controller。

创建manager过程中,创建了两个很重要的对象:cache和client。这两个对象是manager中所有controller共享的。

下面是创建Manager的函数:

// New returns a new Manager for creating Controllers.
func New(config *rest.Config, options Options) (Manager, error) {
    // Initialize a rest.config if none was specified
    if config == nil {
        return nil, fmt.Errorf("must specify Config")
    }

    // Set default values for options fields
    options = setOptionsDefaults(options)

    // Create the mapper provider
    mapper, err := options.MapperProvider(config)
    if err != nil {
        log.Error(err, "Failed to get API Group-Resources")
        return nil, err
    }

    // Create the cache for the cached read client and registering informers
    cache, err := options.NewCache(config, cache.Options{Scheme: options.Scheme, Mapper: mapper, Resync: options.SyncPeriod, Namespace: options.Namespace})
    if err != nil {
        return nil, err
    }

    apiReader, err := client.New(config, client.Options{Scheme: options.Scheme, Mapper: mapper})
    if err != nil {
        return nil, err
    }

    writeObj, err := options.NewClient(cache, config, client.Options{Scheme: options.Scheme, Mapper: mapper})
    if err != nil {
        return nil, err
    }
    // Create the recorder provider to inject event recorders for the components.
    // TODO(directxman12): the log for the event provider should have a context (name, tags, etc) specific
    // to the particular controller that it's being injected into, rather than a generic one like is here.
    recorderProvider, err := options.newRecorderProvider(config, options.Scheme, log.WithName("events"), options.EventBroadcaster)
    if err != nil {
        return nil, err
    }

    // Create the resource lock to enable leader election)
    resourceLock, err := options.newResourceLock(config, recorderProvider, leaderelection.Options{
        LeaderElection:          options.LeaderElection,
        LeaderElectionID:        options.LeaderElectionID,
        LeaderElectionNamespace: options.LeaderElectionNamespace,
    })
    if err != nil {
        return nil, err
    }

    // Create the metrics listener. This will throw an error if the metrics bind
    // address is invalid or already in use.
    metricsListener, err := options.newMetricsListener(options.MetricsBindAddress)
    if err != nil {
        return nil, err
    }

    // Create health probes listener. This will throw an error if the bind
    // address is invalid or already in use.
    healthProbeListener, err := options.newHealthProbeListener(options.HealthProbeBindAddress)
    if err != nil {
        return nil, err
    }

    stop := make(chan struct{})

    return &controllerManager{
        config:                config,
        scheme:                options.Scheme,
        cache:                 cache,
        fieldIndexes:          cache,
        client:                writeObj,
        apiReader:             apiReader,
        recorderProvider:      recorderProvider,
        resourceLock:          resourceLock,
        mapper:                mapper,
        metricsListener:       metricsListener,
        internalStop:          stop,
        internalStopper:       stop,
        port:                  options.Port,
        host:                  options.Host,
        certDir:               options.CertDir,
        leaseDuration:         *options.LeaseDuration,
        renewDeadline:         *options.RenewDeadline,
        retryPeriod:           *options.RetryPeriod,
        healthProbeListener:   healthProbeListener,
        readinessEndpointName: options.ReadinessEndpointName,
        livenessEndpointName:  options.LivenessEndpointName,
    }, nil
}
  • cache

cache用到了kubernetes中一个重要的工具包:informer。

cache主要就是创建了InformersMap,scheme里面的每个GVK (GroupVersionKind结构体,包含Group、Version、Kind三个字段,可以唯一确定一个资源) 都创建了对应的 Informer,通过 informersByGVK这个map来存放GVK和Informer的映射关系,每个 Informer会根据ListWatch 函数对对应的GVK进行List和Watch。我们为controller开发的Reconcile方法最终都会注册到informer的handler中,这样利用informer就可以达到监控资源的事件并触发Reconcile目的。

// newSpecificInformersMap returns a new specificInformersMap (like
// the generical InformersMap, except that it doesn't implement WaitForCacheSync).
func newSpecificInformersMap(config *rest.Config,
    scheme *runtime.Scheme,
    mapper meta.RESTMapper,
    resync time.Duration,
    namespace string,
    createListWatcher createListWatcherFunc) *specificInformersMap {
    ip := &specificInformersMap{
        config:            config,
        Scheme:            scheme,
        mapper:            mapper,
        informersByGVK:    make(map[schema.GroupVersionKind]*MapEntry), // schema GVK和informer映射
        codecs:            serializer.NewCodecFactory(scheme),
        paramCodec:        runtime.NewParameterCodec(scheme),
        resync:            resync,
        startWait:         make(chan struct{}),
        createListWatcher: createListWatcher,
        namespace:         namespace,
    }
    return ip
}

MapEntry中包含最终创建的informer对象。

// MapEntry contains the cached data for an Informer
type MapEntry struct {
    // Informer is the cached informer
    Informer cache.SharedIndexInformer

    // CacheReader wraps Informer and implements the CacheReader interface for a single type
    Reader CacheReader
}
  • client

从下面的代码可以看出,读操作使用上面创建的 cache,写操作使用client直连kubernetes。

// defaultNewClient creates the default caching client
func defaultNewClient(cache cache.Cache, config *rest.Config, options client.Options) (client.Client, error) {
    // Create the Client for Write operations.
    c, err := client.New(config, options)
    if err != nil {
        return nil, err
    }

    return &client.DelegatingClient{
        Reader: &client.DelegatingReader{
            CacheReader:  cache,
            ClientReader: c,
        },
        Writer:       c,
        StatusClient: c,
    }, nil
}

CRD Reconcile执行

在main.go中, 通过下面的代码把Manager中的client、scheme以及日志对象传递给相应的CRD对象。

if err = (&controllers.TestCrdReconciler{
    Client: mgr.GetClient(),
    Log:    ctrl.Log.WithName("controllers").WithName("TestCrd"),
    Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
    setupLog.Error(err, "unable to create controller", "controller", "TestCrd")
    os.Exit(1)
}
// TestCrdReconciler reconciles a TestCrd object
type TestCrdReconciler struct {
    client.Client
    Log    logr.Logger
    Scheme *runtime.Scheme
}

然后在Reconcile方法中就可以直接使用client来进行CURD操作。

总结

kubernetes的强大之处之一就是支持CRD对API进行扩展,当今很多项目都大量使用到CRD,像calico、istio以及kubevirt等等。如果有在kubernetes上层进行二次开发需求,可以优先考虑CRD,这是一种非常优雅的扩展方式,也是kubernetes生态的发展趋势。除此之外,有kubebuilder这个CRD开发利器,也能让我们的开发工作事半功倍。

资源链接

matrix github:https://github.com/cxwen/matrix

kubebuilder github: https://github.com/kubernetes-sigs/kubebuilder
kubebuilder的文档: https://book.kubebuilder.io/

Go语言环境安装: https://www.runoob.com/go/go-environment.html

go语言中文网go安装包下载: https://studygolang.com/dl

kustomize github: https://github.com/kubernetes-sigs/kustomize

kubernetes的官方文档对象规约(Spec)与状态(Status): https://kubernetes.io/zh/docs/concepts/overview/working-with-objects/kubernetes-objects/#%E5%AF%B9%E8%B1%A1%E8%A7%84%E7%BA%A6-spec-%E4%B8%8E%E7%8A%B6%E6%80%81-status

controller-runtime github: https://github.com/kubernetes-sigs/controller-runtime