代码生成器
介绍
client-go为每种k8s内置资源提供了对应的clientset和informer。那么我们要监听和操作自定义资源对象,应该如何做呢?
方式一:使用client-go提供的dynamicClient来操作自定义资源对象,当然由于dynamicClient是基于RESTClient实现的,所以我们可以使用RESTClient来达到同样的目的。
方式二: 使用conde-generator来帮我们生成我们需要的代码,这样我们就可以像使用client-go为k8s内置资源对象提供的方式监听和操作自定义资源了。
code-generator
code-generator 就是 Kubernetes 提供的一个用于代码生成的项目,它提供了以下工具为 Kubernetes 中的资源生成代码:
- deepcopy-gen: 生成深度拷贝方法,为每个 T 类型生成 func (t T) DeepCopy() T 方法,API 类型都需要实现深拷贝
 - client-gen: 为资源生成标准的 clientset
 - informer-gen: 生成 informer,提供事件机制来响应资源的事件
 - lister-gen: 生成 Lister,为 get 和 list 请求提供只读缓存层(通过 indexer 获取)
 
Informer 和 Lister 是构建控制器的基础,使用这4个代码生成器可以创建全功能的、和 Kubernetes 上游控制器工作机制相同的 production-ready 的控制器。
code-generator 还包含一些其它的代码生成器,例如 Conversion-gen 负责产生内外部类型的转换函数、Defaulter-gen 负责处理字段默认值。大部分的生成器支持—input-dirs参数来读取一系列输入包,处理其中的每个类型,然后生成代码:
    1、部分代码生成到输入包所在目录,例如 deepcopy-gen 生成器,也可以使用参数—output-file-base “zz_generated.deepcopy” 来定义输出文件名
    2、其它代码生成到 —output-package 指定的目录,例如 client-gen、informer-gen、lister-gen 等生成器
