Kubernetes CustomResource 代码生成
Feb 3, 2021 19:15 · 2260 words · 5 minute read
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-gen、informer-gen、lister-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
本地标签不用为你的每个类型都实现上述方法了。
上面的例子中 Database
和 DatabaseList
都是顶级类型因为它们都作为 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 中也这么搞。