经过前两篇的学习与实操,也大致掌握了一个k8s资源的Controller写法了,如有不熟,可回顾
自己实现一个Controller——标准型
自己实现一个Controller——精简型
但是目前也只能对k8s现有资源再继续扩展controller,万一遇到了CRD呢,进过本篇的学习与实操,你就懂了。
先说说CRD-controller的作用,本CR原意是记录云主机ECS及node节点映射信息,而本controller则把这个映射操作省略掉,只为所有创建成功的CR打一个Label。而本篇为达成此目的,需要执行的步骤有以下几个:
往k8s创建一个CRD
定义对应CRD的api,包含了struct
给CRD的api注册到scheme
实现这个CRD的clinet,封装其请求apiserver的相关操作
实现一个Informer和Lister
实现一个controller
通过上述步骤,可以绕开ApiBuilder脚手架,自己手捏一个CRD-Controller出来。可以更好的理解整个Informer机制的架构
创建CRD的manifest如下所示
这里比较值得注意是ApiGroup需要定好,这个group到后续给scheme注册资源类型时用到,影响往apiserver去交互管理资源。
这个api可能容易给人造成误解,实际是定义CR的struct,包含什么字段,文件路径api/v1/ecs-bing.go
自上而下定义EcsBinding和EcsBindingList两个struct,由于要实现runtime.Object的接口,需要实现DeepCopyObject方法,如果用脚手架生成的代码,这部分实现接口的代码就不用手敲
scheme注册这里分两部分,一部分是定义一个scheme,另一部分是在各个api里面提供AddToScheme函数,这个函数用于把各种类型各种版本的api(也就是GVK)注册到scheme
先看第一部分,文件路径client/versiond/scheme/register.go
在AddToScheme中就是调用各个kind的AddToScheme,尽管这里只有一个Kind。第二部分的又回去api/v1/ecs-bing.go
此处的Group需要和之前定义CRD时的group一致
这里实际定义了一个clientSet,clientset应该包含多个version,一个version包含多个资源类型,但是这里只有一个version,一个kind。clientSet的结构如下所示
clientSet位于client/versiond/clientset.go
EcsV1位于client/versiond/typed/ecsbinding/v1/ecs_client.go中,它的RESTClient也用于传递给EcsClient,用于EcsClient对apiserver通讯的http客户端
EcsBindingClient位于client/version/typed/ecsbingding/v1/ecs-binding.go中,定义了client的各种操作方法,封装了对apiserver的各个http请求。
各个client的初始化,则是由最外层把Config一层一层的往里面具体的Client传。整套client的代码不在这里展示,仅展示一下调用链
当调用EcsV1的EcsBinding方法(也就是获取EcsClient)时,才调用newEcsbindings构造函数构造一个client
ecsbindv1.NewForConfig的代码如下:
在这个函数中先给config设置默认参数,最后按照这些默认参数构造出一个RESTClient,这个RESTClient传递给EcsV1Client,一个作用是把它自己的一个成员restClient,另一个作用就是用于构造EcsClient所需的RESTClient。
setConfigDefaults函数定义如下
函数给config指定了groudversion这个gv就是hopegi.com v1;api的地址固定是"/apis",通过这两句可以确定客户端跟apiserver通讯时的地址是/apis/hopegi.com/v1,后面设置scheme的序列化器,用于把apiserver响应的json数据反序列化成struct数据。
EcsBindingClient接口定义的函数如下
以List方法实现作例子
client成员则是先前构造时传入的RESTClient,Resource指定资源的名ecsbingding,当有CR返回时需要执行SetGroupVersionKind,否则拿到的CR结构体会丢失GroupVersion和Kind信息
在实现某个资源的Informer之前,要实现一个Informer的Factory。这个Factory的作用有几个,其一是用于构造一个Informer;另外就是在start一个Controller的时候调用它Start方法,Start方法内部就会把它管理的所有Informer Run起来。
SharedInformerFactory接口的定义如下所示,代码位于controller-demo/informers/factory.go
这里主要是暴露一个构造并获取各个Group的Interface,Start方法的接口则来源于它继承的internalinterfaces.SharedInformerFactory接口,代码位于 controller-demo/informers/internalinterface/factory_interfaces.go
除了Start方法,InformerFor跟构造一个Informer有关,实现Informer的时候会调用到factory的方法,后续会再介绍
V1的Interface则是涵盖了这个版本下各个资源的客户端接口,代码位于controller-demo/informers/ecsbind/v1/interface.go
这样也刚好跟k8s的api的层级相呼应,先是ApiGroup,再到Version,最后到Kind,就是GVK
一个Informer的最核心逻辑是List和Watch方法,不过我们实现一个Infomer时只需要给SharedIndexInformer提供这两个方法就可以,调用这两个方法的逻辑由SharedIndexInformer统一实现
实际上仅仅是调用了client而已,client则是来源于这个CR的Informer——EcsBindingInformer,看看它的接口定义和结构
对外暴露的EcsBindingInformer仅仅是一个接口,暴露Informer和LIster两个方法,实现则交由一个内部的结构实现,纵观这个CRD的client,CR的client,clientset,Informer乃至后续的lister都是这样的模式。
EcsBindingInformer的Informer()实现如下
如前面介绍Factory的时候所介绍的,Informer创建时需要调用factory的InformerFor方法,传入资源的指针以及一个函数回调
回调的声明在internalinterface处,controller-demo/informers/internalinterface/factory_interfaces.go
在这里就是ecsBindingInformer.defaultInformer,调用这个方法时就会把factory的client传递到构造SharedIndexInformer函数,这样List函数和Watch函数就有client使用,相当于整个构造过程是
创建一个client,将这个client传递给Factory
创建一个Informer时,会通过Factory经过GVK三个层次的接口调到对应资源的Informer,同时factory的实例也会经过每一级往下传递
调用Inform()方法获得SharedIndexInformer,依次经过EcsBindingInformer.Informer()-->d.defaultInformer(即:NewInformerFunc回调)-->NewFilteredEcsBindingInformer
EcsBindingInformer接口的另一个方法就是获取Lister,仅仅需要把SharedIndexInformer的Indexer传递过去则可,Lister的缓存机构已由SharedIndexInformer实现
作为apiserver的缓存,供controller调用快速获取资源,因此它需要提供两个查询的方法,代码位于controller-demo/listers/ecsbind/v1/ecs-binding-lister.go
controller所依赖的各个组件都已经实现完毕,现在可以实现这个crd的controller,完整的实现不展示,大致跟上一篇NodeController类似。仅展示他的字段和构造函数
最后把controller加到controller的Start方法中,统一启动
本篇虽然是说定义个CRD的controller,然而却把更多的篇幅放到的controller外的一些组件上:api,client,informer。但正事如此自己编码过一次,才会加深印象,后续在查看K8S源码时遇到controller的源码抠出其核心逻辑,通过client去翻查api地址,才会快速上手。本篇的目的也就如此。
client-go源码分析--informer机制流程分析
kubernetes client-go解析
深入浅出kubernetes之client-go的SharedInformer