HAOJX

kubernetes开发教程(4)--自动代码生成

字数统计: 2.2k阅读时长: 9 min
2019/09/10 Share

在此讲中, 我们介绍go的自动代码生成功能, 事实上kubernetes在很多地方都在用code generators这个功能, 我接下来会说如何使用code generators来编写custom resources

在kubernetes开发早期 , 随着越来越多的资源被加进系统, 越来越多的代码不得不被重写。 这个时候代码生成使得代码的维护更加容易 , 在早起是用Gengo library 之后在gengo的基础上, kubernetes发展出了k8s.io/code-generator 它是一个代码生成器的集合

使用代码生成器

通常在代码生成器的调用都是一样的方法, 只是group , version等不同而已, 使用k8s.io/code-generator/generate-groups.sh或者hack/update-codegen.sh能更加简单的向CR中添加代码生成

举个例子, 使用如下方法调用

1
2
3
4
5
6
$ vendor/k8s.io/code-generator/generate-groups.sh all \
github.com/programming-kubernetes/cnat/cnat-client-go/pkg/generated
github.com/programming-kubernetes/cnat/cnat-client-go/pkg/apis \
cnat:v1alpha1 \
--output-base "${GOPATH}/src" \
--go-header-file "hack/boilerplate.go.txt"

其中有4个标准的代码生成器

  • deepcopy-gen

    生成func (t *T) DeepCopy() *Tfunc (t *T) DeepCopyInto(*T)方法

  • client-gen

    创建类型化客户端集合(typed client sets)

  • informer-gen

    为CR创建一个informer , 当CR有变化的时候, 这个informer可以基于事件接口获取到信息变更

  • lister-gen

    为CR创建一个listers , 就是为GET and LIST请求提供read-only caching layer

后面2个是构建controller的基础

让我们先看看上述脚本后面跟的几个参数

  • 第二个参数是要生成的clients, listers, and informers的包名
  • 第三个参数是API group的包
  • 第四个参数是API 组及其版本
  • 将 –output-base 作为标志传递给所有生成器,以定义在其中找到给定包的基目录
  • – go-header-file 使我们能够将版权标头放入生成的代码中

有些生成器(如 deepcopy-gen)直接在 API group包中创建文件,这些文件遵循标准的命名方案,并生成一个 zz_generated.前缀,以便很容易从版本控制系统中排除它们

如果这个项目遵循了 k8s.io/sample-controllerーthe 的样本控制器模式,那么这个项目就是一个蓝图项目,它复制了内置在 Kubernetes 的许多控制器所建立的模式ーー那么代码生成就是这样开始的

1
$ hack/update-codegen.sh

用Tag控制生成器

虽然一些代码生成器行为是通过前面描述的命令行标志(特别是要处理的包)来控制的,但是更多的属性是通过 Go 文件中的标记来控制的

1
2
// +some-tag
// +some-other-tag=value

这里有2种形式的tag:

  • Global tags , 在doc.go上方进行标记
  • Local tags 声明在struct结构体上方

有许多标记必须直接位于类型(或全局标记的包行)之上的注释中,而其他标记必须与类型(或包行)分开,两者之间必须至少有一个空行。 例如

1
2
3
4
5
6
> // +second-comment-block-tag
>
> // +first-comment-block-tag
> type Foo struct {
> }
>

出现这种情况是历史原因, Kubernetes 的 API 文档生成器过去不知道代码生成标签,而只导出第一个注释块。 因此,该块中的标记将显示在 apihtml 文档中

global tags

global tags被写在doc.go中 , 典型的pkg/apis/group/version/doc.go文件长这样:

1
2
3
4
5
// +k8s:deepcopy-gen=package

// Package v1 is the v1alpha1 version of the API.
// +groupName=cnat.programming-kubernetes.info
package v1alpha1

该文件的第一行告诉 deepcopy-gen 默认情况下为该包中的每个类型创建深层复制方法。 如果您的类型不需要深度拷贝,那么您可以使用本地标签// +k8s:deepcopy-gen=false ,如果不启用包范围的深度拷贝,则必须通过 // +k8s:deepcopy-gen=true每个所需类型选择深度拷贝

第二个标记,// +groupName=example.com 定义了完全限定的 API 组名。 如果 Go 父包名称与组名称不匹配,则此标记是必需的。

使用// +groupName tag , client generator将使用正确的 HTTP 路径/apis/foo.project.example.com来生成客户机

除了+groupName 还有一个+groupGoName,它定义了一个定制的 Go 标识符(用于变量和类型名称)来代替父包名称 , 例如,在默认情况下,生成器将使用大写的父包名作为标识 , 这个时候你可以自定义标识

Local Tags

本地标记可以直接写在 API 类型之上,也可以写在 API 类型之上的第二个注释块中 , 比如下面的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// AtSpec defines the desired state of At
type AtSpec struct {
// Schedule is the desired time the command is supposed to be executed.
// Note: the format used here is UTC time https://www.utctime.net
Schedule string `json:"schedule,omitempty"`
// Command is the desired command (executed in a Bash shell) to be executed.
Command string `json:"command,omitempty"`
// Important: Run "make" to regenerate code after modifying this file
}

