Kubernetes CRD 编写

参照官方样例项目 kubernetes/sample-controller 中的部分内容编写,此文仅仅为一个流程记录,部分文件中 import 部分的相关依赖根据项目不用的命名情况需要作出相应的修改。

编程环境:

  • Go v1.13.8
  • MacOS 10.15.8
  • Kubernetes v1.17.0

样例项目地址 schwarzeni/k8s-sample-controller-doc-demo

Project Setup

注意,由于后期使用 code-generator 时,code-generator 不支持 go module 模式,所以项目虽然依然可以使用 go module ,但是目录需要建成 GOPATH 的样式,例如如下

1
go mod init github.com/schwarzeni/k8s-sample-controller-doc-demo

此时项目 k8s-sample-controller-doc-demo 父文件夹路径应该为 github.com/schwarzeni/

安装相关的依赖,对于 1.17 版本的 Kubernetes ,下载 0.17.0 版本的相关客户端,下载 code-generator 时可能会报错,这个不用管

1
2
3
4
go get -u k8s.io/code-generator@v0.17.0
go get -u k8s.io/client-go@v0.17.0
go get -u k8s.io/apimachinery@v0.17.0
go get -u k8s.io/api@v0.17.0

建立 hack/tools.go 并写入如下内容,便于之后使用 go mod vendor

1
2
3
4
5
6
// +build tools

// This package imports things required by build scripts, to force `go mod` to see them as dependencies
package tools

import _ "k8s.io/code-generator"

之后执行命令:

1
go mod vendor

此时项目结构如下:

1
2
3
4
5
6
7
.
├── go.mod
├── go.sum
├── vendor
└── ...
├── hack
└── tools.go

文件 vendor/k8s.io/code-generator/generate-groups.sh 就是用来生成代码的脚本


Code Generation

首先先定义相关的 K8S 资源,然后使用 K8S 提供的工具生成相关的访问代码。这里的代码全部参照 kubernetes/sample-controller 中的相关代码。

在根目录下新建文件夹 pkg/apis/samplecontroller 作为定义资源的目录,在此目录下新建四个文件并填入相应的内容,IDE 可能会报错,这个不用管

1
2
3
4
5
6
pkg/apis/samplecontroller
├── register.go
└── v1alpha1
├── doc.go
├── register.go
└── types.go

register.go

1
2
3
4
package samplecontroller

// GroupName is the group name used in this package
const GroupName = "samplecontroller.k8s.io"

v1alpha1/doc.go

1
2
3
4
5
// +k8s:deepcopy-gen=package
// +groupName=samplecontroller.k8s.io

// Package v1alpha1 is the v1alpha1 version of the API.
package v1alpha1

v1alpha1/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
package v1alpha1

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

v1alpha1/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
package v1alpha1

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

samplecontroller "github.com/schwarzeni/k8s-sample-controller-doc-demo/pkg/apis/samplecontroller"
)

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

// 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 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
)

// 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
}

再执行一次 go mod vendor

为自动生成的文件准备一个 header

hack/boilerplate.go.txt

1
2
3
/*
this is a header file
*/

执行如下命令生成代码:(我在 Goland 的终端中执行此命令会报错,但是在普通终端中执行就没问题了,目前还不太清楚原因)

1
2
3
4
5
bash vendor/k8s.io/code-generator/generate-groups.sh all \
github.com/schwarzeni/k8s-sample-controller-doc-demo/pkg/generated github.com/schwarzeni/k8s-sample-controller-doc-demo/pkg/apis \
samplecontroller:v1alpha1 \
--go-header-file hack/boilerplate.go.txt \
--output-base /Users/nizhenyang/my-project/cloud/src

对于 --output-base ,假如你的项目绝对路径为 /Users/nizhenyang/my-project/cloud/src/github.com/schwarzeni/k8s-sample-controller-doc-demo ,将后半段 github.com/schwarzeni/k8s-sample-controller-doc-demo 视为 GOPATH,则这个参数的值就是前半段路径。

执行完代码之后,在 pkg/ 下会出现新生成的 generated 文件夹, 大致的结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pkg/generated
├── clientset
│   └── versioned
│   ├── clientset.go
│   ├── doc.go
│   ├── fake
│   ├── scheme
│   └── typed
├── informers
│   └── externalversions
│   ├── factory.go
│   ├── generic.go
│   ├── internalinterfaces
│   └── samplecontroller
└── listers
└── samplecontroller
└── v1alpha1

pkg/apis/samplecontroller/v1alpha1/ 下新生成了 zz_generated.deepcopy.go 文件。

生成代码任务完成,再执行一次 go mod vendor


Testing

这里写一个测试程序来验证我们生产代码的可用性,首先,根据自定义资源的格式准备两份 yaml 配置文件:

crd.yaml

1
2
3
4
5
6
7
8
9
10
11
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: foos.samplecontroller.k8s.io
spec:
group: samplecontroller.k8s.io
version: v1alpha1
names:
kind: Foo
plural: foos
scope: Namespaced

example-foo.yaml

1
2
3
4
5
6
7
apiVersion: samplecontroller.k8s.io/v1alpha1
kind: Foo
metadata:
name: example-foo
spec:
deploymentName: example-foo
replicas: 1

执行命令 kubectl apply -f crd.yaml 创建资源定义

删除 vendor 目录,在项目根目录新建 main.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
package main

import (
"log"
"time"

"github.com/schwarzeni/k8s-sample-controller-doc-demo/pkg/apis/samplecontroller/v1alpha1"
"k8s.io/client-go/tools/cache"

clientset "github.com/schwarzeni/k8s-sample-controller-doc-demo/pkg/generated/clientset/versioned"
informers "github.com/schwarzeni/k8s-sample-controller-doc-demo/pkg/generated/informers/externalversions"
"k8s.io/client-go/tools/clientcmd"
)

func main() {
// 你的Kubernetes配置文件路径,一般为 ~/.kube/config
config, err := clientcmd.BuildConfigFromFlags("", "config/config")
if err != nil {
panic(err)
}
exampleClient, err := clientset.NewForConfig(config)
sharedInformers := informers.NewSharedInformerFactory(exampleClient, time.Second*2)
informer := sharedInformers.Samplecontroller().V1alpha1().Foos().Informer()

informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
mObj := obj.(*v1alpha1.Foo)
log.Printf("New Foo Added: %s", mObj.GetName())
},
DeleteFunc: func(obj interface{}) {
mObj := obj.(*v1alpha1.Foo)
log.Printf("Delete Foo : %s", mObj.GetName())
},
})

stopCh := make(chan struct{})
defer close(stopCh)
informer.Run(stopCh)
}

执行该程序

执行 kubectl apply -f foo-example.yaml ,发现程序输出了 “New Foo Added: example-foo”

执行 kubectl delete -f foo-example.yaml ,发现程序输出了 “Delete Foo : example-foo”