示例
接来下我们使用code-generator进行实战演示:
首先我们将项目拉到本地:
$ git clone https://github.com/kubernetes/code-generator.git$ git checkout 0.23.3
然后我们进入到cmd目录下,就会看到我们上面介绍的工具:
接着我们对client-gen,deepcopy-gen,infromer-gen,lister-gen进行安装,会安装到GOPATH的bin目录下:
# 进行安装$ go install ./cmd/{client-gen,deepcopy-gen,informer-gen,lister-gen}# 获取GOPATH路径$ go env | grep GOPATHGOPATH="/Users/Christian/go"# 查看ls /Users/Christian/go/binclient-gen deepcopy-gen goimports lister-gencontroller-gen defaulter-gen informer-gen type-scaffold
发现我们已经成功的安装了,这时候我们就可以直接使用这些工具了,比如我们可以使用—help命令来查看如何使用client-gen:
当然通常情况下我们不会去单独的使用某一个工具。
接下来我们来创建我们的项目,此处我们可以仿照sample controller项目进行编写:
$ mkdir operator-test && cd operator-test$ go mod init operator-test$ mkdir -p pkg/apis/example.com/v1➜ operator-test tree.├── go.mod├── go.sum└── pkg└── apis└── example.com└── v1├── doc.go├── register.go└── types.go4 directories, 5 files
接下来我们对v1下面的三个go文件进行填充(可以直接复制sample-controller,对其进行做简单修改):
doc.go主要是用来声明要使用deepconpy-gen以及groupName。
// pkg/crd.example.com/v1/doc.go// +k8s:deepcopy-gen=package// +groupName=example.compackage v1
types.go主要是定义crd资源对应的go中的结构。
// pkg/crd.example.com/v1/types.gopackage v1import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"// +genclient// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object// Bar is a specification for a Bar resourcetype Bar struct {metav1.TypeMeta `json:",inline"`metav1.ObjectMeta `json:"metadata,omitempty"`Spec BarSpec `json:"spec"`// Status BarStatus `json:"status"`}// BarSpec is the spec for a Bar resourcetype BarSpec struct {DeploymentName string `json:"deploymentName"`Image string `json:"image"`Replicas *int32 `json:"replicas"`}// BarStatus is the status for a Bar resourcetype BarStatus struct {AvailableReplicas int32 `json:"availableReplicas"`}// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object// BarList is a list of Bar resourcestype BarList struct {metav1.TypeMeta `json:",inline" :"metav1.TypeMeta"`metav1.ListMeta `json:"metadata" :"metav1.ListMeta"`Items []Bar `json:"items" :"items"`}
register.go顾名思义,就是注册资源。
package v1import (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 objectsvar SchemeGroupVersion = schema.GroupVersion{Group: "example.com", Version: "v1"}// Kind takes an unqualified kind and returns back a Group qualified GroupKindfunc Kind(kind string) schema.GroupKind {return SchemeGroupVersion.WithKind(kind).GroupKind()}// Resource takes an unqualified resource and returns a Group qualified GroupResourcefunc Resource(resource string) schema.GroupResource {return SchemeGroupVersion.WithResource(resource).GroupResource()}var (// SchemeBuilder initializes a scheme builderSchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)// AddToScheme is a global function that registers this API group & version to a schemeAddToScheme = SchemeBuilder.AddToScheme)// Adds the list of known types to Scheme.func addKnownTypes(scheme *runtime.Scheme) error {scheme.AddKnownTypes(SchemeGroupVersion,&Bar{},&BarList{},)metav1.AddToGroupVersion(scheme, SchemeGroupVersion)return nil}
这时候会发现&Bar{},&BarLis{}会报错,这是因为我们还没有为其实现deepcopy方法。
由于在自动生成代码的时候,需要指定header的信息,所以我们为了方便,可以将code-generator项目下的hack包直接拷贝到我们当前项目根目录下。
接下来我们使用code-generator来为我们自动生成代码:
# 运行 code-generator/generate-group.sh./../../github/code-generator/generate-groups.sh all \# 指定 group 和 version,生成deeplycopy以及clientoperator-test/pkg/client operator-test/pkg/apis crd.example.com:v1 \# 指定头文件--go-header-file=./hack/boilerplate.go.txt \# 指定输出位置,默认为GOPATH--output-base ../Generating deepcopy funcsGenerating clientset for crd.example.com:v1 at operator-test/pkg/client/clientsetGenerating listers for crd.example.com:v1 at operator-test/pkg/client/listersGenerating informers for crd.example.com:v1 at operator-test/pkg/client/informers
这时候我们再来查看项目结构:
➜ operator-test tree.├── go.mod├── go.sum├── hack│ └── boilerplate.go.txt└── pkg├── apis│ └── crd.example.com│ └── v1│ ├── doc.go│ ├── register.go│ ├── types.go│ └── zz_generated.deepcopy.go└── client├── clientset│ └── versioned│ ├── clientset.go│ ├── doc.go│ ├── fake│ │ ├── clientset_generated.go│ │ ├── doc.go│ │ └── register.go│ ├── scheme│ │ ├── doc.go│ │ └── register.go│ └── typed│ └── crd.example.com│ └── v1│ ├── bar.go│ ├── crd.example.com_client.go│ ├── doc.go│ ├── fake│ │ ├── doc.go│ │ ├── fake_bar.go│ │ └── fake_crd.example.com_client.go│ └── generated_expansion.go├── informers│ └── externalversions│ ├── crd.example.com│ │ ├── interface.go│ │ └── v1│ │ ├── bar.go│ │ └── interface.go│ ├── factory.go│ ├── generic.go│ └── internalinterfaces│ └── factory_interfaces.go└── listers└── crd.example.com└── v1├── bar.go└── expansion_generated.go22 directories, 29 files
这时候我们就可以像操作内置资源一样,操作我们的自定义资源了。
我们先准备crd以及对应的cr,这边也是可以直接从sample-controller项目进行拷贝,做简单的修改即可。
# manifests/example.com_bars.yaml---apiVersion: apiextensions.k8s.io/v1kind: CustomResourceDefinitionmetadata:annotations:controller-gen.kubebuilder.io/version: (devel)creationTimestamp: nullname: bars.crd.example.comspec:group: crd.example.comnames:kind: BarlistKind: BarListplural: barssingular: barscope: Namespacedversions:- name: v1schema:openAPIV3Schema:description: Bar is a specification for a Bar resourceproperties:apiVersion:description: 'APIVersion defines the versioned schema of this representationof an object. Servers should convert recognized schemas to the latestinternal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'type: stringkind:description: 'Kind is a string value representing the REST resource thisobject represents. Servers may infer this from the endpoint the generatedsubmits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'type: stringmetadata:type: objectspec:description: BarSpec is the spec for a Bar resourceproperties:deploymentName:type: stringimage:type: stringreplicas:format: int32type: integerrequired:- deploymentName- image- replicastype: objectrequired:- spectype: objectserved: truestorage: true# manifests/cr.yaml---apiVersion: crd.example.com/v1kind: Barmetadata:name: bar-demonamespace: defaultspec:image: "nginx:1.17.1"deploymentName: example-barreplicas: 2
接下来我们来编写main函数,这时候我们就可以使用client-go像操作我们内置资源一样,操作crd资源了。
package mainimport ("context""fmt"v1 "k8s.io/apimachinery/pkg/apis/meta/v1""k8s.io/client-go/tools/cache""k8s.io/client-go/tools/clientcmd""log"clientSet "operator-test/pkg/client/clientset/versioned""operator-test/pkg/client/informers/externalversions")func main() {config, err := clientcmd.BuildConfigFromFlags("", clientcmd.RecommendedHomeFile)if err != nil {log.Fatalln(err)}clientset, err := clientSet.NewForConfig(config)if err != nil {log.Fatalln(err)}list, err := clientset.CrdV1().Bars("default").List(context.TODO(), v1.ListOptions{})if err != nil {log.Fatalln(err)}for _, bar := range list.Items {fmt.Println(bar.Name)}factory := externalversions.NewSharedInformerFactory(clientset, 0)factory.Crd().V1().Bars().Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{AddFunc: nil,UpdateFunc: nil,DeleteFunc: nil,})// todo}// ====// 程序输出结果:bar-demo
代码生成tag
在我们上面的示例中,我们在源码中添加了很多tag,我们使用这些tag来标记一些供生成器使用的属性。这些tag主要分为两类:
- 在doc.go的package语句智商提供的全局tag
 - 在需要被处理的类型上提供局部tag
 
tag的使用方法如下所示:
// +tag-name// 或者// +tag-name=value
我们可以看到 tag 是通过注释的形式存在的,另外需要注意的是 tag 的位置非常重要,很多 tag 必须直接位于 type 或 package 语句的上一行,另外一些则必须和 go 语句隔开至少一行空白。
全局tag
必须在目标包的doc.go文件中声明,一般路径为pkg/apis/
// 为包中任何类型生成深拷贝方法,可以在局部 tag 覆盖此默认行为// +k8s:deepcopy-gen=package// groupName 指定 API 组的全限定名// 此 API 组的 v1 版本,放在同一个包中// +groupName=crd.example.compackage v1
局部tag
局部tag要么直接声明在类型之前,要么位于类型之前的第二个注释块中。下面的 types.go 中声明了 CR 对应的类型:
// 为当前类型生成客户端,如果不加此注解则无法生成 lister、informer 等包// +genclient// 提示此类型不基于 /status 子资源来实现 spec-status 分离,产生的客户端不具有 UpdateStatus 方法// 否则,只要类型具有 Status 字段,就会生成 UpdateStatus 方法// +genclient:noStatus// 为每个顶级 API 类型添加,自动生成 DeepCopy 相关代码// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object// K8S 资源,数据库type Database struct {metav1.TypeMeta `json:",inline"`metav1.ObjectMeta `json:"metadata,omitempty"`Spec DatabaseSpec `json:"spec"`}// 不为此类型生成深拷贝方法// +k8s:deepcopy-gen=false// 数据库的规范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// 数据库列表,因为 list 获取的是列表,所以需要定义该结构type DatabaseList struct {metav1.TypeMeta `json:",inline"`metav1.ListMeta `json:"metadata"`Items []Database `json:"items"`}
在上面 CR 的定义上面就通过 tag 来添加了自动生成相关代码的一些注释。此外对于集群级别的资源,我们还需要提供如下所示的注释:
// +genclient:nonNamespaced// 下面的 Tag 不能少// +genclient
另外我们还可以控制客户端提供哪些 HTTP 方法:
// +genclient:noVerbs// +genclient:onlyVerbs=create,delete// +genclient:skipVerbs=get,list,create,update,patch,delete,deleteCollection,watch// 仅仅返回 Status 而非整个资源// +genclient:method=Create,verb=create,result=k8s.io/apimachinery/pkg/apis/meta/v1.Status// 下面的 Tag 不能少// +genclient
使用 tag 定义完需要生成的代码规则后,执行上面提供的代码生成脚本即可自动生成对应的代码了。
补充
除了上面介绍的代码生成方式,我们还可以直接使用sample-controller项目提供的hack/update-condegen.sh脚本。
#!/usr/bin/env bashset -o errexitset -o nounsetset -o pipefailSCRIPT_ROOT=$(dirname "${BASH_SOURCE[0]}")/..# 代码生成器包的位置CODEGEN_PKG=${CODEGEN_PKG:-$(cd "${SCRIPT_ROOT}"; ls -d -1 ./vendor/k8s.io/code-generator 2>/dev/null || echo ../code-generator)}# generate-groups.sh <generators> <output-package> <apis-package> <groups-versions># 使用哪些生成器,可选值 deepcopy,defaulter,client,lister,informer,逗号分隔,all表示全部使用# 输出包的导入路径# CR 定义所在路径# API 组和版本bash "${CODEGEN_PKG}"/generate-groups.sh "deepcopy,client,informer,lister" \k8s.io/sample-controller/pkg/generated k8s.io/sample-controller/pkg/apis \samplecontroller:v1alpha1 \--output-base "$(dirname "${BASH_SOURCE[0]}")/../../.." \--go-header-file "${SCRIPT_ROOT}"/hack/boilerplate.go.txt# 自动生成的源码头部附加的内容:# --go-header-file "${SCRIPT_ROOT}"/hack/custom-boilerplate.go.txt
执行上面的脚本后,所有 API 代码会生成在 pkg/apis 目录下,clientsets、informers、listers 则生成在 pkg/generated 目录下。不过从脚本可以看出需要将 code-generator 的包放置到 vendor 目录下面,现在我们都是使用 go modules 来管理依赖保,我们可以通过执行 go mod vendor 命令将依赖包放置到 vendor 目录下面来。
我们还可以进一步提供 hack/verify-codegen.sh 脚本,用于判断生成的代码是否 up-to-date:
#!/usr/bin/env bashset -o errexitset -o nounsetset -o pipefail# 先调用 update-codegen.sh 生成一份新代码# 然后对比新老代码是否一样SCRIPT_ROOT=$(dirname "${BASH_SOURCE[0]}")/..DIFFROOT="${SCRIPT_ROOT}/pkg"TMP_DIFFROOT="${SCRIPT_ROOT}/_tmp/pkg"_tmp="${SCRIPT_ROOT}/_tmp"cleanup() {rm -rf "${_tmp}"}trap "cleanup" EXIT SIGINTcleanupmkdir -p "${TMP_DIFFROOT}"cp -a "${DIFFROOT}"/* "${TMP_DIFFROOT}""${SCRIPT_ROOT}/hack/update-codegen.sh"echo "diffing ${DIFFROOT} against freshly generated codegen"ret=0diff -Naupr "${DIFFROOT}" "${TMP_DIFFROOT}" || ret=$?cp -a "${TMP_DIFFROOT}"/* "${DIFFROOT}"if [[ $ret -eq 0 ]]thenecho "${DIFFROOT} up to date."elseecho "${DIFFROOT} is out of date. Please run hack/update-codegen.sh"exit 1fi
