"kubernetes Code-Generator"

  "kubernetes"

Posted by Xu on September 19, 2018

k8s代码自动生成过程的解析(Code-Generator).md

k8s越发展成为一个为分布式应用的平台,越多的项目将会在技术栈的更高的级别来使用提供的扩展点来构建软件。在k8s1.7中介绍的CRD作为初始版并逐渐提升为1.8的beta版,CRD是一个为原生构建的模块,可以解决很多用户场景的需求,尤其是那些实现了控制器模式的用户场景。此外,CRD非常容易创建和使用。

在K8S1.8中,在基于golang的项目中他们的使用更加频繁,自然。使用用户提供的自定义的资源,我们能够使用相同的code-generation工具而不是k8s自身的源码。这一篇blog就是介绍code-generator是如何工作和你的项目如何去运用它们,使得最少行数的代码,他会给你自动生成deepcopy函数,typed client,lister,informers,所有这些代码都适用一个脚本调用和一系列的代码注释生成。一个完整的作为实例的项目,可以参考repo

Code Generation - Why?

那些基于golang使用第三方资源或自定义资源CRD的用户可能会惊讶突然在k8s1.8中,client-go code-generation都是被要求提供的。更具体的是,client-go要求runtime.Object对象类型(基于Golang的CustomResource必须实现runtime.Object接口)必须有DeepCopy方法,于是,code-generation来扮演deepcopy-gen generator的角色,你可以在k8s.io/code-generator目录下找到该部分的代码。

接下来deepcopy-gen有一个手动的code-generators,大多数用户的自定义资源想要使用的。

  • deepcopy-gen :为每个类型T创建了一个函数 func(t* T)DeepCopy() * T
  • client-gen : 为每个自定义资源的apiGroup创建了typed clientsets
  • informer-gen : 为自定义资源创建一个informers,它会基于接口提供一个event事件来对服务器上的自定义资源的任何改动做出反应
  • lister-gen:为自定义资源创建listers,listers会提供一个只读的缓存层来相应GET和LIST请求

最后两个informer-gen和lister-gen是构建控制器的基础,接下来我们将深入了解controller控制器的更多细节,这四个code-generator提供一个强大的基础来构建拥有完整功能,产品就绪的控制器,和k8s主流控制使用相同的机制和包

在k8s.io/code-generator中为其它的环境提供来更多的代码生成器,例如,如果你想构建自己的aggragated API server,你将使用内置类型type和versioned types。Conversion-gen 将在这些内置和外部的types中创建conversions转换函数。

在项目中调用Code-Generators

所有的k8s code-generator在k8s.io/gengo中被实现。它们共享大量命令行参数。最基础的是,所有generators获取一个输入packages的列表(–input-dirs),generators将逐个遍历列表中的type,并且为他们输出产生的代码。产生的代码为:

  • 要么进入到和inut-dirs相同的目录下生成代码,如deepcopy-gen(携带参数with –output-file-base “zz_generated.deepcopy”来定义产生代码文件名)
  • 要么输出到一个或多个指定的输出packages当中(使用–output-package),如client-,informer-和lister-gen。

在k8s.io/code-generator有一个脚本generator-group.sh,该脚本做了所有调用generator所需的复杂的工作,你在项目需要做的只需要一行命令,通常在hack/update-codegen.sh:

$ 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

该命令运行项目会以如下package tree来安装,所有的API都期望在/pkg/apis目录下面并且clientsets,informers,和listers都在pkg/client中被创建。用另外的话说,pkg/client被完全创建,zz_generated.deepcopy.go文件紧接着types.go文件被创建,types.go包含我们自定义资源的golang类型,这两个文件都不支持手动修改,通过运行hack/update-codegen.sh创建

通常,接下来有一个hack/verify-codegen.sh脚本,如果有任何一个文件不是最新版本时,该脚本会以非零值返回结束。这非常有助于放入一个CI脚本:如果一个开发者偶然修改这些文件,或者这些文件是过时的,CI将会注意到并报告异常。

Contorlling 产生的代码-Tags

当code-generator的一些行为通过命令行参数像被上面描述的一样被控制时,许多属性都能通过golang文件中的tags来控制:

有两类tags:

  • 全局tags,在doc.go中
  • 局部tags,关于一个被处理的type

通常tags有形如//+tag-name或//+tag-name=value,这些tag会被写入注释当中,依赖于这些tags,注释的位置就非常重要。有大量的tags注释必须要紧挨着一个type的上方,(对于全局tags来说位于package所在行),其它的注释要和type分离隔开中间至少一个空行,在1.9版本中我们致力于使这些注释更加一致。下面是两个注释的例子:

Global Tags

Global tags被写入一个包package的doc.go文件中,典型的pkg/apis/< apigroup >/< version >/dock.go如下所示:

// +k8s:deepcopy-gen=package,register

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

这段注释告诉deepcopy-gen默认为该package中的每个类型创建deepcopy方法。如果你有一个types其deepcopy不是必要的或不被要求,你可以为这样的type添加一个局部的tag//+k8s:deepcopy-gen=false.如果你不想开启package范围的deepcopy,你需要为每个需要deepcopy的type添加注释//+k8s:deepcopy-gen=true.

注意:注释中的”register”关键词将启动deepcopy方法注册到scheme中,这在k8s1.9被完全舍弃,因为该scheme不再对runtime.Objects对象的deepcopy负责。你可以并应该在基于1.8版本的项目中这样做,因为这样更快,更不容易出错。

最后,注释//+groupName=example.com 定义整个API group的名称,如果你收到错误,client-gen将会生成错误代号。警告:该tag必须在紧挨着package上方的注释块。

Local Tags

