程序印象

Kubernets深入系列: 为自定义资源生成代码(译)

2018/04/04 Share

作者:STEFAN SCHIMANSKI OCTOBER 16, 2017

翻译:狄卫华

原文:Kubernetes Deep Dive: Code Generation for CustomResources

原文链接:https://blog.openshift.com/kubernetes-deep-dive-code-generation-customresources/


本系列翻译链接:


随着 Kubernetes 作为分布式应用程序的平台越广,越来越多的项目将使用其提供的扩展点功能,在更高层次上构建软件。 CustomResourceDefinitions(CRD)在Kubernetes 1.7中作为 alpha 版本引入并在1.8版中提升为beta 版本的,其对于许多用户场景来说是一个自然的构造组件,尤其是那些以某种方式实现controller(或有时称为operator)模式的构件。而且 CustomResourceDefinitions 很容易创建和使用。

从 Kubernetes 1.8 版本开始,CRD 在基于 golang 的项目中的使用也变得更加方便:通过用户提供的CustomResources,我们可以使用与 Kubernetes 项目或 OpenShift 中使用的相同代码生成(code-generation)工具。这篇文章展示了代码生成工具是如何工作的以及如何使用最少的代码行将它们应用到自己的项目中,从而为您生成 deepcopy 函数/typed clients/listers/informer,所有这些的生成仅需要一个 shell 脚本调用和部分代码注释。 openshift-evangelists/crd-code-generation 仓库提供了一个适合作为开始的完整项目。

为什么需要代码生成?

那些在 golang 语言使用 ThirdPartyResources 或 CustomResourceDefinition 的用户可能会感到惊讶,从Kubernetes 1.8 版本开始突然需要客户端代码生成。 更具体地说,client-go 需要 runtime.Object 类型(golang中的CustomResources必须实现runtime.Object接口)必须具有DeepCopy方法, 这里代码生成工具中的 deepcopy-gen 生成器可以满足这一需求,其可以在 k8s.io/code-generator 仓库中找到。

除了 deepcopy-gen 还有以下大多数使用者在 CustomResources 中使用的:

  • deepcopy-gen:为每种类型T生成方法: func (t* T) DeepCopy() *T
  • client-gen:为 CustomResource APIGroups 生成 typed clientsets
  • informer-gen:为 CustomResources 创建 informers,用来提供基于事件的影响接口,以方便对于服务端的 CustomResources 的变化进行对应的处理
  • lister-gen:为 CustomResources 创建 listers,用来提供对于 GET/List 请求提供只读的缓存层

最后两个是构建 controller(或operator)的基础。 在后续的博客文章中,我们将详细介绍 controller。 这四种代码生成器为构建构功能完备、生产环境使用的 controller 提供了强大的基础,使用的机制和软件包与Kubernetes上游 controllers 完全相同。

k8s.io/code-generator 中还提供了更多的生成器(generator)应用在其他的场景,比如如果构建自己的聚合API 服务器,则除了版本化(versioned)类型之外,还将使用内部类型。 Conversion-gen 将在这些内部和外部类型之间创建转换函数。 Defaulter-gen 将负责处理的某些默认的字段值。

在项目中使用代码生成器

所有的 Kubernetes 代码生成器都是在 k8s.io/gengo 的基础上实现的。他们共享一些常见的命令行标志。 基本上所有的生成器都会得到一个输入包列表(--input-dirs),它们按类型逐个输入并输出生成的代码。 生成的代码如下:

  • 或者像输入文件一样进入到相同的目录,例如 deepcopy-gen(使用--output-file-base “zz_generated.deepcopy” 来定义文件名)。
  • 或者生成一个或多个输出包(带--output-package),例如 client-,informer- 和 lister-gen- (通常生成为pkg/client)。

上面的描述可能听起来像使用的时候需要设置很长琐碎的命令行参数,但是这很幸运不是真的:k8s.io/code-generator 提供了一个 shell 脚本 generator-group.sh,它负责调用代码生成器以及 CustomResources 使用中的的所有特殊小要求。在项目中你所做编写一行命令,通常是在 hack/update-codegen.sh:

