Kubernetes CustomResource 代码生成

Feb 3, 2021 19:15 · 2260 words · 5 minute read Kubernetes

CustomResourceDefinitions(CRD) 在 Kubernetes 1.7 版作为 alpha 引入并在 1.8 版中被提升到了 beta(目前 1.20 版又变成了 apiextensions.k8s.io/v1),在某些叫做 operator 的自定义控制器中尤为常见。

为啥要代码生成?

client-go 要求 runtime.Object 类型(golang 编写的自定义资源需要实现 runtime.Object 接口)必须要有 DeepCopy 方法。这就要用到 k8s.io/code-generator 项目中的 deepcopy-gen 生成器了。

除了 deepcopy-gen 还有其他大概率会用到的代码生成器:

  • deepcopy-gen 给每个 T 类型创建一个 func (t* T) DeepCopy() *T 方法
  • client-gen 给自定义资源 API 组创建 Kubernetes 的客户端
  • informer-gen 给自定义资源创建 informer,这是一种基于事件的接口用来及时反馈数据库中自定义资源的变化。
  • lister-gen 给自定义资源创建 lister,为 GET 和 LIST 请求提供只读的缓存层。

下面的两种是构建控制器的基础(又叫做 operator)。这四种代码生成器提供了打造全功能的控制器的能力,和 Kubernetes 上游一样。

k8s.io/code-generator 中还有其他代码生成器。如果你想要构建自己的 aggregated API server,得处理内部类型,可以用 conversion-gen 转换内外部类型的方法;defaulter-gen 负责给某些字段设置默认值。

在你的项目中使用代码生成器

Kubernetes 代码生成器都是 k8s.io/gengo 的上层实现,它们共享一些通用的命令行 flag。所有代码生成器都通过 --input-dirs 获取输入,然后逐一检查输入的类型再输出代码。

  • deepcopy-gen 在输入路径下(--output-file-base "zz_generated.deepcopy" 定义文件名)生成代码
  • client-geninformer-genlister-gen 们也会向一个或多个路径输出(使用 --output-package flag),通常在 pkg/client 路径下生成

看上去要先搞定一堆命令行参数的用法,但是 k8s.io/code-generator 提供一个叫做 generator-group.sh 的 shell 脚本封装了对代码生成器的调用。你要做的就是在项目里创建一个 hack/update-codegen.sh 脚本并写上几行:

vendor/k8s.io/code-generator/generate-groups.sh all \
github.com/openshift-evangelist/crd-code-generation/pkg/client \
github.com/openshift-evangelist/crd-code-generation/pkg/apis \
example.com:v1

所有的 API 都在 pkg/apis 路径下,clientset、informer、lister 都在 pkg/client 路径下被创建出来。也就是说,pkg/client 完全是生成出来的,types.go 和 zz_generated.deepcopy.go 文件包含了自定义资源的 golang 类型,只要执行:

$ hack/update-codegen.sh

通常还要弄个 hack/verify-codegen.sh 脚本,只要存在生成的文件不是最新的,就会以非零的返回码退出。这在 CI 中很有用:如果开发者不小心修改了文件,或者文件落后了,CI 都能看到并汇报。

使用标签(Tag)控制生成代码

除了通过命令行 flag 来控制代码生成器的行为(尤其是要处理的包),更多的属性还要通过 golang 代码中的注释标签来控制。分为两种标签:

  • doc.go 中 package 行上面的全局标签
  • 想要处理的类型定义上面的局部标签

标签都遵循 // +tag-name 或者 // +tag-name=value 这种格式,并且都写成 golang 代码注释。标签的位置至关重要,有些标签必须放在类型正上方的注释里(全局标签的话是 package 行),其他标签必须与类型/ package 分开,中间至少空上一行。

全局标签

全局标签在包的 doc.go 文件里。比如 pkg/apis/<apigroup>/<version>/doc.go:

// +k8s:deepcopy-gen=package,register
// Package v1 is the v1 version of the API.
// +groupName=example.com
package v1

deepcopy-gen 会为这个包中的每个类型都创建 deepcopy 方法。如果有些类型不需要 deepcopy 方法,可以打这个局部标签 // +k8s:deepcopy-gen=false 来手动取消;如果不要整包范围的 deepcopy,你就要选择性地给想要的类型打上 // +k8s:deepcopy-gen=true

