K8s Client代码自动生成

在Istio和Knative,以及众多中间件operator中都会经常看到有使用informer去watch自定义CRD资源,然后reconsile的代码。其实每一个controller/operator都是使用api-server的客户端在操作资源。对于k8s支持的核心资源类型可以直接引用client-go,对于CRD,当前主要有三种方案。

  • 使用原生k8s的code-generator生成client代码
  • 直接使用kubebuilder,生成controller框架的同时自动生成client代码
  • 使用coreos和redhat提供的operator framework(有点类似于第二种)

code-generator介绍

项目目录: k8s.io/code-generator

对于初学者来讲,后两种方式都比较容易上手,开发者可以将关注点集中在怎么实现controller的业务上面,而不用去关心client的实现。但是,考虑到经常阅读源代码,时常接触到一些高度一致的目录结构而一头雾水;同时,便于更高效的阅读k8s相关项目的角度,我们接下来重点聊下k8s原生code-generator生成client库的过程,以及对应的目录结构的内容。

代码生成

为啥要生成client库的代码?显然是因为我们定义了k8s core resource之外的CRD,那么在使用脚本自动生成cliet代码之前,很显然我们需要做一些对资源的定义(除了资源的定义之外,重点还有一系列的注释,这些注释有特定的语法,这里不做详述,具体可以参考后记中推荐的官方文档),这样脚本才可能生成我们所需的代码。接下来分两步演示:1. 预定义资源 2. 生成客户端代码

定义资源

准备controller项目路径,这一步需要考虑好controller的命名,CRD资源的版本号,这些信息需要用来命名目录。

1
2
3
4
5
# 创建目录 (在$GOPATH/k8s.io下执行命令)
mkdir -p project/pkg/apis/controller/v1

# 进入controller目录
cd project/pkg/apis/controller

这里需要预先按照对应的结构生成几个文件, 目录结构如图所示:

1
2
3
4
5
6
7
8
9
10
~/gopath/src/k8s.io/project  tree
.
└── pkg
└── apis
└── controller
├── register.go
└── v1
├── doc.go
├── register.go
└── types.go
  1. 最顶层的register.go定义了groupName;
  2. 在版本号目录下又有三个文件:
    1. doc.go定义了版本的包名;
    2. types.go定义资源的数据结构;
    3. register.go真正用于注册该版本下的资源。
  • 顶层register.go

    1
    2
    3
    4
    5
    6
    7
    8
    # 生成顶层register.go
    cat << EOF >> register.go
    package controller

    const (
    GroupName = "controller.ljchen.net"
    )
    EOF
  • doc.go
    注意,类似于下面的注释语句 // +k8s:deepcopy-gen=package// +groupName=controller.ljchen.net都是code-generator的语法,详情请参考 #后记 中推荐的文章。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # 进入v1目录,生成doc.go/types.go/register.go
    cd v1

    # 生成doc.go
    cat << EOF >> doc.go
    // +k8s:deepcopy-gen=package
    // +groupName=controller.ljchen.net

    // Package v1 is the v1 version of the API.
    package v1
    EOF
  • 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
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    # 生成types.go
    cat << EOF >> types.go
    package v1

    import (
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    )

    // +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"`
    }
    EOF
  • register.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
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    # 生成register.go
    cat << EOF >> register.go
    package v1

    import (
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/apimachinery/pkg/runtime"
    "k8s.io/apimachinery/pkg/runtime/schema"

    samplecontroller "k8s.io/project/pkg/apis/controller"
    )

    // SchemeGroupVersion is group version used to register these objects
    var SchemeGroupVersion = schema.GroupVersion{Group: samplecontroller.GroupName, Version: "v1"}

    // Kind takes an unqualified kind and returns back a Group qualified GroupKind
    func Kind(kind string) schema.GroupKind {
    return SchemeGroupVersion.WithKind(kind).GroupKind()
    }

    // Resource takes an unqualified resource and returns a Group qualified GroupResource
    func Resource(resource string) schema.GroupResource {
    return SchemeGroupVersion.WithResource(resource).GroupResource()
    }

    var (
    SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
    AddToScheme = SchemeBuilder.AddToScheme
    )

    // Adds the list of known types to Scheme.
    func addKnownTypes(scheme *runtime.Scheme) error {
    scheme.AddKnownTypes(SchemeGroupVersion,
    &Foo{},
    &FooList{},
    )
    metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
    return nil
    }
    EOF

生成客户端代码

自动生成client代码的脚本位于code-generator项目中,generate-groups.sh是对项目中多个二进制文件编排的整合。

脚本用法如下:

generate-groups.sh {generators} {output-package} {apis-package} {groups-versions} …

下面就使用以上脚本来自动生成刚才我们所写资源的client库。请千万注意这里的执行路径和参数的路径!

1
2
3
4
5
6
7
8
9
10
11
~$ pwd
~$ /gopath/src/k8s.io
~$ $GOPATH/src/k8s.io/code-generator/generate-groups.sh all \
k8s.io/project/pkg/generated \ # 注意路径
k8s.io/project/pkg/apis \
controller:v1

Generating deepcopy funcs
Generating clientset for controller:v1 at k8s.io/project/pkg/generated/clientset
Generating listers for controller:v1 at k8s.io/project/pkg/generated/listers
Generating informers for controller:v1 at k8s.io/project/pkg/generated/informers

生成后的目录结构如下所示:

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
39
40
41
42
43
44
45
46
47
48
49
50
 ~/gopath/src/k8s.io/project  tree
.
└── pkg
├── apis
│   └── controller
│   ├── register.go
│   └── v1
│   ├── doc.go
│   ├── register.go
│   ├── types.go
│   └── zz_generated.deepcopy.go
└── generated
├── clientset
│   └── versioned
│   ├── clientset.go
│   ├── doc.go
│   ├── fake
│   │   ├── clientset_generated.go
│   │   ├── doc.go
│   │   └── register.go
│   ├── scheme
│   │   ├── doc.go
│   │   └── register.go
│   └── typed
│   └── controller
│   └── v1
│   ├── controller_client.go
│   ├── doc.go
│   ├── fake
│   │   ├── doc.go
│   │   ├── fake_controller_client.go
│   │   └── fake_foo.go
│   ├── foo.go
│   └── generated_expansion.go
├── informers
│   └── internalversion
│   ├── factory.go
│   ├── generic.go
│   ├── internalinterfaces
│   │   └── factory_interfaces.go
│   └── v1
│   ├── interface.go
│   └── internalversion
│   ├── foo.go
│   └── interface.go
└── listers
└── v1
└── internalversion
├── expansion_generated.go
└── foo.go

后记

客户端代码生成后,接下来就是实现自己的controller代码了。这块可以这里就不再详述,具体可以参考k8s官方的sample-controller 代码。

code-generator有自己的语法,当然,如果你只是简单的要实现一个operator可以直接复制上面的代码来修改,否则请参考这里

另外,对于controller中的原理,这篇文章有讲的比较详细,可以参考。

0%