1
2
3
$ 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

脚本运行后大体会建立如下的包管理结构:

img

所有的 APIs 会被放在 pkg/apis 包内,clientsets/informers/listers 被创建在 pkg/client。换句话说,pkg/client 代码是被完全生成的,就像包含我们的 CustomResource golang语言类型的 types.go 文件下面的 zz_generated.deepcopy.go 文件一样。 两者都不应该手动修改,而是通过运行以下命令创建:

1
$ hack/update-codegen.sh

通常,还会有一个 hack/verify-codegen.sh 脚本,如果任何生成的文件不是最新的,它将以非零的返回码终止。 这对于放入CI 脚本非常有帮助:如果开发人员偶然修改了文件或文件过时,CI 会产生告警并通知。

通过 Tags 控制生成的代码

虽然代码生成器的某些行为是通过上述的命令行标志(特别是要处理的包)控制的,但通过 go 文件中的标记可以控制更多的属性。

有两钟类型的 Tag:

  • 全局的 Tag 定义在 package 级别的 doc.go文件中
  • 局部的 Tag 定义在要处理的类型

通常标签的规则为// +tag-name// +tag-name=value,它们定义在注释中。 根据标签的不同,注释的位置可能很重要。 有些标签必须位于类型的正上方(或全局标签的package行),有些标签必须与类型(package行)分开,并且间隔至少有一条空行。 我们正在努力使得版本1.9 中(PR #53579 和 Issue #53893)其定义更加一致且不太容易出错。 间隔有空行可能很重要,能够更好地遵循 example 和遵守基本的规则。

全局 Tag

全局的 tag 定义在 包文件的 doc.go 文件中。一个典型的pkg/apis/<apigroup>/<version>/doc.go 定义看起来像下面这样:

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


// Package v1 is the v1 version of the API.
// +groupName=example.com
package v1

译者注:在当前版本 1.9 和 1.10 中, register 不再需要

注释表明 deepcopy-gen 默认为该包中的每个类型创建 deepcopy 方法。如果您有不需要 deepcopy 的类型,可以通过设置此类型局部 Tag // +k8s:deepcopy-gen=false 来实现。如果您不启用 package 范围内的 deepcopy 函数,则必须通过 // +k8s:deepcopy-gen=true 来设置需要的类型。

注意:上面示例的值中的 register 关键字将启用将 deepcopy 方法注册到 scheme 中。这将在Kubernetes 1.9中完全消失,因为 scheme 不再负责执行 runtime.Objects 的深层次的复制。取而代之只需调用 yourobject.DeepCopy()yourobject.DeepCopyObject()。你现在就可以在基于1.8 版本的项目中做到这一点,因为它更快,更不易出错。此外,你将准备1.9这将需要这种方式。

最后,// +groupName=example.com 定义完全限定的 API 组名称。如果填写错误,客户端会产生错误的代码。需要注意的是,这个 Tag 必须位于 package 上方的注释块中(参见 Issue #53893)。

局部 Tag

局部标签直接写在 API 类型的上面或写在它上面的第二个注释块中。 以下是关于 CustomResources 我们在 API Server 深入系列) 文章中定义的golang 类型 types.go 的样例:

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
// +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,也可以选择禁止。 但是样例中的类型都是 API 类型,需要使用 deepcopy。 因此我们不需要在此示例中的 types.go 中启用或禁止 deepcopy,而只能在 doc.go 中的package 设置。

RUNTIME.OBJECT 和 DEEPCOPYOBJECT

这个特殊的deepcopy tag 需要进一步的解释:

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

如果您尝试在基于Kubernetes 1.8的 client-go 上使用 CustomResources — 有些人可能已经很高兴了,因为他们意外地将 master 分支的k8s.op/apmachinery 放到了 vender 目录– 您遇到了 CustomResource 类型会出现的编译器错误:没有实现 runtime.Object,因为 DeepCopyObject() runtime.Object没有在你的类型上定义。 原因是在 1.8 版本中,runtime.Object 接口是用 这个方法签名 ) 进行了扩展,因此也是每个 runtime.Object 必须实现DeepCopyObjectDeepCopyObject() runtime.Object的实现是非常容易的:

