Funky's NoteBook

Kubernetes's API

字数统计: 5,671阅读时长: 25 min
2019/04/17 Share

Kubernetes 声明式 API

Kubernetes 项目中,一个API 对象在 Etcd 里的完整资源路径由:Group(API 组)、Version(API 版本)和 Resource (API 资源类型)三部分组成。

API

通过该图可以清楚看见 kubernetes 里 API 组织方式是层层递进的。

1
2
3
apiVersion: batch/v2alpha1
kind: CronJob
...

当Group和完整的版本保证后,kind标识版本下的对象,此时APIServer 就可以创建这个对象了。

api-object

首先发起 创建 CronJob 的 Post 请求后,我们编写的 YAML 的信息就被提交给了 APIServer。而 APIServer 的第一个功能,就是过滤这个请求,并完成一些前置性工作,比如授权、超时处理、审计等。

然后,请求会进入 MUX 和 Routes 流程。MUX 和 Routes是 APIServer 完成 URL 和 Handler 绑定的场所,而 APIServer 的 Handler 用于匹配API组织,找到对应的CronJob的类型定义。

接着,APIServer 根据CronJob类型定义,使用用户提交的YAML文件里的字段,创建一个CronJob对象。

最后,APIServer 会把验证过的API对象转换为用户最初提交的版本,进行序列化操作,并调用 Etcd 的API 把它保存起来。

Kubernetes CRD 设计

假设创建一个 network 对象文件名为example-network.yaml,它的 YAML 文件类似为:

1
2
3
4
5
6
7
apiVersion: samplecrd.k8s.io/v1
kind: Network
metadata:
name: example-network
spec:
cidr: "192.168.0.0/16"
gateway: "192.168.0.1"

可以看出,描述网络的 API 资源类型是Network,API组是 samplecrd.k8s.io,API 版本是 v1。这就是一个CRD实例,也叫CR。

接下来我们编写一个CRD 的 YAML,命名为 network.yaml,内容如下所示:

1
2
3
4
5
6
7
8
9
10
11
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: networks.samplecrd.k8s.io
spec:
group: samplecrd.k8s.io
version: v1
names:
kind: Network
plural: networks
scope: Namespaced

这里指定了group:samplecrd.k8s.ioversion:v1这样的API信息,也指定了CR资源类型叫 Network,复数(plural)为:networks。

接着声明了它的 scope 是 Namespaced,也就是我们定义的这个Network属于Namespace对象,类似于Pod。

以上是一个Network API资源类型的API部分的宏观定义。

首先,构建项目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ tree $GOPATH/src/github.com/<your-name>/k8s-controller-custom-resource
.
├── controller.go
├── crd
│ └── network.yaml
├── example
│ └── example-network.yaml
├── main.go
└── pkg
└── apis
└── samplecrd
├── register.go
└── v1
├── doc.go
├── register.go
└── types.go
  • pkg/apis/samplecrd 就是 API 组的名字,v1是版本,而 v1 下面的 types.go 文件里,则定义了Networ对象的完整表述。

接着,在 pkg/apis/samplecrd 下创建一个名为 register.go 文件,用来放置后面用到的全局变量。

1
2
3
4
5
6
package samplecrd

const (
GroupName = "samplecrd.k8s.io"
Version = "v1"
)

然后,在 pkg/apis/samplecrd 目录创建一个 doc.go文件。内容如下:

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

// +groupName=samplecrd.k8s.io
package v1

<tag_name>+[=value] 格式的注释, 这就是 Kubernetes 进行代码生成要用的Annotation 风格的注释。

+k8s:deepcopy-gen=package 意思是,请为整个 v1 包里的所有类型定义自动生成 DeepCopy 方法;而+groupName=samplecrd.k8s.io,则定义了这个包对应的 API 组的名字。

可以看到,这些定义在 doc.go 文件的注释,起到的是全局的代码生成控制的作用,所以也被称为 Global Tags。

接下来,添加 types.go 文件,它的作用就是定义一个 Network 类型需要哪些字段(比如,spec里面的内容)。

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
package v1
...
// +genclient
// +genclient:noStatus
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

// Network describes a Network resource
type Network struct {
// TypeMeta is the metadata for the resource, like kind and apiversion
metav1.TypeMeta `json:",inline"`
// ObjectMeta contains the metadata for the particular object, including
// things like...
// - name
// - namespace
// - self link
// - labels
// - ... etc ...
metav1.ObjectMeta `json:"metadata,omitempty"`

Spec networkspec `json:"spec"`
}
// networkspec is the spec for a Network resource
type networkspec struct {
Cidr string `json:"cidr"`
Gateway string `json:"gateway"`
}

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

