参考:

  • https://time.geekbang.org/column/article/42076 张磊 – 深入剖析kubernetes
  • https://github.com/kubernetes/sample-controller

这篇文章主要介绍一个operator的工作原理,以及如何自己用代码实现一个operator。


在kubernetes项目中,一个API对象在ETCD中的完整资源路径是由Group(API组)Version(API版本)Resource(API资源类型)组成

现在我要声明要创建一个 CronJob 对象,那么我的 YAML 文件的开始部分会这么写:

apiVersion: batch/v2alpha1
kind: CronJob
...

在这个yaml中,CronJob就是这个 API 对象的资源类型(Resource),batch就是它的组(Group),v2alpha1 就是它的版本(Version)


CRD

在Kubernetes v1.7 之后,出现了一个新的API插件机制:CRD。 CRD的全称是Custom Resource Definition。它指的就是,允许用户在 Kubernetes 中添加一个跟 Pod、Node 类似的、新的 API 资源类型,即:自定义 API 资源。
例如我们要添加一个名字为Nginx的API资源类:

apiVersion: samplecrd.k8s.io/v1
kind: Nginx
metadata:name: nginx-sample
spec:size: 3image: nginx:1.7.9ports:- name: httpport: 80targetPort: 80

从这个 yaml 可以看到,API 资源类型是 Nginx ,API 组是 samplecrd.k8s.io ,API 版本是 v1。上面的 YAML 文件,就是一个具体的自定义API资源实例,也叫 CR,这个 CR 就类似于我们创建的 deployment。samplecrd.k8s.io/v1 对应的就是 apps/v1 , Nginx 对应的就是 Deployment。我们之所以能创建 deployment 的前提是有 apps/v1 这个版本,或者其他能够识别 Deployment 资源类型的版本,那么我们要创建 Nginx 这个资源类型实例的时候,也需要认识这个 Nginx 资源类型,那么我们就需要通过 CRD 把这个 Resource 创建出来:

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:name: nginxes.samplecrd.k8s.io
spec:group: samplecrd.k8s.ioversion: v1names:kind: Nginxplural: nginxes

通过上面的 yaml 我们创建了一个 group 为 apiextensions.k8s.io ,版本为 v1 , resource 为 Nginx 的资源,也就是说我们可以使用下面这种形式创建资源了。

apiVersion: samplecrd.k8s.io/v1
kind: Nginx

自定义控制器(Controller)

那么像 Deployment 我们可以指定什么 metadata , spec, name, labels 等等,那这个 Nginx 我们可以使用什么字段呢?这时候我们就要写代码了。 从这里开始就属于写operator了,这里有两种办法, 一是通过 client-go 来写,第二个就是通过 operator-sdk 来写。 这里我们通过 client-go 来写,了解一下其中的原理, 第二种方法,我们之后再说。首先我们创建一个代码的框架。

$ tree $GOPATH/src/sample-custom-controller
.
├── controller.go
├── go.mod
├── main.go
└── pkg├── apis│   └── samplecontroller│       ├── register.go│       └── v1│           ├── docs.go│           ├── register.go│           └── types.go└── signals├── signal.go└── signal_posix.go

在 pkg/apis/sample-controller 目录下创建一个 register.go 的文件,用来防止后面要用到的全局变量

package samplecontrollerconst (GroupName = "samplecontroller.k8s.io"Version   = "v1"
)

然后在 pkg/apis/sample-controller/v1 下创建三个文件 docs.go, register.go, types.go ,我们看下 docs.go 文件:

// +k8s:deepcopy-gen=package
// +groupName=samplecontroller.k8s.io
package v1

在这个文件中,你会看到 +[=value]格式的注释,这就是 Kubernetes 进行代码生成要用的 Annotation 风格的注释。其中,+k8s:deepcopy-gen=package 意思是,请为整个 v1 包里的所有类型定义自动生成 DeepCopy 方法;而+groupName=samplecontroller.k8s.io,则定义了这个包对应的 API 组的名字。