1
2
3
4
5
6
7
func (in *T) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
} else {
return nil
}
}

幸运的是您不必为每种类型都实现此功能,只需将以下局部 tag 放在 顶级 API 类型的上方即可:

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

在我们上面的例子中,DatabaseDatabaseList 都是顶级类型,因为它们被用作 runtime.Objects。 有一个经验法则,顶级类型是那些具有 metav1.TypeMetaembedded嵌入的类型。 其他的类型,则是客户端为使用 client-gen 创建的类型。

需要注意的是,在定义具有某种接口类型字段的 API 类型(如字段SomeInterface)的情况下,// +k8s:deepcopy-gen:interfaces 仍然可以被使用。 此时 // +k8s:deepcopy-gen:interfaces=example.com/pk /apis/example.SomeInterface 将会生成一个 DeepCopySomeInterface() SomeInterface 方法。 这允许它以类型正确的方式对这些字段进行深度复制。

CLIENT-GEN TAGS

最后,有些标签来控制 client-gen,我们在例子中看到了两个标签:

1
2
// +genclient
// +genclient:noStatus

第一个 Tag 告诉 client-gen 创建一个这种类型的客户端(总是选择启用)。 请注意,您不必将它放在 API 对象的List 类型的上方

第二个 Tag 告诉 client-gen 这种类型不通过/status子资源路径控制 spec-status。 生成的客户端将没有UpdateStatus 方法(如果不设置该参数,只要在结构中找到Status字段,client-gen 就会无脑地生成UpdateStatus方法)。 /status 子资源尽在 1.8 版本的go语言实现中被支持。 但是由于PR 913中的 CustomResources 讨论了子资源,所以这可能会很快发生变化。

对于集群范围的资源,您必须使用 Tag:

1
// +genclient:nonNamespaced

对于特殊用途的客户端,您可能还需要详细控制客户端提供的 HTTP 方法。 这可以使用几个 Tag 完成,例如:

1
2
3
4
// +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

前三个应该是不言而喻的,但最后一个需要一些介绍, 上面代码中这个 Tag 的类型将是仅创建的,不会返回 API 类型本身,而是返回 metav1.Status。 对于 CustomResources 来说,这没什么意义,但对于用 golang 编写的用户提供的 API 服务器,这些资源可以存在,并且可以在实践中用到,例如在OpenShift API 中。

在主函数中使用类型化客户端

虽然基于Kubernetes 1.7及更早版本的大多数示例都使用 client-go dynamic client 管理 CustomResources,但本地 Kubernetes API 类型在很长一段时间内拥有更方便的类型化客户端。 这在 1.8 中有所改变:如上所述,client-gen 为您的自定义类型创建了一个原生的,功能全面且易于使用的类型化客户端。 实际上,client-gen 并不知道您是将它应用于 CustomResource 类型还是本地类型。

因此,使用这个客户端与使用客户端 Kubernetes 客户端完全等价。 这是一个非常简单的例子:

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
import (
...
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/tools/clientcmd"
examplecomclientset "github.com/openshift-evangelist/crd-code-generation/pkg/client/clientset/versioned"
)

var (
kuberconfig = flag.String("kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.")
master = flag.String("master", "", "The address of the Kubernetes API server. Overrides any value in kubeconfig. Only required if out-of-cluster.")
)

func main() {
flag.Parse()

cfg, err := clientcmd.BuildConfigFromFlags(*master, *kuberconfig)
if err != nil {
glog.Fatalf("Error building kubeconfig: %v", err)
}

exampleClient, err := examplecomclientset.NewForConfig(cfg)
if err != nil {
glog.Fatalf("Error building example clientset: %v", err)
}

list, err := exampleClient.ExampleV1().Databases("default").List(metav1.ListOptions{})
if err != nil {
glog.Fatalf("Error listing all databases: %v", err)
}

for _, db := range list.Items {
fmt.Printf("database %s with user %q\n", db.Name, db.Spec.User)
}
}