// NetworkList is a list of Network resources
type NetworkList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata"`

Items []Network `json:"items"`
}

而其中的 Spec 字段,就是需要我们自己定义的部分。所以,在 networkspec 里,我定义了 Cidr 和Gateway 两个字段。其中,每个字段最后面的部分比如json:”cidr”,指的就是这个字段被转换成 JSON 格式之后的名字,也就是 YAML 文件里的字段名字。

此外,还需要定义一个 NetworkList用于描述一组 Network对象需要包括哪些字段,因为 k8s 中,获取所有X对象的List()方法,返回都是List类型,而不是X类型的数组。

同样的,在Network和NetworkList类型上,也有代码生成注释。

其中+genclient的意思是为下面这个API资源类型生成对应的Client代码。而+genclient:noStatus的意思是这个API资源类型定义没有Status字段,否则生成的Client就会自动带上UpdateStatus方法。

如果你的定义类型包括了Status字段的话,就不需要这句+genclient:noStatus注释了,比如:

1
2
3
4
5
6
7
8
9
10
// +genclient

// Network is a specification for a Network resource
type Network struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

Spec NetworkSpec `json:"spec"`
Status NetworkStatus `json:"status"`
}

+genclient 只需要写在 Network 类型上,而不用写在 NetworkList 上。因为NetworkList 只是一个返回值类型,Network 才是“主类型”。

由于在Global Tags里已经定义了为所有类型生成DeepCopy方法,这里就不需要再显式加上 +k8s:deepcopy-gen=true ,当然你可以使用 +k8s:deepcopy-gen=false来阻止某些类型生成DeepCopy。

+k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object的注释。他的意思是生成DeepCopy的时候,实现k8s提供的runtime.Object接口。否则,在某些版本的k8s里这个类型定义会不嫌编译错误,只是一个固定操作,记住即可。

最后 ,再编写的一个 pkg/apis/samplecrd/v1/register.go 文件。

APIServer 工作原理的讲解中已经提到 registry 作用就是注册一个类型(Type)给 APIServer。其中,Network 资源类型在服务器端的注册工作,APIServer 会自动帮助我们完成,但是,客户端需要知道Network 资源类型的定义。这就需要我们在项目里添加一个 register.go 文件。它最主要的功能,就是定义了如下所示的addKnownTypes() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package v1
...
// addKnownTypes adds our types to the API scheme by registering
// Network and NetworkList
func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(
SchemeGroupVersion,
&Network{},
&NetworkList{},
)

// register the type in the scheme
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
return nil
}

有个这个方法,k8s就能够在生成客户端的时候,知道Network、NetworkList类型的定义了。

像上面这种 register.go文件里面的内容其实是非常固定的,以后可以直接使用上面提供的这部分代码做模版,然后把其中的资源类型、GroupName和Version替换成你自己的定义即可。

这样,Network对象定义的工作就全部完成了,它定义了两部分内容:

  • 自定义资源类型 API的描述包括组(Group)、版本(Version)、资源类型(Resource)等。
  • 自定义资源类型对象的描述,包括Spec、Status等。

接下来使用 k8s 提供的代码生成工具k8s.io/code-generator为上面的Network资源类型自动生成 clientset、informer和lister。其中,clientset 就是 操作Network 对象所需要使用的客户端。

1
2
3
4
5
6
7
8
9
10
11
12
13
# 代码生成的工作目录,也就是我们的项目路径
$ ROOT_PACKAGE="github.com/resouer/k8s-controller-custom-resource"
# API Group
$ CUSTOM_RESOURCE_NAME="samplecrd"
# API Version
$ CUSTOM_RESOURCE_VERSION="v1"

# 安装 k8s.io/code-generator
$ go get -u k8s.io/code-generator/...
$ cd $GOPATH/src/k8s.io/code-generator

# 执行代码自动生成,其中 pkg/client 是生成目标目录,pkg/apis 是类型定义目录
$ ./generate-groups.sh all "$ROOT_PACKAGE/pkg/client" "$ROOT_PACKAGE/pkg/apis" "$CUSTOM_RESOURCE_NAME:$CUSTOM_RESOURCE_VERSION"

执行完毕会生成以下的目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ tree
.
├── controller.go
├── crd
│ └── network.yaml
├── example
│ └── example-network.yaml
├── main.go
└── pkg
├── apis
│ └── samplecrd
│ ├── constants.go
│ └── v1
│ ├── doc.go
│ ├── register.go
│ ├── types.go
│ └── zz_generated.deepcopy.go
└── client
├── clientset
├── informers
└── listers

有了这些内容,现在你就可以在K8S集群里创建一个Network 对象的 CRD:

1
2
$ kubectl apply -f crd/network.yaml
customresourcedefinition.apiextensions.k8s.io/networks.samplecrd.k8s.io created

这个操作,就告诉了k8s,我现在要添加一个自定义的API对象,而这个类型的 API 信息,正是network.yaml 里定义的内容,可以通过命令查看crd:

1
2
3
$ kubectl get crd
NAME CREATED AT
networks.samplecrd.k8s.io 2018-09-15T10:57:12Z

然后我们就可以创建一个Network对象了,这里用到的是 example-network.yaml:

1
2
$ kubectl apply -f example/example-network.yaml 
network.samplecrd.k8s.io/example-network created

通过命令,k8s集群里面创建了一个Network对象,路径是samplecrd.k8s.io/v1/networks。

此时通过命令获取创建的 Network对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ kubectl get network
NAME AGE
example-network 8s
$ kubectl describe network example-network
Name: example-network
Namespace: default
Labels: <none>
...API Version: samplecrd.k8s.io/v1
Kind: Network
Metadata:
...
Generation: 1
Resource Version: 468239
...
Spec:
Cidr: 192.168.0.0/16
Gateway: 192.168.0.1

为 Network 这个自定义 API 对象编写一个自定义控制器

首先为这个控制器编写main函数

Main 函数的主要工作就是定义并初始化一个自定义控制器(Custom Controller),然后启动它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func main() {
...

cfg, err := clientcmd.BuildConfigFromFlags(masterURL, kubeconfig)
...
kubeClient, err := kubernetes.NewForConfig(cfg)
...
networkClient, err := clientset.NewForConfig(cfg)
...

networkInformerFactory := informers.NewSharedInformerFactory(networkClient, ...)

controller := NewController(kubeClient, networkClient,
networkInformerFactory.Samplecrd().V1().Networks())

go networkInformerFactory.Start(stopCh)

if err = controller.Run(2, stopCh); err != nil {
glog.Fatalf("Error running controller: %s", err.Error())
}
}

main 函数主要通过三步:

  • main 函数根据我们提供的master 配置(APIServer 的地址端口和kubeconfig 的路径),创建一个 k8s的client(kubeclient)和Network对象的 Client (networkClient)

main函数会直接使用 InClusterConfig 来创建 client。这个方式会假设你自定义的控制器是以Pod的方式运行在k8s集群里的,这个控制器会使用默认的serviceaccount数据卷里的授权信息来访问api-server。

  • main 函数 Network 对象创建一个 InformerFactory(即networkInformerFactory)的工厂,并使用它生成一个Network对象的Informer,传递给控制器。
  • main函数启动上述的Informer,然后执行controler.Run 启动自定义控制器。

工作原理:

controller

这个控制器要做的事是从k8s的apiserver里获取它关心的对象,也就是我们定义的Network对象。这个操作依靠的是一个叫做Informer(通知器)的代码库完成的。Informer与API对象是一一对应的,所以我们传递给自定义控制器的,正是一个Network对象的Informer,当我们在创建这个Informer的时候,需要给他传递一个networkClient。

事实上,Network Informer 正是使用这个 networkClient,跟APIServer 建立了连接,但是真正负责这个连接的则是Informer 所使用的Reflector包。Reflector 使用的是一种叫做ListAndWatch的方法来获取并监听这些Network对象实例的变化。

在ListAndWatch机制下,一旦APIServer端有新的Network实例被创建、删除或者更新,Reflector都会收到事件通知,此时,该事件与它对应的API对象这个组合就被成为增量(Delta),它会被放进一个Delta FIFO Queue(即:增量先进先出队列)中。

而另一方面Informer会不断从这个 Delta FIFO Queue 读取(Pop)增量。每拿到一个增量,Informer就好判断这个增量里的事件类型,然后创建或者更新本地对象的缓存。这个缓存在k8s里叫做 Store。