下面看一下 types.go 文件,这个文件定义自定义资源的字段以及字段类型,我们可以在yaml使用什么字段。

// +k8s:deepcopy-gen=package
// +groupName=samplecontroller.k8s.io
package v1import (metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"corev1 "k8s.io/api/core/v1"
)type NginxSpec struct {Size int32 `json:"size"`Image string `json:"image"`Resources corev1.ResourceRequirements `json:"resources,omitempty"`Envs []corev1.EnvVar `json:"envs,omitempty"`Ports []corev1.ServicePort `json:"ports,omitempty"`
}type NginxStatus struct {AvailableReplicas int32 `json:"availablereplicas "`
}// +genclient
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type Nginx struct {metav1.TypeMeta `json:",inline"`metav1.ObjectMeta `json:"metadata,omitempty"`Spec   NginxSpec   `json:"spec,omitempty"`Status NginxStatus `json:"status,omitempty"`
}// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type NginxList struct {metav1.TypeMeta `json:",inline"`metav1.ListMeta `json:"metadata"`Items []Nginx `json:"items"`
}

+genclient的意思是为这个 API 资源类型生成对应的 Client 代码,+k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 意思是在生成 DeepCopy 的时候,实现 Kubernetes 提供的 runtime.Object 接口。否则,在某些版本的 Kubernetes 里,你的这个类型定义会出现编译错误。

下面看一下 register.go 文件:

func Kind(kind string) schema.GroupKind {return SchemeGroupVersion.WithKind(kind).GroupKind()
}func addKnowTypes(scheme *runtime.Scheme) error {scheme.AddKnownTypes(SchemeGroupVersion ,&Nginx{},&NginxList{},)metav1.AddToGroupVersion(scheme, SchemeGroupVersion )return nil
}func Resource(resource string) schema.GroupResource {return SchemeGroupVersion.WithResource(resource).GroupResource()
}

这个文件的主要功能就是让客户端知道 Nginx 资源类型的定义, 有了这个文件,kubernetes 在生成客户端的时候就知道 Nginx类型和 NginxList 类型了。

有了这些东西,我们就可以通过kubernetes 的代码生成工具为 Nginx 资源类型生成 clientset,informer 和 lister 代码了。

$ export CUSTOM_RESOURCE_NAME="samplecontroller"
$ export CUSTOM_RESOURCE_VERSION="v1"
$ export ROOT_PACKAGE="sample-custom-controller"
$ go get -u k8s.io/code-generator/...
$ /bin/bash $GOPATH/src/pkg/mod/k8s.io/code-generator@v0.18.6/generate-groups.sh all "$ROOT_PACKAGE/pkg/client" "$ROOT_PACKAGE/pkg/apis" "$CUSTOM_RESOURCE_NAME:$CUSTOM_RESOURCE_VERSION"

当最后一条命令报这个错误的时候:
Failed loading boilerplate: open $GOPATH/src/k8s.io/code-generator/hack/boilerplate.go.txt: no such file or directory
可以在 GOPATH/src/下创建一个k8s.io的目录,然后将GOPATH/src/ 下创建一个 k8s.io 的目录, 然后将GOPATH/src/下创建一个k8s.io的目录,然后将GOPATH/src/pkg/mod/k8s.io/code-generator@v0.x 目录拷贝为 $GOPATH/src/k8s.io/code-generator,然后在执行最后一条命令。

生成了代码之后,我们需要写自定义控制器了。自定义控制器简单来说就是定义对 Nginx 资源的操作,类似于 Controller 对 Deployment 的操作,我在创建一个 Deployment 的时候,会有什么操作,删除的时候需要做什么操作,以及更新等。自定义控制器主要包含 main函数,控制器定义和业务逻辑 三个部分。 main函数的主要作用就是初始化自定义控制器,然后启动它。下面看一下 main 函数定义:

func main() {flag.Parse()stopCh := signals.SetupSignalHandler()cfg, err := clientcmd.BuildConfigFromFlags(masterURL, kubeconfig)kubeClient, err := kubernetes.NewForConfig(cfg)nginxClient, err := clientset.NewForConfig(cfg)nginxInformerFactory := informers.NewSharedInformerFactory(nginxClient, time.Second * 30)controller := NewController(kubeClient, nginxClient, nginxInformerFactory.Samplecontroller().V1().Nginxes())go nginxInformerFactory.Start(stopCh)if err = controller.Run(2, stopCh); err != nil {glog.Fatalf("Error running controller: %s", err.Error())}
}func init() {flag.StringVar(&kubeconfig, "kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.")flag.StringVar(&masterURL, "master", "", "The address of the Kubernetes API server. Overrides any value in kubeconfig. Only required if out-of-cluster.")
}
  • 首先通过 apiserver 的url 和 kubeconfig 文件创建一个 kubernetes 的 client(kubeClient)和 Nginx 对象的 client(nginxClient)
  • 然后创建了一个 nginxInformerFactory ,并使用他生成一个 Nginx 对象的 Informer,传递给控制器
  • 最后main 函数启动上述的 Informer,然后执行 controller.Run,启动自定义控制器。

这个 Informer 是什么?
Informer,其实就是一个带有本地缓存和索引机制的、可以注册 EventHandler 的 client。
我们启动控制器,需要从 APIServer 里获取它 Nginx 对象,这个操作是是通过 Informer 库完成的。Informer 与 API 对象是一一对应的,这里传递给控制器的,就是一个 Nginx 对象的 Informer,如果要对 Deployment 进行操作, 那么就需要传递一个 Deployment 的 Informer,这个后面会讲怎么操作一个 Deployment。在创建 Nginx 这个 Informer 的时候,需要用到 nginxClient,因为通过 nginxClient 和 APIserver 建立的连接,但是维护这个连接的是 Reflector 包,Reflector 包通过 ListAndWatch 机制,来获取和监听这些 Nginx 对象实例的变化。


Informer 作用是什么?
Informer 作用主要是有两个:

  • 同步本地缓存:当 APIServer 端有新的实例被创建、删除或者更新, Reflector 都会收到事件通知,此时改事件以及它对应的 API 对象组合就会被放进一个队列中,这个队列叫做 Delta FIFO Queue。Informer 会不断从这个队列中读取元素,判断他的事件类型,然后创建或更新本地对象的缓存。
  • 触发 在 ResourceEventHandler 中注册的事件

下面看一下自定义控制器的代码:

func NewController(kubeclientset kubernetes.Interface,nginxclientset clientset.Interface,nginxInformer informer.NginxInformer,) *Controller {utilruntime.Must(nginxscheme.AddToScheme(scheme.Scheme))klog.Infof("Create Event broadcaster")eventBroadcaster := record.NewBroadcaster()eventBroadcaster.StartLogging(klog.Infof)eventBroadcaster.StartRecordingToSink(&typedcorev1.EventSinkImpl{Interface: kubeclientset.CoreV1().Events("")})recorder := eventBroadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{Component:controllerAgentName})controller := &Controller {kubeclientset: kubeclientset,nginxclientset: nginxclientset,nginxLister: nginxInformer.Lister(),nginxSynced: nginxInformer.Informer().HasSynced,workerqueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "Nginx"),recorder: recorder,}nginxInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{AddFunc: controller.enqueueNginx,UpdateFunc: func(old, new interface{}) {oldNginx := old.(*samplecontrollerv1.Nginx)newNginx := new.(*samplecontrollerv1.Nginx)if oldNginx.ResourceVersion == newNginx.ResourceVersion{return}controller.enqueueNginx(new)},DeleteFunc: controller.enqueueNginxForDelete,})return controller
}