它可以与 kubeconfig 文件一起使用,实际上它可以与 kubectl 和 Kubernetes 客户端一起使用。

与使用动态客户端的传统 TPR 或 CustomResource 代码不同,您不必进行类型转换。 相反,实际的客户调用看起来完全是本地的,它是:

1
list, err := exampleClient.ExampleV1().Databases("default").List(metav1.ListOptions{})

结果是您的集群中所有数据库的示例中的一个 DatabaseList。 如果您将类型切换到集群范围(即不使用名称空间,请不要忘记告诉客户端使用 // +genclient:nonNamespaced tag),这些调用变成

1
list, err := exampleClient.ExampleV1().Databases().List(metav1.ListOptions{})

在 Golang 语言中创建 CustomResourceDefinition

由于这个问题经常出现,所以这里介绍一下如何通过 golang 代码编程创建一个CRD。

client-gen 总是创建所谓的客户端集(clientsets)。客户端集将一个或多个API组绑定到一个客户端内。通常这些API组来自一个仓库,并放置在一个基本package内,例如本博客文章中的 pkg/apis或 Kubernetes 的 k8s.io/api

CustomResourceDefinitions 由 kubernetes/apiextensions-apiserver 仓库提供。此 API server[备注:apiextensions-apiserver](也可以独立启动)可以由 kube-apiserver 嵌入,以便 CRD 在每个 Kubernetes 群集上都可用。创建 CRD 的客户端位于 apiextensions-apiserver 仓库中,当然也使用 client-gen。在阅读完这篇博客之后,你应该不会惊讶和意外在 kubernetes/apiextensions-apiserver/tree/master/pkg/client 仓库中找到创建的客户端。创建客户端实例以及如何创建CRD 代码样例如下:

1
2
3
4
5
6
7
8
import (
...
apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset”
)

apiextensionsClient, err := apiextensionsclientset.NewForConfig(cfg)
...
createdCRD, err := apiextensionsClient.ApiextensionsV1beta1().CustomResourceDefinitions().Create(yourCRD)

需要注意的是在创建完成之后,您必须等待新建立的 CRD 上的已建立Established。 只有这样,kube-apiserver 才会开始为资源服务。 如果您不等待处理这种情况,则在未就绪之前每个CRD 操作都将返回一个 404 HTTP 状态码。

进一步的资料

目前,Kubernetes generators 相关文档有很大的改进空间,任何形式的帮助都是非常受欢迎的。 它们从刚从 Kubernetes 项目中独立抽取到到 k8s.io/code-generator 中 以方便 CustomResource 用户公开使用。 随着时间的推移,这些文档肯定会有所改进,这篇博文也旨在为此做出贡献。

有关不同 generators 的更多信息,通常很好的方法是查看 Kubernetes内部的例子(例如 k8s.io/api),[OpenShift](https://github.com/openshift/origin(具有许多高级用例)以及 generators 自身代码:

本博客文章中的所有示例均可作为功能齐全的仓库提供,可以轻松地作为您自己实验的蓝图:


除特别声明本站文章均属原创(翻译内容除外),如需要转载请事先联系,转载需要注明作者原文链接地址。


CATALOG
  1. 1. 为什么需要代码生成?
  2. 2. 在项目中使用代码生成器
  3. 3. 通过 Tags 控制生成的代码
    1. 3.1. 全局 Tag
    2. 3.2. 局部 Tag
      1. 3.2.1. RUNTIME.OBJECT 和 DEEPCOPYOBJECT
      2. 3.2.2. CLIENT-GEN TAGS
  4. 4. 在主函数中使用类型化客户端
  5. 5. 在 Golang 语言中创建 CustomResourceDefinition
  6. 6. 进一步的资料