局部tag要么被写入到紧挨着API type上方的注释块或第二个注释块。有一个例子types.go如下,用于自定义资源的序列化

// +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"`
}

我们可以看到我们默认开启了所有类型types的deepcopy,这些types,虽然都是API types并且需要deepcopy.因此我们不必在改例子将deepcopy打开或关闭,只需要在package范围doc.go中启用deepcopy即可

runtime.Object and DeepCopyObject

有一个特殊的deepcopy tag需要深入的介绍:

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

如果你尝试在client-go使用自定义资源,基于k8s1.8,一些人很高兴,因为他们已经在主流分支提供了/k8s.io/apimachiney-你可以确定你的自定义资源没有实现runtime.Object接口的编译错误,因为DeepCopyObject() runtime.Object并没有为你的type定义。原因是因为在1.8中runtime.Object接口被扩展with this method signature并且因此每个runtime.Object必须实现DeepCopyObject,接口DeepCopyObject() runtime.Object的实现是无用的

func (in *T) DeepCopyObject() runtime.Object {
    if c := in.DeepCopy(); c != nil {
        return c
    } else {
        return nil
    }
}

但幸运的是你不必为每个类型types实现该函数deepcopyobject(),只需要添加如下局部tag在你的API type的顶部:

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

在我们给出的例子中,Database和DatabaseList是top-level的类型,因为他们被当作runtime.Object使用,As a rule of thumb,根据经验,top-level的类型是那些有metav1.TypeMeta内嵌的类型。同时,那些也是客户端clients为使用client-gen创建的类型

注意:注释// +k8s:deepcopy-gen:interfaces tag可以并且应该当你定义一些拥有接口类型字段的API types时被使用,例如,字段SomeInterface.然后//+k8s:deepcopy-gen:interfaces=example.com/pkg/apis/example.SomeInterface将会导致DeepCopySomeInterface() SomeInterface函数的生成,这将允许用正确的方式它来deepcopy这些接口类型的字段

CLIENT-GEN TAGS

最后,有部分tags控制client-gen,其中有两个在我们的实例中被使用:

// +genclient
// +genclient:noStatus

第一个tag告诉client-gen为该类型创建一个client,第二个tag告诉client-gen该类型调用/status子资源时没有将spec-status分离。最后的client酱没有UpdateStatus方法(client-gen之遥在结构体中发现Status字段将尽可能块的产生该函数)。一个/status子资源只可能在1.8版本中实现。

对于集群范围的资源,必须使用tag//+genclient:nonNamespaced

对于特殊原因的clients,你可能想要在被客户端client提供的http方法上控制一些细节。这可以使用如下标签tag来实现,如:

// +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需要好好解释一下:拥有该tag的type将仅仅被创建并将不会返回API types自身,除了metav1.Status。对自定义资源,这不是很好理解,但对于用户提供的用golang写的API-server来说,这些资源可以存在,并且他们已经被实践了,例如OpenShift API.

使用types clients的主要函数

在k8s1.7或更老版本的为自定义资源使用cient-go 动态client(就是一些对该资源访问的一些客户端代码的逻辑实现)例子中,原生的API types一直都有一个更方便的typed client.这在1.8版本中被改变:上面描述的client-gen可以为”自定义资源”创建了一个原生的功能完备的并且容易使用的typed client.因此,使用这个client-gen客户端完全等同于使用client-go的k8s client。下面是一些非常简单的例子:

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和k8s client使用。

对比于老遗留版本TPR或带有动态client自定义资源代码,你不必有类型转换,取而代之的是,实际上的客户端调用看起来更加原生,如下:

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

返回的结果就是在你的集群中所有数据库的实例列表。如果你将你的type切换成集群范围(没有namespace;不要忘记使用//+genclient:noNamespaced tag告诉client-gen),该调用变为:

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

使用golang编程创建一个自定义资源定义

因为这个问题经常出现,简要介绍一下如何使用go语言编程来创建一个CRD。

client-gen总创建所谓的clientsets,Clientsets绑定一个或多个API Groups到一个client上,通常,这些API Groups来自于一个仓库repository并且和一个base package中,例如,你的/pkg/apis作为示例,或来自/k8s.io/api作为k8s的示例来定义这些API Groups

CRD被 kubernetes/apiextensions-apiserver repository 提供。这个API server(可以单独启动)内嵌了kube-apiserver,例如CRDS是可以在每个k8s集群中获取。但是创建CRDs的客户端client位于apiextensions-apiserver repository,当然也使用client-gen。读完这篇blog后,你不应该感到惊讶在 kubernetes/apiextensions-apiserver/tree/master/pkg/client发现client,也不应该感到出乎意料它看起来像创建了一个client实例并且如何创建CRD:

import (
    ...
    apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset”
)

apiextensionsClient, err := apiextensionsclientset.NewForConfig(cfg)
...
createdCRD, err := apiextensionsClient.ApiextensionsV1beta1().CustomResourceDefinitions().Create(yourCRD)//创建一个CRD

可以看到在这次创建后,你将不得不等待已经建立好的condition设置在新的CRD上。然后kube-apiserver才能开始服务该资源,如果你没有等到该condition设置好,则每个CR自定义资源的操作都会返回404HTTP 错误码

进一步相关材料

k8s generator的文档还有很大空间去完善,目前,任何形式的帮助都是欢迎的,这些文档从k8s项目中获取放到k8s.io/code-generator提供给CR用户共同参考。文档将会随着时间不断提升,更新

想要获取不同generator更进一步的信息,可以去查看一些k8s内部的示例,(例如,在k8s.io/api),在OpenShift内部,有很多高级的测试用例,及generator他们自身内部也有相关文档:

所有的这些blog中的例子都可以作为一个充分的函数repository取得,可以用作你自己实验的一些参考: