简介
code-generator是Kubernetes提供的一个代码生成工具,可以自动生成客户端代码、DeepCopy
函数、Informers
和Listers
、API服务器、文档以及其他与API相关的代码。主要用于快速开发CRD资源。
github地址:https://github.com/kubernetes/code-generator
主要组件
- applyconfiguration-gen:为CRD生成apply配置文件代码。
- client-gen:生成客户端代码
- conversion-gen:生成对象间转换需要的conversion函数,方便在不同版本之间进行对象转换。
- deepcopy-gen:为每个
T
类型生成func (t* T) DeepCopy() *T
方法,API类型都需要实现深拷贝,在客户端和服务器之间交换数据时,需要独立的数据副本。 - defaulter-gen:生成设置默认值的方法,如果某些字段未设置,则自动填充默认值。
- go-to-protobuf:将
Go
语言对象转换为Protobuf
定义。Protobuf 是一种高效的二进制序列化格式,用于优化 Kubernetes API 性能。 - informer-gen:为
listwatch
生成shared informer
代码。 - lister-gen:生成
lister
代码。Listers
为get和list请求提供只读缓存层(通过indexer
获取),可以实现高效的资源列表查询。 - prerelease-lifecycle-gen:生成API生命周期的代码,如标记API的
alpha
、beta
、stable
状态等,用于管理API版本的生命周期。 - register-gen:生成API注册代码,包括资源类型和其对应的API组版本的注册信息。
如果要开发CRD,经常用到的是deepcopy-gen
和register-gen
。
如果要开发apiserver,则还会经常用到defaulter-gen
,conversion-gen
,go-to-protobuf
。
无论通过CRD还是apiserver,都可能要为API生成客户端,也就会用到applyconfiguration-gen
,client-gen
,lister-gen
,informer-gen
。
常用tag
标记(Tags)是用来指导代码生成过程的特殊注释。这些标记可以在你的Go源代码中直接使用,告诉code-generator如何生成特定的代码。code-generator中的tag分为global tags
和local tags
。
- global tags: 全局的tag,位于具体版本的
doc.go
文件中。 - local tags: 本地的tag,位于
types.go
文件中的struct上。
使用方法有两种:
- // +tagName
- // +tagName=value
doc.go中的tag
// +k8s:openapi-gen=true
// +k8s:deepcopy-gen=package
// +k8s:defaulter-gen=TypeMeta
// +k8s:conversion-gen=k8s.io/code-generator/examples/apiserver/apis/example
// +groupName=example.apiserver.code-generator.k8s.io
package v1 // import "k8s.io/code-generator/examples/apiserver/apis/example/v1"
+k8s:openapi-gen=true
:指示openapi-gen
为这个包生成OpenAPI定义。OpenAPI是一种与语言无关的规范,用于描述RESTful API。
+k8s:deepcopy-gen=package
:指示deepcopy-gen
为当前包中的所有类型生成深拷贝方法。
+k8s:defaulter-gen=TypeMeta
:表示要为当前文件中的TypeMeta
结构体生成默认值设置函数。
+k8s:conversion-gen=k8s.io/code-generator/examples/apiserver/apis/example
:指示conversion-gen
为指定的API组和路径生成类型转换函数。
+groupName=example.apiserver.code-generator.k8s.io
:表示生成的代码属于 example.apiserver.code-generator.k8s.io
这个API组。
types.go中的tag
package example
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
// +genclient
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// TestType is a top-level type. A client is created for it.
type TestType struct {
metav1.TypeMeta
metav1.ObjectMeta
Status TestTypeStatus
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// TestTypeList is a top-level list type. The client methods for lists are automatically created.
// You are not supposed to create a separated client for this one.
type TestTypeList struct {
metav1.TypeMeta
metav1.ListMeta
Items []TestType
}
type TestTypeStatus struct {
Blah string
}
+genclient
:指示client-gen
为当前类型生成一个客户端接口。
+k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
:// +k8s:deepcopy-gen
表示为当前文件中的类型生成深度拷贝方法。:interfaces=
指定要为哪些接口生成深度拷贝方法。k8s.io/apimachinery/pkg/runtime.Object
表示要为runtime.Object
接口生成深度拷贝方法。
type TypeMeta struct {
Kind string `json:"kind,omitempty" protobuf:"bytes,1,opt,name=kind"`
APIVersion string `json:"apiVersion,omitempty" protobuf:"bytes,2,opt,name=apiVersion"`
}
type Object interface {
GetObjectKind() schema.ObjectKind
DeepCopyObject() Object
}
type ObjectKind interface {
SetGroupVersionKind(kind GroupVersionKind)
GroupVersionKind() GroupVersionKind
}
即要为该文件中的类型实现GetObjectKind()
和DeepCopyObject()
这两个方法。而类型定义的struct中已经定义了metav1.TypeMeta
,而TypeMeta
继承了func GetObjectKind() schema.ObjectKind
方法,所以只需要实现DeepCopyObject()
方法即可。
client-gen中的常用tag
+genclient
: 指示client-gen
为这个类型生成一个客户端接口。+genclient:nonNamespaced
: 当类型不是命名空间范围内的资源时,使用这个标记。默认情况下,client-gen
生成的客户端方法假设资源是命名空间范围内的。+genclient:noVerbs
: 这个标记告诉client-gen
不为这个类型生成任何方法。这在你只需要类型的声明而不需要与之交互时很有用。+genclient:noStatus
: 指示client-gen
不为类型生成更新状态的方法。这适用于那些没有Status子资源的自定义资源。+genclient:onlyVerbs
: 列出要生成的方法,这个标记限制client-gen
生成的操作类型。例如+genclient:onlyVerbs=create,update
将只生成创建和更新操作。+groupName=<name>
: 指定客户端接口应该属于哪个API组。这对于自定义资源尤其重要,因为它们可能不属于Kubernetes核心API组。
deepcopy-gen中的常用tag
+k8s:deepcopy-gen=<arguments>
: 这是控制深拷贝方法生成的主要标记。它可以接受不同的参数来指导生成过程。例如,当设置为interfaces时,可以指定某个类型实现了自定义的深拷贝接口。+k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
: 这个标记指定类型应该实现Kubernetes的runtime.Object
接口,它需要有自己的深拷贝方法。+k8s:deepcopy-gen=false
: 使用这个标记可以排除某些类型或字段不生成深拷贝方法。这对于那些不需要深拷贝或者深拷贝逻辑需要自定义实现的情况非常有用。+k8s:deepcopy-gen=package
: 这个标记放在包的doc.go
文件中,表示为该包中的所有类型生成深拷贝方法,除非另外指定。
informer-gen中的常用tag
+k8s:informers
:使用informer-gen
必须要写这一行。
+k8s:informers:groupVersion=${group}/${version}
:指定生成的Informer
代码对应的API组和版本。
+k8s:informers:internalVersion=internal/version
:指定生成的Informer
代码对应的内部API版本。
+k8s:informers:versionedClientSet=false
:指定生成的Informer
代码是否需要为版本化的客户端集(Versioned ClientSet)生成代码。
+k8s:informers:cache
:指定生成的Informer
代码是否需要使用缓存。
如何使用
上面很多个gen工具中有重复参数,以前的版本提供了generate-groups.sh
和generate-internal-groups.sh
脚本批量生成。在k8s 1.28 alpha版本之后可以使用kube_codegen.sh
脚本生成。脚本目录分别为:
https://github.com/kubernetes/code-generator/blob/release-1.24/generate-groups.sh
https://github.com/kubernetes/code-generator/blob/release-1.24/generate-internal-groups.sh
https://github.com/kubernetes/code-generator/blob/master/kube_codegen.sh
生成代码
初始化项目
apimachinery的版本根据集群版本做对应修改。
mkdir foo-controller && cd foo-controller
go mod init foo-controller
go get k8s.io/apimachinery@v0.24.16
编写API文件
项目对外API都会包含这3个文件:doc.go
,types.go
和register.go
。创建目录api/<group>/<version>
,创建这三个文件,目录结构如下:
mkdir -p pkg/apis/foo/v1 && cd pkg/apis/foo/v1
touch doc.go
touch types.go
touch register.go
doc.go
通常用于包级别的文档注释和说明。它可以包含有关该包的概述、作者信息、许可证信息以及其他相关文档内容。
// +k8s:deepcopy-gen=package
// +groupName=foo.example.com
package v1
types.go
通常包含structs的定义,用于描述API对象的结构。这些类型定义将被code-generator
用来生成客户端库、深拷贝函数等。
package v1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// These const variables are used in our custom controller.
const (
GroupName string = "foo.example.com"
Kind string = "Foo"
Version string = "v1"
Plural string = "foos"
Singluar string = "foo"
ShortName string = "foo"
Name string = Plural + "." + GroupName
)
// +genclient
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// Foo is a specification for a Foo resource
type Foo struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec FooSpec `json:"spec"`
Status FooStatus `json:"status"`
}
// FooSpec is the spec for a Foo resource
type FooSpec struct {
DeploymentName string `json:"deploymentName"`
Replicas *int32 `json:"replicas"`
}
// FooStatus is the status for a Foo resource
type FooStatus struct {
AvailableReplicas int32 `json:"availableReplicas"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// FooList is a list of Foo resources
type FooList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata"`
Items []Foo `json:"items"`
}
register.go
用于注册定义在types.go
中的类型到Kubernetes中。这包括告诉Kubernetes这些类型的存在,以及如何在序列化、反序列化和存储时处理这些类型。这个文件确保了Kubernetes可以正确地识别和处理自定义资源。
package v1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
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
)
// SchemeGroupVersion is group version used to register these objects.
var SchemeGroupVersion = schema.GroupVersion{
Group: GroupName,
Version: Version,
}
func Resource(resource string) schema.GroupResource {
return SchemeGroupVersion.WithResource(resource).GroupResource()
}
func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(SchemeGroupVersion,
&Foo{},
&FooList{},
)
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
return nil
}
代码生成
创建hack目录。包括boilerplate.go.txt
,verify-codegen.sh
,update-codegen.sh
。
mkdir hack
touch hack/update-codegen.sh
touch hack/boilerplate.go.txt
touch hack/verify-codegen.sh
boilerplate.go.txt
通常用作许可证和版权信息的模板。当使用code-generator
生成代码时这个文件中的内容会被自动添加到的每个文件的顶部。这是一种确保所有生成的代码文件都包含正确的许可证信息的机制,有助于维护项目的法律合规性。
/*
Copyright The Kubernetes Authors.
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.
*/
update-codegen.sh
脚本的作用调用生成器脚本,根据指定的API定义和选项生成代码。当你修改了API定义或其他需要通过代码生成器处理的部分时,你可以运行这个脚本来自动更新所有相关的代码文件,确保项目中的自动生成代码是最新的。
#!/usr/bin/env bash
# 设置脚本在执行过程中遇到任何错误时立即退出
set -o errexit
# 设置脚本在使用未定义的变量时立即退出
set -o nounset
# 设置脚本在管道中的任何一个命令失败时立即退出
set -o pipefail
# 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.
../vendor/k8s.io/code-generator/generate-groups.sh "all" \
foo-controller/pkg/generated \
foo-controller/pkg/apis \
"foo:v1" \
--go-header-file $(pwd)/boilerplate.go.txt \
--output-base $(pwd)/../../
# To use your own boilerplate text append:
# --go-header-file "${SCRIPT_ROOT}"/hack/custom-boilerplate.go.txt
参数解释:
- all:指定要生成的代码类型,all表示所有,包括client,informer,listener,deepcopy等。
foo-controller/pkg/generated
:指定生成的代码的输出目录。foo-controller/pkg/apis
:指定包含API定义的目录。foo:v1
:指定要生成的API版本和组。--go-header-file $(pwd)/boilerplate.go.txt
:指定用于生成的代码文件头部的文本文件,即当前目录下的boilerplate.go.txt
文件。--output-base $(pwd)/../../
:指定生成的代码的基础目录,即输出目录的上一级目录。
verify-codegen.sh
脚本的作用是验证当前项目中自动生成的代码是否是最新的。如果生成的代码与源代码不一致,这意味着有人在没有重新生成代码的情况下更改了API定义或者其他需要自动生成代码的部分。这个脚本在CI(持续集成)流程中特别有用,确保所有的更改都反映在自动生成的代码中,保持代码的一致性。
#!/usr/bin/env bash
set -o errexit
set -o nounset
set -o pipefail
OUTPUT_PKG=generated
MODULE=foo
SCRIPT_ROOT=$(dirname "${BASH_SOURCE[0]}")/..
DIFFROOT="${SCRIPT_ROOT}/${OUTPUT_PKG}"
TMP_DIFFROOT="${SCRIPT_ROOT}/_tmp/${OUTPUT_PKG}"
_tmp="${SCRIPT_ROOT}/_tmp"
cleanup() {
rm -rf "${_tmp}"
}
trap "cleanup" EXIT SIGINT
cleanup
mkdir -p "${TMP_DIFFROOT}"
cp -a "${DIFFROOT}"/* "${TMP_DIFFROOT}"
"${SCRIPT_ROOT}/hack/update-codegen.sh"
echo "copying generated ${SCRIPT_ROOT}/${MODULE}/${OUTPUT_PKG} to ${DIFFROOT}"
cp -r "${SCRIPT_ROOT}/${MODULE}/${OUTPUT_PKG}"/* "${DIFFROOT}"
echo "diffing ${DIFFROOT} against freshly generated codegen"
ret=0
diff -Naupr "${DIFFROOT}" "${TMP_DIFFROOT}" || ret=$?
cp -a "${TMP_DIFFROOT}"/* "${DIFFROOT}"
if [[ $ret -eq 0 ]]
then
echo "${DIFFROOT} up to date."
else
echo "${DIFFROOT} is out of date. Please run hack/update-codegen.sh"
exit 1
fi
生成代码
go mod tidy
go mod vendor
chmod -R 777 hack
# vendor目录中没有code-generator目录,因为k8s.io/code-generator这个依赖在项目中并没有真正被引用过,所以使用go mod vendor是无法将这个依赖更新到vendor中。可以选择手动拷贝,注意修改对应目录。也可以用tools.go来手动导入这个包。
go env | grep GOMODCACHE
cd $GOMODCACHE/k8s.io
cp -r code-generator@v0.24.16 foo-controller/vendor/k8s.io/code-generator
chmod -R 777 vendor
cd hack
./update-codegen.sh
// +build tools
package tools
import _ "k8s.io/code-generator"
目录结构如下:
- client-gen: 生成了
pkg/generated/clientset
文件夹及其下属所有文件。用于连接apiserver,管理foos.foo.example.com
CRD资源。 - informer-gen:生成了
pkg/generated/informers
文件夹及其下属所有文件。用于接收来自服务器的CRD的变更事件。 - lister-gen:生成了
pkg/generated/listers
文件夹及其下属所有文件。用于提供只读的cache layer给GET和LIST请求访问。 - deepcopy-gen:生成了
pkg/apis/foo/v1/zz_generated.deepcopy.go
文件。包含深拷贝资源对象的方法。
测试
package main
import (
"context"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/client-go/util/homedir"
"fmt"
foov1 "foo-controller/pkg/apis/foo/v1"
fooclientset "foo-controller/pkg/generated/clientset/versioned"
fooinformers "foo-controller/pkg/generated/informers/externalversions"
"log"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func main() {
// 获取config
config, _ := clientcmd.BuildConfigFromFlags("", homedir.HomeDir()+"/.kube/config")
// 创建clientset
clientset, err := fooclientset.NewForConfig(config)
if err != nil {
log.Fatal(err)
}
// 获取foo
foo, err := clientset.FooV1().Foos("default").Get(context.TODO(), "foo", metav1.GetOptions{})
if err != nil {
log.Fatal(err)
}
fmt.Printf("foo name: %s,deployment name: %s,replicas: %d\n", foo.Name, foo.Spec.DeploymentName, foo.Spec.Replicas)
// 创建SharedInformerFactory
factory := fooinformers.NewSharedInformerFactory(clientset, 10)
fooInformer := factory.Foo().V1().Foos()
fooInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
foo := obj.(*foov1.Foo)
fmt.Printf("foo name: %s\n", foo.Name)
},
UpdateFunc: func(oldObj, newObj interface{}) {
foo := newObj.(*foov1.Foo)
fmt.Printf("foo name: %s\n", foo.Name)
},
DeleteFunc: func(obj interface{}) {
foo := obj.(*foov1.Foo)
fmt.Printf("foo name: %s\n", foo.Name)
},
})
// 启动informer
stopCh := make(chan struct{})
defer close(stopCh)
factory.Start(stopCh)
factory.WaitForCacheSync(stopCh)
// 从本地缓存中获取foo
fooLister := fooInformer.Lister()
foos, err := fooLister.List(labels.Everything())
if err != nil {
log.Fatal(err)
}
for _, foo := range foos {
fmt.Printf("foo name: %s\n", foo.Name)
}
}
build时需要先执行go mod tidy和go mod vendor
。由于没有创建crd foos.foo.example.com
,所以这里获取不到。而crd可以通过kubeilder
来生成。下篇文章介绍下如何把kubeilder
和code-generator
结合使用。