// AtStatus defines the observed state of At
type AtStatus struct {
// Phase represents the state of the schedule: until the command is executed
// it is PENDING, afterwards it is DONE.
Phase string `json:"phase,omitempty"`
// Important: Run "make" to regenerate code after modifying this file
}

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

// At runs a command at a given schedule.
type At struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

Spec AtSpec `json:"spec,omitempty"`
Status AtStatus `json:"status,omitempty"`
}

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

// AtList contains a list of At
type AtList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []At `json:"items"`
}

下面我们就将一一介绍这些标签

deepcopy-gen Tags

深度复制方法生成通常默认通过 Global// +k8s:deepcopy-gen=package tag 生成, 如果我们在 API 类型包中有一个 helper 结构,那么我们就必须禁用深度复制生成 , 比如

1
2
3
4
5
6
// +k8s:deepcopy-gen=false
//
// Helper is a helper struct, not an API type.
type Helper struct {
...
}

runtime.Object and DeepCopyObject

有一个特殊的深层复制标签需要多加说明下

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

之前我们就说 runtime.Object必须实现DeepCopyObject() runtime.Object方法,原因在于,Kubernetes 内部的通用代码必须能够创建对象的深层副本

Deepcopyobject ()方法只是调用生成的 DeepCopy 方法。 后者的签名因类型而异(DeepCopy () * t 取决于 t)。 前者的签名总是 DeepCopyObject ()运行时

1
2
3
4
5
6
7
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放在顶级 API 类型之上使用 deepcopy-gen 生成此方法,这告诉 deepcopy-gen 为运行时创建这样一个方法,叫DeepCopyObject()

顶级类型都会被用作runtime.Object ,顶级类型是那些内嵌metav1.TypeMeta 的类型

比如:如果 API 类型有一个接口类型 Foo 的字段 , 比如

1
2
3
type SomeAPIType struct {
Foo Foo `json:"foo"`
}

API 类型必须是深复制的,因此字段 Foo 也必须是深复制的。 如果不在 Foo 接口中添加 DeepCopyFoo () Foo,怎么能够以通用方式(不使用类型转换)实现这一点呢?

1
2
3
4
type Foo interface {
...
DeepCopyFoo() Foo
}

在这种情况下,可以使用相同的标签

1
2
3
4
// +k8s:deepcopy-gen:interfaces=<package>.Foo
type FooImplementation struct {
...
}

client-gen Tags

最后,还有一些控制 client-gen 的标签

1
// +genclient

它告诉 client-gen 为此类型创建一个客户机 , 但是他不能置于list类型之上 比如 自定义类型foo 就可以放在foo类型上 , 但是不能放在foolist之上 , 不清楚foo和foolist类型的, 请看官方的simple-controller项目的type.go文件

当然还有其他例子, 用法

比如

1
// +genclient:noStatus

如果没有这个标记,client-gen 将盲目地生成 UpdateStatus ()方法。 但是,只有在 resource custom definition 清单中实际启用了 / status 子资源才能不加

再比如

客户机生成器必须选择正确的 HTTP 路径,无论是否带有名称空间。 对于集群范围的资源,必须使用以下标记

1
// +genclient:nonNamespaced

默认是生成一个名称空间的客户机。 同样,这必须与 CRD 清单中的范围设置相匹配。 对于特殊用途的客户机,您可能还希望详细控制提供哪些 HTTP 方法。 你可以通过使用几个标签来做到这一点,例如

1
2
3
4
5
// +genclient:noVerbs
// +genclient:onlyVerbs=create,delete
// +genclient:skipVerbs=get,list,create,update,patch,delete,watch
// +genclient:method=Create,verb=create,
// result=k8s.io/apimachinery/pkg/apis/meta/v1.Status

对于// +genclient:method=标记,一个常见的情况是添加一个方法来缩放资源 , 如果CR启用 / Scale 子资源,下面的标签创建了相应的客户端方法

1
2
3
4
// +genclient:method=GetScale,verb=get,subresource=scale,\
// result=k8s.io/api/autoscaling/v1.Scale
// +genclient:method=UpdateScale,verb=update,subresource=scale,\
// input=k8s.io/api/autoscaling/v1.Scale,result=k8s.io/api/autoscaling/v1.Scale

第一个标签创建getter GetScale , 第二个创建setter UpdateScale

informer-gen and lister-gen

代码自动生成会生成对应类型的专门informer和lister的

CATALOG
  1. 1. 使用代码生成器
  2. 2. 用Tag控制生成器
    1. 2.1. global tags
    2. 2.2. Local Tags
      1. 2.2.1. deepcopy-gen Tags
      2. 2.2.2. runtime.Object and DeepCopyObject
      3. 2.2.3. client-gen Tags
      4. 2.2.4. informer-gen and lister-gen