我们在 main 函数中创建了两个 client(kubeClient 和 nginxClient),和一个 Informer , 这里我们初始化 controller 的时候会用到这三个对象。在自定义控制器中,还设置了一个队列。上面说到 APIServer 有新的 Nginx 实例被创建、删除或者更新的时候,会把事件和对象的一个组合放在一个队列中,说的就是这个队列,但是实际加入队列的不是API对象本身,而是他们的key(<namespace>/<name>)初始化 Controller 后,又给 nginxInformer 绑定了对应的事件,分别对应新增,更新和删除操作。

最后,也是图中的最后一个部分,控制循环,也就是 controller.Run() 启动的控制循环:

func (c *Controller) Run(threadiness int, stopCh <-chan struct{}) error {defer utilruntime.HandleCrash()defer c.workerqueue.ShutDown()if ok := cache.WaitForCacheSync(stopCh, c.nginxSynced); !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 启动无限循环的任务,而这个无限循环中的逻辑就是我们的业务逻辑。那我们来看一下我们的业务逻辑:

func (c *Controller) runWorker() {for c.processNextWorkItem() {}
}func (c *Controller) processNextWorkItem() bool {obj, shutdown := c.workerqueue.Get()...err := func(obj interface{}) error {...if key, ok = obj.(string); !ok {c.workerqueue.Forget(obj)return nil}if err := c.syncHandler(key); err != nil {c.workerqueue.AddRateLimited(key)return fmt.Errorf("error syncing '%s': %s, requeuing", key, err.Error())}c.workerqueue.Forget(obj)return nil}(obj)...
}func (c *Controller) syncHandler(key string) error {namespace, name, err := cache.SplitMetaNamespaceKey(key)if err != nil {utilruntime.HandleError(fmt.Errorf("invalid resource key: %s", key))return nil}nginx, err := c.nginxLister.Nginxes(namespace).Get(name)if err != nil {if errors.IsNotFound(err) {utilruntime.HandleError(fmt.Errorf("Nginx '%s' in work queue no longer exists", key))return nil}return err}c.recorder.Event(nginx, corev1.EventTypeNormal, SuccessSynced, MessageResourceSynced)// 这里补充你的业务逻辑// 例如你要创建一个deployment// 自定义控制器代码可以参考:https://github.com/kubernetes/sample-controller/blob/master/controller.go// 在sample中您可以看到controller中更详细的代码return nil
}

首先在队列中获取一个元素,也就是一个key,上面说了这个key并不是一个对象,而是 namespce/name 的一个组合,然后我们在 syncHandler 方法中通过 nginxLister 获取到对应的 Nginx 的对象。获取到 Nginx 对象后,Nginx 对象进行一些操作了。比如 Nginx 对象控制的是一个 deployment, 那我们就可以看这个deployment 是不是存在, 是不是期望的状态,如果不是则进行一些操作。 到此为止,一个简单的自定义控制器就完成了, 我们就可以直接运行了。

$ go run . -kubeconfig=/root/.kube/config -master='http://127.0.0.1:8080'

来,手写一个Operator (一)相关推荐

  1. vue手写一个计算器

    计算器大家都不陌生 有计算器机器 有手机计算器 网页计算器! 那么好 今天我来给大家手写一个计算器 啥都不说上操作 请听题:vue手写计算器 一个个小方块拼成一个计算器 绿色比较好 可以缓解视力哦 i ...

  2. 【干货】JDK动态代理的实现原理以及如何手写一个JDK动态代理

    动态代理 代理模式是设计模式中非常重要的一种类型,而设计模式又是编程中非常重要的知识点,特别是在业务系统的重构中,更是有举足轻重的地位.代理模式从类型上来说,可以分为静态代理和动态代理两种类型. 在解 ...

  3. vue @click 赋值_vue 手写一个时间选择器

    vue 手写一个时间选择器 最近研究了 DatePicker 的实现原理后做了一个 vue 的 DatePicker 组件,今天带大家一步一步实现 DatePicker 的 vue 组件. 原理 Da ...

  4. 肝一波 ~ 手写一个简易版的Mybatis,带你深入领略它的魅力!

    零.准备工作 <dependencies><dependency><groupId>mysql</groupId><artifactId>m ...

  5. 硬核!手写一个优先队列

    文章收录在首发公众号:bigsai 期待你的到访! 前言 事情还要从一个故事讲起: 对于上面那只可爱的小狗狗不会,本篇即为该教程,首先,我要告诉这只可爱的小狗狗,这种问题你要使用的数据结构为优先队列, ...

  6. ds查找—二叉树平衡因子_面试官让我手写一个平衡二叉树,我当时就笑了

    平衡二叉树对于初学者一直是一个比较复杂的知识点,因为其里面涉及到了大量的旋转操作.把大量的同学都给转晕了.这篇文章最主要的特点就是通过动画的形式演示.确保大家都能看懂.最后是手写一个平衡二叉树. 一. ...

  7. 俄罗斯小方块游戏html,通过h5的canvas手写一个俄罗斯方块小游戏

    开始自己手写一个好玩的俄罗斯方块吧,上变形,左右移动,下加速,空格瞬移等功能,无聊的时候学习下canvas,f12 修改分数,体验金手指的快乐吧 1.定义界面,和按钮 上 下 左 右 2.js部分 1 ...

  8. vue 使用fs_模仿vue-cli,手写一个脚手架

    vue-cli 在vue的开发的过程中,经常会使用到vue-cli脚手架工具去生成一个项目.在终端运行命令vue create hello-world后,就会有许多自动的脚本运行. 为什么会这样运行呢 ...

  9. 未能加载文件或程序集或它的某一个依赖项_手写一个miniwebpack

    前言 之前好友希望能介绍一下 webapck 相关的内容,所以最近花费了两个多月的准备,终于完成了 webapck 系列,它包括一下几部分: webapck 系列一:手写一个 JavaScript 打 ...

最新文章

  1. MySQL中的二进制类型
  2. Http请求报头设置(C#)
  3. linux vim自动换行,VIM 的自动换行及自动折行设置
  4. python语言属于哪一种语言_Python与Java:你应该学习哪种语言,他们有什么区别?...
  5. 【付出总有回报】广州广汕公路科目三路考通过!小结供大家参考
  6. js求两圆交点_Chart.js找到交点Point并绘制一个圆
  7. Primefaces,Hibernate和SpringRoo集成
  8. ANSI C: union
  9. python处理xls到csv文件
  10. 前缀无歧义编码(PFC)
  11. 从挣扎突破到英雄联盟!中国SaaS头部企业阵营渐显
  12. keil5破解失败【经验分享】
  13. cad添加自己线性_如何在CAD中添加自行创建的线型
  14. 华为自带计算机怎么算平方立方,智能家庭中心:华为荣耀立方体验评测
  15. 应用层下的人脸识别(三):人脸比对
  16. visio 连接线样式设置 如箭头线
  17. H5微信内部支付宝签约代扣
  18. 0055-在OpenCV环境下合成高动态范围图像(HDR)
  19. C语言实现输出九九乘法表
  20. UE4C++开发学习笔记(01)——创建一个能操控的Character

热门文章

  1. 含论文基于SSH超市进销存管理系统【数据库设计、源码、开题报告】
  2. 中国人民银行面试题目(经典题目2)
  3. java实现截图功能
  4. Javascript变量、作用域与内存
  5. MongoDB日常运维之用户管理
  6. 计算机专业学单片机有用吗,“智能”占领电脑时代,当今为什么要学习单片机?...
  7. Gym - 101606L Lizard Lounge——LIS
  8. 计算机一级office考试攻略,计算机一级考试MSOffice应试技巧
  9. Whitelabel Error Page 的原因分析
  10. win10 格式化 linux u盘,2.win10格式化磁盘和u盘