# 使用code-generator生成crd的clientset、informer、listers ## 概念简介 ### code-generator `k8s.io/client-go` 提供了对k8s原生资源的informer和clientset等等,但对于自定义资源的操作则相对低效,需要使用 rest api 和 dynamic client 来操作,并自己实现反序列化等功能。 code-generator 提供了以下工具用于为k8s中的资源生成相关代码,可以更加方便的操作自定义资源: - `deepcopy-gen`: 生成深度拷贝对象方法 使用方法: - 在文件中添加注释`// +k8s:deepcopy-gen=package` - 为单个类型添加自动生成`// +k8s:deepcopy-gen=true` - 为单个类型关闭自动生成`// +k8s:deepcopy-gen=false` - `client-gen`: 为资源生成标准的操作方法(get;list;watch;create;update;patch;delete) 在pkg/apis/${GROUP}/${VERSION}/types.go中使用,使用// +genclient标记对应类型生成的客户端, 如果与该类型相关联的资源不是命名空间范围的(例如PersistentVolume), 则还需要附加// + genclient:nonNamespaced标记, - // +genclient - 生成默认的客户端动作函数(create, update, delete, get, list, update, patch, watch以及 是否生成updateStatus取决于.Status字段是否存在)。 - // +genclient:nonNamespaced - 所有动作函数都是在没有名称空间的情况下生成 - // +genclient:onlyVerbs=create,get - 指定的动作函数被生成. - // +genclient:skipVerbs=watch - 生成watch以外所有的动作函数. - // +genclient:noStatus - 即使.Status字段存在也不生成updateStatus动作函数 - `informer-gen`: 生成informer,提供事件机制(AddFunc,UpdateFunc,DeleteFunc)来响应kubernetes的event - `lister-gen`: 为get和list方法提供只读缓存层 - `conversion-gen`是用于自动生成在内部和外部类型之间转换的函数的工具 一般的转换代码生成任务涉及三套程序包: - 一套包含内部类型的程序包, - 一套包含外部类型的程序包 - 单个目标程序包(即,生成的转换函数所在的位置,以及开发人员授权的转换功能所在的位置)。包含内部类型的包在Kubernetes的常规代码生成框架中扮演着称为`peer package`的角色。 使用方法: - 标记转换内部软件包 `// +k8s:conversion-gen=` - 标记转换外部软件包`// +k8s:conversion-gen-external-types=` - 标记不转换对应注释或结构 `// +k8s:conversion-gen=false` - `defaulter-gen` 用于生产Defaulter函数 - 为包含字段的所有类型创建defaulters,`// +k8s:defaulter-gen=` - 所有都生成`// +k8s:defaulter-gen=true|false` - `go-to-protobuf` 通过go struct生成pb idl - `import-boss` 在给定存储库中强制执行导入限制 - `openapi-gen` 生成openAPI定义 使用方法: - `+k8s:openapi-gen=true` 为指定包或方法开启 - `+k8s:openapi-gen=false` 指定包关闭 - `register-gen` 生成register - `set-gen` code-generator整合了这些gen,使用脚本[generate-groups.sh](https://github.com/kubernetes/code-generator/blob/master/generate-groups.sh)和[generate-internal-groups.sh](https://github.com/kubernetes/code-generator/blob/master/generate-internal-groups.sh)可以为自定义资源生产相关代码。 ### kubebuilder Kubebuilder是用于使用 [自定义资源定义(CRD)](https://kubernetes.io/docs/tasks/access-kubernetes-api/extend-api-custom-resource-definitions)构建Kubernetes API的框架。 类似于*Ruby on Rails*和*SpringBoot之*类的Web开发框架,Kubebuilder可以提高速度并降低开发人员管理的复杂性,以便在Go中快速构建和发布Kubernetes API。它建立在用于构建核心Kubernetes API的规范技术的基础之上,以提供减少样板和麻烦的简单抽象。 Resource + Controller = Operator,可以利用Kubebuilder编写自定义资源的Operator。 ## 结合背景 `Kubebuilder` 与 `code-generator` 都可以为CRD生成Kubernetes API相关代码,从代码生成层面来讲, 两者的区别在于: - Kubebuilder不会生成informers、listers、clientsets,而code-generator会。 - Kubebuilder会生成Controller、Admission Webhooks,而code-generator不会。 - Kubebuilder会生成manifests yaml,而code-generator不会。 - Kubebuilder还带有一些其他便利性设施。 使用Kubebuilder可以快捷生成CRD以及相关的控制器框架,然而由于Kubebuilder不会生成clientset等包,当别的服务想要操作CRD时将会很麻烦。 两者结合后可以使用Kubebuilder生成CRD和一整套控制器架构,再使用code-generator生成informers、listers、clientsets等。 ## 初始化项目 ### 创建一个项目 ``` [root@kubebuilder ~]# mkdir -p $GOPATH/src/my.domain/example [root@kubebuilder ~]# cd $GOPATH/src/my.domain/example [root@kubebuilder example]# kubebuilder init --domain my.domain [root@kubebuilder example]# tree -CL 2 . ├── config │   ├── default │   ├── manager │   ├── prometheus │   └── rbac ├── Dockerfile ├── go.mod ├── go.sum ├── hack │   └── boilerplate.go.txt ├── main.go ├── Makefile └── PROJECT ``` ### 创建一个api ``` [root@kubebuilder example]# kubebuilder create api --group example --version v1 --kind Guestbook Create Resource [y/n] y Create Controller [y/n] y ... [root@kubebuilder example]# tree -CL 3 . ├── api │   └── v1 │   ├── groupversion_info.go │   ├── guestbook_types.go │   └── zz_generated.deepcopy.go ├── bin │   └── controller-gen ├── config │   ├── crd │   │   ├── kustomization.yaml │   │   ├── kustomizeconfig.yaml │   │   └── patches │   ├── default │   │   ├── kustomization.yaml │   │   ├── manager_auth_proxy_patch.yaml │   │   └── manager_config_patch.yaml │   ├── manager │   │   ├── controller_manager_config.yaml │   │   ├── 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 │   │   ├── guestbook_editor_role.yaml │   │   ├── guestbook_viewer_role.yaml │   │   ├── kustomization.yaml │   │   ├── leader_election_role_binding.yaml │   │   ├── leader_election_role.yaml │   │   ├── role_binding.yaml │   │   └── service_account.yaml │   └── samples │   └── example_v1_guestbook.yaml ├── controllers │   ├── guestbook_controller.go │   └── suite_test.go ├── Dockerfile ├── go.mod ├── go.sum ├── hack │   └── boilerplate.go.txt ├── main.go ├── Makefile └── PROJECT ``` **注意:** 如果修改了 `api/v1/guestbook_types.go` ,需要执行以下命令来更新代码和manifests: ``` make && make manifests ``` ## 使用code-generator ### 更新依赖版本 初始化项目后的go.mod: ``` [root@kubebuilder example]# cat go.mod module my.domain go 1.16 require ( github.com/onsi/ginkgo v1.14.1 github.com/onsi/gomega v1.10.2 k8s.io/apimachinery v0.20.2 k8s.io/client-go v0.20.2 sigs.k8s.io/controller-runtime v0.8.3 ) ``` 需要将初始化的k8s库更新到要使用的版本,如: ``` [root@kubebuilder example]# K8S_VERSION=v0.18.6 [root@kubebuilder example]# go get k8s.io/client-go@$K8S_VERSION [root@kubebuilder example]# go get k8s.io/apimachinery@$K8S_VERSION ``` ### 安装code-generator k8s的版本号与 `go.mod` 中的 `k8s.io/client-go` 的版本保持一致即可。 **注意:**需要将依赖复制到vendor中 ``` 最新版本已经不包含, github.com/googleapis/gnostic/OpenAPIv2 ,如果不改版本会报如下错误 ... github.com/googleapis/gnostic/OpenAPIv2: module github.com/googleapis/gnostic@latest found (v0.5.5), but does not contain package github.com/googleapis/gnostic/OpenAPIv2 Error: failed to create API: unable to run post-scaffold tasks of "base.go.kubebuilder.io/v3": exit status 1 ... [root@kubebuilder example]# go get github.com/googleapis/gnostic@v0.4.0 [root@kubebuilder example]# go get k8s.io/code-generator@$K8S_VERSION [root@kubebuilder example]# go mod vendor ``` ### 创建&修改所需文件 需要在api目录下创建code-generator所需的文件,并添加相关注释。 - 新增 `api/v1/doc.go` **注意:**修改groupName,package与api的version保持一致。 ``` [root@kubebuilder example]# cat api/v1/doc.go // +k8s:deepcopy-gen=package // Package v1 is the v1alpha1 version of the API. // +groupName=example.my.domain package v1 ``` - 新增 `api/v1/register.go` **注意:**package与api的version保持一致。 ``` [root@kubebuilder example]# cat api/v1/register.go package v1 import ( "k8s.io/apimachinery/pkg/runtime/schema" ) // SchemeGroupVersion is group version used to register these objects. var SchemeGroupVersion = GroupVersion // Resource takes an unqualified resource and returns a Group qualified GroupResource func Resource(resource string) schema.GroupResource { return SchemeGroupVersion.WithResource(resource).GroupResource() } ``` - 修改 `api/v1/{crd}_types.go` 文件,添加注释 `// +genclient` ``` [root@kubebuilder example]# cat api/v1/guestbook_types.go ... // +genclient //+kubebuilder:object:root=true // GuestbookList contains a list of Guestbook type GuestbookList struct { ... ``` ### 准备脚本 在项目 `hack` 目录下准备以下文件: - 新建 `hack/tools.go` 文件 ``` [root@kubebuilder example]# cat hack/tools.go // +build tools package tools import _ "k8s.io/code-generator" ``` - 新建 `hack/update-codegen.sh`,注意根据项目修改相应变量: `MODULE` 和 `go.mod` 保持一致 `API_PKG=api`,和 `api` 目录保持一致 `OUTPUT_PKG=generated/example`,与生成Resource时指定的group保持一致 `GROUP=example`, 和生成Resource时指定的group 保持一致 `VERSION=v1`, 和生成Resource时指定的version保持一致 ``` [root@kubebuilder example]# cat ./hack/update-codegen.sh #!/usr/bin/env bash #表示有报错即退出 跟set -e含义一样 set -o errexit #执行脚本的时候,如果遇到不存在的变量,Bash 默认忽略它 ,跟 set -u含义一样 set -o nounset # 只要一个子命令失败,整个管道命令就失败,脚本就会终止执行 set -o pipefail #kubebuilder项目的MODULE MODULE=my.domain/example #api包 APIS_PKG=api #代码生出输出,生成Resource时指定的group一样 OUTPUT_PKG=generated/example # group-version such as cronjob:v1 GROUP=example VERSION=v1 GROUP_VERSION=$GROUP:$VERSION SCRIPT_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)} # kubebuilder2.3.2版本生成的api目录结构code-generator无法直接使用 rm -rf "${APIS_PKG}/${GROUP}" && mkdir -p "${APIS_PKG}/${GROUP}" && cp -r "${APIS_PKG}/${VERSION}/" "${APIS_PKG}/${GROUP}" # generate the code with: # --output-base because this script should also be able to run inside the vendor dir of # k8s.io/kubernetes. The output-base is needed for the generators to output into the vendor dir # instead of the $GOPATH directly. For normal projects this can be dropped. #client,informer,lister(注意: code-generator 生成的deepcopy不适配 kubebuilder 所生成的api) bash "${CODEGEN_PKG}"/generate-groups.sh "client,informer,lister" \ ${MODULE}/${OUTPUT_PKG} ${MODULE}/${APIS_PKG} \ ${GROUP_VERSION} \ --go-header-file "${SCRIPT_ROOT}"/hack/boilerplate.go.txt # --output-base "${SCRIPT_ROOT}" # --output-base "${SCRIPT_ROOT}/../../.." ``` **注意:** 1. kubebuilder2.3.2版本生成的api目录结构code-generator无法直接使用,需要在sh脚本中进行处理。 2. 修改脚本执行参数可以选择生成的代码,如:"client,informer,lister"。**注意无需再次生成deepcopy:**code-generator 生成的deepcopy不适配 kubebuilder 所生成的api。 - 修改 `Makefile` ,添加生成命令 ``` [root@kubebuilder example]# chmod +x hack/update-codegen.sh [root@kubebuilder example]# vim Makefile 71 update-codegen: ## generetor clientset informer inderx code 72 ./hack/update-codegen.sh ``` ### 生成代码 项目根目录下执行`make update-codegen `即可,将生成如下代码结构: ``` [root@kubebuilder example]# tree generated/ generated/ └── example ├── clientset │   └── versioned │   ├── clientset.go │   ├── doc.go │   ├── fake │   │   ├── clientset_generated.go │   │   ├── doc.go │   │   └── register.go │   ├── scheme │   │   ├── doc.go │   │   └── register.go │   └── typed │   └── example │   └── v1 │   ├── doc.go │   ├── example_client.go │   ├── fake │   │   ├── doc.go │   │   ├── fake_example_client.go │   │   └── fake_guestbook.go │   ├── generated_expansion.go │   └── guestbook.go ├── informers │   └── externalversions │   ├── example │   │   ├── interface.go │   │   └── v1 │   │   ├── guestbook.go │   │   └── interface.go │   ├── factory.go │   ├── generic.go │   └── internalinterfaces │   └── factory_interfaces.go └── listers └── example └── v1 ├── expansion_generated.go └── guestbook.go ``` 之后便可以通过clientset等包对自定义资源对象进行操作。 **注意事项:** kubebuilder2.3.2版本生成的api目录结构为 `api/v1`,而code-generator需要的api目录结构为 `api/example/v1`,相比较增加了group这一层。 - `hack/update-codegen.sh` 脚本会自动根据kubebuilder的api生成code-generator所需目录结构。 - code-generator 生成的deepcopy不适配 kubebuilder 所生成的api。 - code-generator 生成代码后再次使用make操作时可能由于生成的代码影响命令正常执行,例如: `make manifests` 在生成CRD模版时,需先删除api以及generated目录中为code-generator生成的代码,才可正常生成CRD模版。 - 使用时kubebuilder正常使用 `api/v1` 中的types,而code-generator生成的clientset等则需要使用 `api/example/v1` 中的types。