比如,事件类型是Added(添加对象),那么Informer 就会通过一个叫做 Indexer的库把这个增量里的API对象保存在本地缓存中,并把它创建索引。相反地,如果增量的事件类型是Deleted(删除对象),那么Informer就会从本地缓存中删除这个对象。

同步本地缓存的工作,是Informer的第一个职责,也是它最重要的职责。

而Informer的第二个职责是根据这些事件的类型,触发事先注册好的ResourceEventHander。这些Hander需要在创建控制器的时候注册给它对应的Informer。

下面编写控制器定义:

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
func NewController(
kubeclientset kubernetes.Interface,
networkclientset clientset.Interface,
networkInformer informers.NetworkInformer) *Controller {
...
controller := &Controller{
kubeclientset: kubeclientset,
networkclientset: networkclientset,
networksLister: networkInformer.Lister(),
networksSynced: networkInformer.Informer().HasSynced,
workqueue: workqueue.NewNamedRateLimitingQueue(..., "Networks"),
...
}
networkInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: controller.enqueueNetwork,
UpdateFunc: func(old, new interface{}) {
oldNetwork := old.(*samplecrdv1.Network)
newNetwork := new.(*samplecrdv1.Network)
if oldNetwork.ResourceVersion == newNetwork.ResourceVersion {
return
}
controller.enqueueNetwork(new)
},
DeleteFunc: controller.enqueueNetworkForDelete,
return controller
}

main 函数里创建了两个 client(kubeclientset 和 networkclient),这里使用者两个client和前面创建的Informer,初始化自定义控制器。这里还设置了一个工作队列(work queue),它处于示意图中间位置的WorkQueue。这个工作队列的作用是,负责同步Informer和控制循环直接的数据。

实际上,kubernetes项目为我们提供了很多个工作队列的实现,可以根据需要选择合适的库直接使用。

接着,为networkInformer注册了三个Handler(AddFunc、UpdateFunc和DeleteFunc),分别对应API对象的增加、更新、删除事件。而具体的处理操作都是将该事件对应的API对象加入到工作队列中。

实际入队的并不是API对象本身,而是他们的key,即:API对象的<namespace>/<name>。而我们后面即将编写的控制循环,则会不断的从这些工作队列里拿到这些key,然后开始执行真正的控制逻辑。

因此,所谓的Informer,其实就是一个带有本地缓存机制和索引机制的、可以注册EventHandler的client。它是自定义控制器跟APIServer进行数据同步的重要组件,Informer通过ListAndWatch的方法把APIServer中的API对象缓存在了本地,并负责更新和维护这个缓存。

其中,ListAndWatch方法的含义是首先通过APIServer的List API获取所有最新版本的API对象,然后通过WatchAPI 来监听所有API对象的变化。通过监听到的事件变化,Informer就可以实时更新本地缓存,并且调用这些事件对应的EventHandler。

此外在整个工程中,每经过resyncPeriod指定的时间,Informer维护的本地缓存,都会使用最近因此List返回的结果强制更新一次,从而保证缓存的有效性。在k8s中,这个缓存强制更新的操作叫做resync。该操作会触发Informer注册的更新事件。但此时这个更新事件对应的Network对象实际上并没有发生变化,也就是新旧两个Network对象的ResourceVersion是一样的。在这种情况下Informer就不需要对这个更新事件再做进一步的处理了。

也就是为什么在上面的UpdateFunc方法里,先判断了新旧两个Network对象版本(ResourceVersion)是否发生变化,然后才开始进行入队操作。

接下来到了示意图的控制循环(Control Loop)部分,也正是main函数最后调用control.Run()启动的控制循环

以下是控制循环的主要内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func (c *Controller) Run(threadiness int, stopCh <-chan struct{}) error {
...
if ok := cache.WaitForCacheSync(stopCh, c.networksSynced); !ok {
return fmt.Errorf("failed to wait for caches to sync")
}

...
for i := 0; i < threadiness; i++ {
go wait.Until(c.runWorker, time.Second, stopCh)
}

...
return nil
}
  • 首先,等待Informer完成一次本地缓存的数据同步操作
  • 接着,直接通过goroutine启动一个或并发多个无限循环任务

而这个无限循环任务每一个循环周期,执行的正是我们真正关心的业务逻辑。

下面编写自定义控制器的业务逻辑:

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
func (c *Controller) runWorker() {
for c.processNextWorkItem() {
}
}

