Kubernetes Informer 源码解析与深度使用 [3/4]: 实现自定义资源 (CRD) Informer
07 May 21 17:25 +0000

本文分 4 部分,分别是:

(1). cache 包源码解析与 Informer 的使用;

(2). informers 包源码解析与 SharedInformerFactory 的使用,以及 Informer 在实际使用中的最佳实践;

(3). 实现自定义资源 (CRD) Informer;

(4). dynamic 包源码解析与 DynamicSharedInformerFactory 的使用;

这里是第 3 部分。

本文使用的所有代码都可以在 articles/archive/dive-into-kubernetes-informer at main · wbsnail/articles · GitHub 找到。

自定义资源 (Custom Resource Definition, CRD)

虽然 Kubernetes 内置的组件和资源类型覆盖了很广泛的使用场景,但用户的需求千奇百怪,作为基础架构,Kubernetes 提供了多种方式扩展 Kubernetes。自定义资源就是扩展 Kubernetes 的重要方式之一。值得一提的是,自定义资源在 Kubernetes 中的地位与内置资源类型没有区别:所有资源类型在 Kubernetes 中都是平等的。

关于自定义资源的定义和资源增删改查操作不在本文的范畴内,官方文档就有详细的说明:Extend the Kubernetes API with CustomResourceDefinitions | Kubernetes

SharedInformerFactory

前一部分中,我给大家分析了 kubernetes/client-go 仓库中的 informers 包,并介绍 SharedInformerFactory 的正确使用方式。但这个包只涉及 Kubernetes 内置资源类型,而我们肯定也需要使用程序维护自定义资源的状态,对于自定义资源的控制器又该如何实现呢?接下来我就给大家介绍:自定义资源 Informer 的实现方式。

👮‍♂️ 如果你没有看过前一部分,建议先看完它。

代码生成

代码生成是各大编程语言中不少见的一种模式,我说的代码生成不是编译生成机器码的过程,而是生成相同语言的代码。为什么需要生成代码?原因比如:

(1). 自动生成一些模式相同的代码以提高效率;

(2). 方便统一代码规范;

(3). 允许使用简洁的方式描述更加复杂的逻辑。

code-generator 是 Kubernetes 的代码生成套件,提供了许多代码生成命令,用于生成自定义资源相关的代码,包括我们关心的 Informer。

实际上所有 Kubernetes 内置资源的 Lister, Informer, SharedInformerFactory,都是使用 code-generator 生成的。还是那句话:所有资源类型在 Kubernetes 中都是平等的。

使用 code-generator 生成自定义资源 Informer

code-generator 使用方式介绍

code-generator 就是一组单纯的命令行工具,用于生成自定义资源相关的代码,命令列表:code-generator/cmd at master · kubernetes/code-generator · GitHub

比如说 deepcopy-gen,用于给类型生成深度拷贝方法,这么做的原因是在 Kubernetes 1.8 之后,自定义资源类型所实现的 runtime.Object 添加了 DeepCopyObject 方法,这个方法不像 runtime.Object 的其他方法,无法抽象成公共方法,因为它返回的是相应类型的对象:

type Object interface {
	// ...

	DeepCopyObject() Object
}

也就意味着,对每一个自定义类型,都需要定义一个 DeepCopyObject 方法,比如类型 T:

type T struct {}

func (t *T) DeepCopy() *T {
    // ...
}

func (t *T) DeepCopyObject() runtime.Object {
    return t.DeepCopy()
}

🤔 我看上面接口定义返回的是 runtime.Object,不是 *T 啊,为什么要有 DeepCopy 方法?有没有可能我抽象出来一个方法返回 runtime.Object,是不是就可以复用它了,是不是就不需要 deepcopy-gen 了?你可以自己思考一下。

典型的 DeepCopyObject 最终实现逻辑像这样:

func (in *T) DeepCopyInto(out *T) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ListMeta.DeepCopyInto(&out.ListMeta)
	if in.Items != nil {
		in, out := &in.Items, &out.Items
		*out = make([]Service, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
	return
}

func (in *T) DeepCopy() *T {
	if in == nil {
		return nil
	}
	out := new(T)
	in.DeepCopyInto(out)
	return out
}

func (in *T) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

就是遍历字段,对每个字段递归进行深度拷贝,它是符合一定模式的,因此我们可以编写程序自动生成这个方法,deepcopy-gen 生成的就是这部分代码。

code-generator 中的所有命令都基于另一个包 kubernetes/gengo 实现,因此它们共享一部分参数。比较值得注意的有:

  • InputDirs (--input-dirs): 需要解析类型的包 import 目录
  • OutputBase (--output-base): 输出文件的基础磁盘目录,默认是 "$GOPATH/src";
  • OutputPackagePath (--output-package): 输出的包 import 路径
  • GoHeaderFilePath (--go-header-file): 版权信息头文件磁盘路径

在共享参数外,每个命令还有一些自己的参数。

本文使用的所有代码都可以在 articles/archive/dive-into-kubernetes-informer at main · wbsnail/articles · GitHub 找到。

以下操作都在 wbsnail/articles 仓库的 archive/dive-into-kubernetes-informer/11-crd-informer 目录下进行。

因为 code-generator 命令的设计问题,代码需要以 Golang 默认的 src 目录结构存放,如果使用默认的 "$GOPATH/src",以上代码需要克隆到 $GOPATH/src/github.com/wbsnail/articles,如果使用 go module,也要求目录结构为 /somewhere/github.com/wbsnail/articles",同时设置 --output-base=/somewhere,不然生成的代码文件位置会有问题。如果不想考虑这种问题,建议就把仓库克隆到 "$GOPATH/src" 好了。

code-generator 命令需要自己使用 Golang 安装,Github 上没有 release 二进制文件,具体做法就是克隆 code-generator 到本地,然后 go install ./cmd/deepcopy-gen。

实际上 code-generator 同步自 kubernetes/staging/src/k8s.io/code-generator at master · kubernetes/kubernetes · GitHub,修改都发生在后者,然后同步到 code-generator 仓库。所以如果你本地已经有 kubernetes 仓库,也可以 go install ./staging/src/k8s.io/code-generator/cmd/deepcopy-gen。

下面我们以 deepcopy-gen 为例看一看 code-generator 命令的使用方式:

创建文件 ./api/stable.wbsnail.com/v1/doc.go:

// +k8s.deepcopy-gen=package

package v1

以及 ./api/stable.wbsnail.com/v1/types.go:

package v1

type Rabbit struct {
	Color string `json:"color,omitempty"`
}

和版权信息头文件 ./boilerplate.go.txt:

/*
Copyright wbsnail.com.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

类型文件所在的磁盘目录是 ./api/{group}/{version},这并不是强制要求,但我们最好保持和 Kubernetes 官方一致,包括 "api" 和 "types.go" 这两个名字。但 "doc.go" 这个文件名是 code-generator 强制要求的,其中声明一些这个包的代码生成参数。

"// +k8s:deepcopy-gen=package" 是一句注释,但熟悉 Golang 的同学都应该知道在 Golang 中注释常常被用来做一些有用的事情,比如 go build 使用的 "// +build",go embed 使用的 "//go:embed" 等等。"// +k8s:deepcopy-gen=package" 是告诉 deepcopy-gen,生成该包中所有类型的深度拷贝方法。

types.go 不用多说,就是我们定义类型的文件。

boilerplate.go.txt 是版权信息头文件,会被加入到每一个生成的文件开头。

执行:

deepcopy-gen \
  --input-dirs github.com/wbsnail/articles/archive/dive-into-kubernetes-informer/11-crd-informer/api/stable.wbsnail.com/v1 \
  --output-file-base zz_generated.deepcopy \
  --go-header-file ./boilerplate.go.txt

其中 --input-dirs 声明了我们需要解析类型的包 import 目录。--output-file-base 是生成的文件名,默认是 deepcopy.generated,我们使用和 Kubernetes 一致的 zz_generated.deepcopy。--go-header-file 声明版权信息头文件磁盘路径。

再强调一遍:(1). 所有操作都在 wbsnail/articles 仓库的 archive/dive-into-kubernetes-informer/11-crd-informer 目录下进行; (2). 因为 code-generator 命令的设计问题,代码需要以 Golang 默认的 src 目录结构存放,比如 "$GOPATH/src/github.com/wbsnail/articles"。

在执行后会生成 1 个文件:

./api/stable.wbsnail.com/v1/zz_generated.deepcopy.go:
// +build !ignore_autogenerated

/*
Copyright wbsnail.com.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

// Code generated by deepcopy-gen. DO NOT EDIT.

package v1

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Rabbit) DeepCopyInto(out *Rabbit) {
	*out = *in
	return
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Rabbit.
func (in *Rabbit) DeepCopy() *Rabbit {
	if in == nil {
		return nil
	}
	out := new(Rabbit)
	in.DeepCopyInto(out)
	return out
}

生成的文件有 4 部分内容:

(1). "// +build !ignore_autogenerated",声明这个文件是自动生成的;

(2). 版权信息;

(3). "// Code generated by deepcopy-gen. DO NOT EDIT.",自动生成文件注释,code-generator 有参数可以调整这段注释,但大部分情况下不用管它;

(4). 生成的深度拷贝方法。

🤖 你可能注意到生成的代码中没有 DeepCopyObject 方法,你知道为什么吗?

代码生成原理

上面说了 code-generator 实际上底层是 gengo 包,这个包的底层又是 go/ast 包和 go/parser 包,它们的作用是解析 Golang 代码,生成对应的抽象语法树 (AST)。通过这两个包 gengo 获取到了我们指定的包的抽象语法树,从而得以遍历其中所有的类型。至于生成的代码片断,那就是简单(也许并不简单)的模版渲染。

gengo 包暴露的核心方法是 *GeneratorArgs.Execute:

func (g *GeneratorArgs) Execute(
	nameSystems namer.NameSystems,
	defaultSystem string,
	pkgs func(*generator.Context, *GeneratorArgs) generator.Packages,
) error {}

它接收参数中最重要的一个是 pkgs,它允许用户根据 *generator.Context 和 *GeneratorArgs 构建要生成的包。pkgs 返回的 generator.Packages 中又有一个很重要的方法 Generators:

type Generator interface {
	// 有很多其他方法,包括生成 Vars, Consts, Imports 等等

	// GenerateType should emit the code for a particular type.
	GenerateType(*Context, *types.Type, io.Writer) error
}


// Package contains the contract for generating a package.
type Package interface {
	// ...

	// Generators returns the list of generators for this package. It is
	// allowed for more than one generator to write to the same file.
	// A Context is passed in case the list of generators depends on the
	// input types.
	Generators(*Context) []Generator
}

使用 gengo 包的关键就在于定义好生成器 (Generators),每个生成器根据包信息和上下文生成对应的一段代码,最终由 gengo 组成 .go 文件并统一输出到磁盘文件中。

随便挑一小段 deepcopy-gen 中实现的 Generator 代码看看:

	sw := generator.NewSnippetWriter(w, c, "$", "$")
	args := argsFromType(t)

	if deepCopyIntoMethodOrDie(t) == nil {
		sw.Do("// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.\n", args)
		if isReference(t) {
			sw.Do("func (in $.type|raw$) DeepCopyInto(out *$.type|raw$) {\n", args)
			sw.Do("{in:=&in\n", nil)
		} else {
			sw.Do("func (in *$.type|raw$) DeepCopyInto(out *$.type|raw$) {\n", args)
		}
		if deepCopyMethodOrDie(t) != nil {
			if t.Methods["DeepCopy"].Signature.Receiver.Kind == types.Pointer {
				sw.Do("clone := in.DeepCopy()\n", nil)
				sw.Do("*out = *clone\n", nil)
			} else {
				sw.Do("*out = in.DeepCopy()\n", nil)
			}
			sw.Do("return\n", nil)
		} else {
			g.generateFor(t, sw)
			sw.Do("return\n", nil)
		}
		if isReference(t) {
			sw.Do("}\n", nil)
		}
		sw.Do("}\n\n", nil)
	}

可以一窥 DeepCopy 方法生成的过程。

关于更多代码如何生成的细节,这里就不展开了,感兴趣自己去阅读 code-generatorgengo 源代码。接下来我们还是专注于 code-generator 各命令的使用。

自定义资源: Rabbit

在进行接下来的操作前,我们先定义一个完整的,符合我们通常印象的自定义资源:

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: rabbits.stable.wbsnail.com
spec:
  group: stable.wbsnail.com
  versions:
  - name: v1
    served: true
    storage: true
    schema:
      openAPIV3Schema:
        type: object
        properties:
          spec:
            type: object
            properties:
              color:
                type: string
    additionalPrinterColumns:
    - name: Color
      type: string
      description: Color
      jsonPath: .spec.color
    - name: Age
      type: date
      description: Age
      jsonPath: .metadata.creationTimestamp
  scope: Namespaced
  names:
    plural: rabbits
    singular: rabbit
    kind: Rabbit

把它应用到集群里,然后创建两个示例资源:

apiVersion: "stable.wbsnail.com/v1"
kind: Rabbit
metadata:
  name: judy
  namespace: tmp
spec:
  color: white
---
apiVersion: "stable.wbsnail.com/v1"
kind: Rabbit
metadata:
  name: bugs
  namespace: tmp
spec:
  color: gray

尝试 GET 一下 rabbits 资源:

kubectl get rabbits -n tmp

输出应该类似于:

NAME   COLOR   AGE
bugs   gray    1h
judy   white   1h

你可以先和兔子们玩玩,尝试一下增删改查操作,玩好以后我们再进入下一步。

调整类型文件

前面我们创建了 doc.go, types.go, boilerplate.go.txt 3 个文件,因为我们调整了 Rabbit 的结构,所以要修改 types.go 文件内容如下:

package v1

import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

// +genclient
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

type Rabbit struct {
	metav1.TypeMeta `json:",inline"`
	// +optional
	metav1.ObjectMeta `json:"metadata,omitempty"`

	Spec RabbitSpec `json:"spec,omitempty"`
}

type RabbitSpec struct {
	Color string `json:"color,omitempty"`
}

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

type RabbitList struct {
	metav1.TypeMeta `json:",inline"`
	// +optional
	metav1.ListMeta `son:"metadata,omitempty"`

	Items []Rabbit `json:"items"`
}

我们找找修改了哪些地方:

(1). "// +genclient" 声明为接下来一个类型(从上到下,最接近这条注释的一个,也就是 Rabbit)生成 Clientset;

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

(2). 以上这行注释声明:生成 DeepCopyX 方法,X 就是后面指定的 interface;

(3). 嵌入了 metav1.TypeMeta 和 metav1.ObjectMeta 类型,这是为了实现 runtime.Object 的其他方法,同时提供 Kubernetes 资源默认的许多字段,比如说 Name, Namespace, Labels, Annotations 等等;

(4). 字段定义被移到 RabbitSpec 中,这是根据我们的定义来的,有的资源,比如 ConfigMap,就没有 Spec,而是 Data;

(5). 多了一个 RabbitList 资源,这是生成 Lister 需要的资源列表类型。

然后还要添加一个文件 ./api/stable.wbsnail.com/v1/register.go:

package v1

import (
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
)

// SchemeGroupVersion is group version used to register these objects
var SchemeGroupVersion = schema.GroupVersion{Group: "stable.wbsnail.com", Version: "v1"}

// Kind takes an unqualified kind and returns back a Group qualified GroupKind
func Kind(kind string) schema.GroupKind {
	return SchemeGroupVersion.WithKind(kind).GroupKind()
}

// Resource takes an unqualified resource and returns a Group qualified GroupResource
func Resource(resource string) schema.GroupResource {
	return SchemeGroupVersion.WithResource(resource).GroupResource()
}

var (
	// SchemeBuilder initializes a scheme builder
	SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
	// AddToScheme is a global function that registers this API group & version to a scheme
	AddToScheme = SchemeBuilder.AddToScheme
)

// Adds the list of known types to Scheme.
func addKnownTypes(scheme *runtime.Scheme) error {
	scheme.AddKnownTypes(SchemeGroupVersion,
		&Rabbit{},
		&RabbitList{},
	)
	metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
	return nil
}

这个文件定义的是类型的注册方法,模版来自 Kubernetes 官方提供的自定义资源 Controller 示例项目 sample-controller。这当中的方法都是在生成 Clientset 的时候有用,大致是建立起从 Golang 类型到 Kubernetes 资源的互相映射关系,从而使接口得到的数据得以转换 (json unmarshal) 为相应的 Golang 类型。具体的细节我没有研究得很深入,感兴趣阅读源码的话可以从 apimachinery 仓库和后面生成的 Clientset 代码入手。

generate-groups.sh

看到这里也许你会想,上面介绍了 deepcopy-gen 的使用,接下来该是 client-gen, lister-gen 和 informer-gen 了吧。话是这么说没错,但是 code-generator 已经很贴心地为我们准备了一个脚本,一键生成深度拷贝方法 (deepcopy-gen),Clientset (client-gen),Lister (lister-gen) 和 Informer (informer-gen),那就是在 code-generator 仓库底下的 generate-groups.sh 脚本文件。

/path/to/code-generator/generate-groups.sh \
  all \
  github.com/wbsnail/articles/archive/dive-into-kubernetes-informer/11-crd-informer/client \
  github.com/wbsnail/articles/archive/dive-into-kubernetes-informer/11-crd-informer/api \
  stable.wbsnail.com:v1 \
  --go-header-file ./boilerplate.go.txt

第 1, 2, 3, 4 个参数分别对应生成器,输出的包,输入的包和资源组版本。当生成器输入 all 时,会调用 4 个生成器:deepcopy-gen, client-gen, lister-gen 和 informer-gen。

执行成功后我们就生成了自定义资源 Rabbit 的 Informer 代码(包括 Clientset 和 Lister 在内)。

生成的文件太多,我就不一一列出来了,代码在这里 articles/archive/dive-into-kubernetes-informer/11-crd-informer at main · wbsnail/articles · GitHub。对生成的文件我也不一一说明了,因为和 deepcopy-gen 一样,它们都是以和 Kubernetes 内置资源类型相同的方式生成出来的,所以结构上,使用上,甚至是注释,都和 client-go 仓库代码有很大程度相同。

比如我们要使用它:

package main

import (
	"fmt"
	"github.com/spongeprojects/magicconch"
	v1 "github.com/wbsnail/articles/archive/dive-into-kubernetes-informer/11-crd-informer/api/stable.wbsnail.com/v1"
	"github.com/wbsnail/articles/archive/dive-into-kubernetes-informer/11-crd-informer/client/clientset/versioned"
	"github.com/wbsnail/articles/archive/dive-into-kubernetes-informer/11-crd-informer/client/informers/externalversions"
	"k8s.io/client-go/tools/cache"
	"k8s.io/client-go/tools/clientcmd"
	"os"
)

func main() {
	kubeconfig := os.Getenv("KUBECONFIG")
	config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
	magicconch.Must(err)

	clientset, err := versioned.NewForConfig(config)
	magicconch.Must(err)

	informerFactory := externalversions.NewSharedInformerFactory(clientset, 0)
	rabbitInformer := informerFactory.Stable().V1().Rabbits().Informer()
	rabbitInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
		AddFunc: func(obj interface{}) {
			rabbit, ok := obj.(*v1.Rabbit)
			if !ok {
				return
			}
			fmt.Printf("A rabbit is created: %s\n", rabbit.Name)
		},
		UpdateFunc: func(oldObj, newObj interface{}) {
			newRabbit, ok := oldObj.(*v1.Rabbit)
			if !ok {
				return
			}
			fmt.Printf("A rabbit is updated: %s\n", newRabbit.Name)
		},
		DeleteFunc: func(obj interface{}) {
			rabbit, ok := obj.(*v1.Rabbit)
			if !ok {
				return
			}
			fmt.Printf("A rabbit is deleted: %s\n", rabbit.Name)
		},
	})

	stopCh := make(chan struct{})
	defer close(stopCh)

	fmt.Println("Start syncing....")

	go informerFactory.Start(stopCh)

	<-stopCh
}

是不是和前一部分介绍的 informers 包完全相同?

以上代码的输出类似于:

Start syncing....
A rabbit is created: judy
A rabbit is created: bugs
A rabbit is updated: judy
A rabbit is updated: bugs

自定义资源的控制器制作完成 🎉 🎉

总结

在以上的内容中,我给大家介绍了自定义资源 Informer 的实现方式,顺便介绍了 code-generator 提供的其他一些相关内容。

接下来,在下一部分中,我给大家介绍 Informer 的一种高级使用方式:动态 Informer。

下一部分:dynamic 包源码解析与 DynamicSharedInformerFactory 的使用

参考资料

Bitnami Engineering: A deep dive into Kubernetes controllers

Bitnami Engineering: Kubewatch, an example of Kubernetes custom controller

Dynamic Kubernetes Informers | FireHydrant

client-go/main.go at master · kubernetes/client-go · GitHub

GitHub - kubernetes/sample-controller: Repository for sample controller. Complements sample-apiserver

Kubernetes Deep Dive: Code Generation for CustomResources

How to generate client codes for Kubernetes Custom Resource Definitions (CRD) | by Roger Liang | ITNEXT


Loading comments...