注意:上述例子中的 register 关键字将 deepcopy 方法注册进 scheme。在 Kubernetes 1.9 之后将被废除,因为 scheme 将不再负责对 runtime.Objects 做 deepcopy,而是调用 yourobject.DeepCopy()yourobject.DeepCopyObject()。在 1.8 之后的项目就应该这么做,更快更不容易出错。

最后 // +groupName=example.com 标签定义了完整的 API 组名称。要是弄错了,client-gen 会生成错误的代码。这个标签必须写在 package 上方

局部标签

局部标签要写在 API 类型上方。下面给个 types.go 自定义资源的例子:

// +genclient
// +genclient:noStatus
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// Database describes a database.
type Database struct {
    metav1.TypeMeta `json:",inline"`
    metav1.ObjectMeta `json:"metadata,omitempty"`

    Spec DatabaseSpec `json:"spec"`
}


// DatabaseSpec is the spec for a Foo resource
type DatabaseSpec struct {
    User string `json:"user"`
    Password string `json:"password"`
    Encoding string `json:"encoding,omitempty"`
}

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// DatabaseList is a list of Database resources
type DatabaseList struct {
    metav1.TypeMeta `json:",inline"`
    metav1.ListMeta `json:"metadata"`

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

注意这里我们默认为所有类型都启用了 deepcopy,这些类型也确实都需要 deepcopy,因此我们没必要管它了,只要在 doc.go 中搞个就完事。

runtime.Object and DeepCopyObject

这里有个特殊的 deepcopy 标签要说下:

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

在生成 DeepCopy 的时候,实现 Kubernetes 提供的 runtime.Object 接口。否则在 Kubernetes 1.8 中,会编译不过,因为你的代码中没有定义 DeepCopyObject() runtime.Object。1.8 中 runtime.Object 接口的签名是这样的 https://github.com/kubernetes/apimachinery/blob/7089aafd1ef57551192f6ec14c5ed1f49494ccd2/pkg/runtime/interfaces.go#L237,因此每个 runtime.Object 不得不实现 DeepCopyObject

func (in *T) DeepCopyObject() runtime.Object {

    if c := in.DeepCopy(); c != nil {

        return c

    } else {

        return nil

    }

}

通过 // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 本地标签不用为你的每个类型都实现上述方法了。

上面的例子中 DatabaseDatabaseList 都是顶级类型因为它们都作为 runtime.Object 被使用了。顶级类型中都嵌入了 metav1.TypeMeta,而且都是使用 client-gen 生成的。

// +k8s:deepcopy-gen:interfaces 在这个案例中也应该被使用,因为自定义的 API 类型中有 interface 类型的字段。然后 // +k8s:deepcopy-gen:interfaces=example.com/pkg/apis/example.SomeInterface 会控制 DeepCopySomeInterface() SomeInterface 方法的生成。这样 deepcopy 这些字段时就不会出错。

client-gen 标签

还有些标签能够控制 client-gen:

// +genclient
// +genclient:noStatus

第一个告诉 client-gen 为这个类型生成一个 client。要注意这不是必要的,事实上也不一定要把它放在 API 对象的 List 类型上。

第二个告诉 client-gen 该类型没有 Status 字段。生成出来的 client 也没有 UpdateStatus 方法(client=gen 在你的结构体中看到 Status 字段就会无脑生成)。

集群范围的资源呢,你要用下面 tag:

// +genclient:nonNamespaced

还有些特殊的用法,比如控制 client 提供哪几种 HTTP 方法等:

// +genclient:noVerbs

// +genclient:onlyVerbs=create,delete

// +genclient:skipVerbs=get,list,create,update,patch,delete,deleteCollection,watch

// +genclient:method=Create,verb=create,result=k8s.io/apimachinery/pkg/apis/meta/v1.Status

前三个都是不言自明的。最后一个要说下,这个标签表示不会返回 API 类型本身,而是返回一个 metav1.Status。对于自定义资源来说没啥意义,但是用户自己用 golang 编写的 API server 中可能存在,而且 OpenShift API 中也这么搞。