func (c *Controller) processNextWorkItem() bool {
obj, shutdown := c.workqueue.Get()

...

err := func(obj interface{}) error {
...
if err := c.syncHandler(key); err != nil {
return fmt.Errorf("error syncing '%s': %s", key, err.Error())
}

c.workqueue.Forget(obj)
...
return nil
}(obj)

...

return true
}

func (c *Controller) syncHandler(key string) error {

namespace, name, err := cache.SplitMetaNamespaceKey(key)
...

network, err := c.networksLister.Networks(namespace).Get(name)
if err != nil {
if errors.IsNotFound(err) {
glog.Warningf("Network does not exist in local cache: %s/%s, will delete it from Neutron ...",
namespace, name)

glog.Warningf("Network: %s/%s does not exist in local cache, will delete it from Neutron ...",
namespace, name)

// FIX ME: call Neutron API to delete this network by name.
//
// neutron.Delete(namespace, name)

return nil
}
...

return err
}

glog.Infof("[Neutron] Try to process network: %#v ...", network)

// FIX ME: Do diff().
//
// actualNetwork, exists := neutron.Get(namespace, name)
//
// if !exists {
// neutron.Create(namespace, name)
// } else if !reflect.DeepEqual(actualNetwork, network) {
// neutron.Update(namespace, name)
// }

return nil
}

在这个执行周期里(processNextWorkItem),我们首先从工作队列里出队(workqueue.Get)了一个成员,也就是一个Key(Network对象的:namespace/name)。

然后在syncHandler方法中,使用这个key,尝试从 Informer 维护的缓存中拿到了它所对应的 Network 对象。

可以看到,在这里使用了networksLister来尝试获取这个 Key 对应的 Network 对象。这个操作其实就是在访问本地缓存的索引。实际上,在kubernetes的源码中,你会经常看到控制器从各种Lister里获取对象比如:podLister、nodeLister等,它们使用的都是Informer和缓存机制。

而如果控制循环从缓存中拿不到这个对象(即:networkList返回了IsNotFound错误),那么意味着这个Network对象的Key是通过签名的删除实践添加进工作队列的。所有尽管队列里有这个key,但是对应的Network对象已经被删除。

这个时候,需要调用Neutron的API,把这个对应的Neutron网络从真实的集群里删除掉。

如果能够获取到对应的Network对象,就可以知晓控制器模式里对比期望状态和实际状态的逻辑了。其中自定义控制器千辛万苦拿到的这个Network对象,正是APIServer里保存的期望状态,即用户通过YAML文件提交到APIServer里的信息。当然这个例子里它已经被缓存到了本地。

控制循环通过调用Neutron API 来查询实际的网络情况。

比如,通过Neurton来查询这个Network对象对应的真实网络是否存在。

  • 如果不存在,需要使用Network对象里的信息(如 CIDR、Gateway),调用Neutron API来创建真实的网络
  • 如果存在,那么读取这个真实的网络信息,判断它是否跟Network对象里的信息一致,从而决定是否要通过Neutron来更新这个存在的真实网络。

这样通过对比期望和实际状态的差异来完成一次调协(Reconcile)过程。

至此,一个完整的自定义API对象及其控制器编写完毕。

运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Clone repo
$ git clone https://github.com/resouer/k8s-controller-custom-resource
$ cd k8s-controller-custom-resource

### Skip this part if you don't want to build
# Install dependency
$ go get github.com/tools/godep
$ godep restore
# Build
$ go build -o samplecrd-controller .

$ ./samplecrd-controller -kubeconfig=$HOME/.kube/config -alsologtostderr=true
I0915 12:50:29.051349 27159 controller.go:84] Setting up event handlers
I0915 12:50:29.051615 27159 controller.go:113] Starting Network control loop
I0915 12:50:29.051630 27159 controller.go:116] Waiting for informer caches to sync
E0915 12:50:29.066745 27159 reflector.go:134] github.com/resouer/k8s-controller-custom-resource/pkg/client/informers/externalversions/factory.go:117: Failed to list *v1.Network: the server could not find the requested resource (get networks.samplecrd.k8s.io)
...

一开始报错是由于Network的CRD并没有被创建:

1
2
3
4
5
6
$ kubectl apply -f crd/network.yaml
...
I0915 12:50:29.051630 27159 controller.go:116] Waiting for informer caches to sync
...
I0915 12:52:54.346854 25245 controller.go:121] Starting workers
I0915 12:52:54.346914 25245 controller.go:127] Started workers

此时日志恢复正常,接下来实现Network的CRUD。

首先创建一个对象:

1
2
3
4
5
6
7
8
9
10
11
$ cat example/example-network.yaml 
apiVersion: samplecrd.k8s.io/v1
kind: Network
metadata:
name: example-network
spec:
cidr: "192.168.0.0/16"
gateway: "192.168.0.1"

$ kubectl apply -f example/example-network.yaml
network.samplecrd.k8s.io/example-network created

查看控制器的输出:

1
2
3
4
5
6
7
8
9
10
...
I0915 12:50:29.051349 27159 controller.go:84] Setting up event handlers
I0915 12:50:29.051615 27159 controller.go:113] Starting Network control loop
I0915 12:50:29.051630 27159 controller.go:116] Waiting for informer caches to sync
...
I0915 12:52:54.346854 25245 controller.go:121] Starting workers
I0915 12:52:54.346914 25245 controller.go:127] Started workers
I0915 12:53:18.064409 25245 controller.go:229] [Neutron] Try to process network: &v1.Network{TypeMeta:v1.TypeMeta{Kind:"", APIVersion:""}, ObjectMeta:v1.ObjectMeta{Name:"example-network", GenerateName:"", Namespace:"default", ... ResourceVersion:"479015", ... Spec:v1.NetworkSpec{Cidr:"192.168.0.0/16", Gateway:"192.168.0.1"}} ...
I0915 12:53:18.064650 25245 controller.go:183] Successfully synced 'default/example-network'
...

可以看到,通过创建Network的操作,触发了EventHandler的添加操作,从而被放进了工作队列。

接着,控制循环从队列里拿到这个对象,打印了正在处理这个Network对象的日志。

可以看到这个Network的ResourceVersion,也是API对象的版本号:479015,而Spec字段的内容和YAML内容一致。

此时修改这个YAML文件的内容:

1
2
3
4
5
6
7
8
$ cat example/example-network.yaml 
apiVersion: samplecrd.k8s.io/v1
kind: Network
metadata:
name: example-network
spec:
cidr: "192.168.1.0/16"
gateway: "192.168.1.1"

执行更新:

1
2
$ kubectl apply -f example/example-network.yaml 
network.samplecrd.k8s.io/example-network configured

观察输出:

1
2
3
...
I0915 12:53:51.126029 25245 controller.go:229] [Neutron] Try to process network: &v1.Network{TypeMeta:v1.TypeMeta{Kind:"", APIVersion:""}, ObjectMeta:v1.ObjectMeta{Name:"example-network", GenerateName:"", Namespace:"default", ... ResourceVersion:"479062", ... Spec:v1.NetworkSpec{Cidr:"192.168.1.0/16", Gateway:"192.168.1.1"}} ...
I0915 12:53:51.126348 25245 controller.go:183] Successfully synced 'default/example-network'

执行删除:

1
$ kubectl delete -f example/example-network.yaml

这次,控制器的输出力,可以看见Informer注册的删除事件被触发,并且控制循环调用 NeutronAPI 删除了真实环境的网络:

1
2
3
W0915 12:54:09.738464   25245 controller.go:212] Network: default/example-network does not exist in local cache, will delete it from Neutron ...
I0915 12:54:09.738832 25245 controller.go:215] [Neutron] Deleting network: default/example-network ...
I0915 12:54:09.738854 25245 controller.go:183] Successfully synced 'default/example-network'

以上就是自定义控制器全部流程,这套流程也完全可以用的k8s原生的默认API对象。

比如在main函数中,再初始化一个k8s默认API对象的Informer工厂如Deployment:

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
...

kubeInformerFactory := kubeinformers.NewSharedInformerFactory(kubeClient, time.Second*30)

controller := NewController(kubeClient, exampleClient,
kubeInformerFactory.Apps().V1().Deployments(),
networkInformerFactory.Samplecrd().V1().Networks())

go kubeInformerFactory.Start(stopCh)
...
}

这种方式就使得这个自定义控制器里面可以通过自定义API对象对默认API对象进行协同,实现更复杂的编排功能。比如创建一个新的Deployment,这个自定义控制器为它创建一个对应的Network供其使用。

CATALOG
  1. 1. Kubernetes 声明式 API
  2. 2. Kubernetes CRD 设计
    1. 2.1. 为 Network 这个自定义 API 对象编写一个自定义控制器