• 目的

  • CRD资源定义

  • 代码生成

  • Controller编写

目的

Custom Resource是扩展Kubernetes的一种方式(另外一种就是通过聚合层API apiserver-aggregation),而controller对指定的resource进行监听和执行对应的动作(watch,diff,action)。

Operator与Controller区别

  • 所有的Operator都是用了Controller模式,但并不是所有Controller都是Operator。只有当它满足: controller模式 + API扩展 + 专注于某个App/中间件时,才是一个Operator。

  • Operator就是使用CRD实现的定制化的Controller. 它与内置K8S Controller遵循同样的运行模式(比如 watch, diff, action)

  • Operator是特定领域的Controller实现

讨论两者区别:https://github.com/kubeflow/tf-operator/issues/300

所以先学习如何构建出一些自定义的Controller肯定是之后实现Operator的基础。

实现一个自定义的Controller由两部分组成:CRD和Controller逻辑代码
这里以sample-controller的代码为例,同时我们自己写的Controller也可以参考这个代码结构。

CRD资源定义

以sample-controler中的为例,我们需要创建的一个 Foo 如下example-foo.yaml:
创建该 Foo 自定义资源后,期望创建出一个名称为 example-foo ,副本数为 1 的deployment。

apiVersion: samplecontroller.k8s.io/v1alpha1
kind: Foo
metadata:name: example-foo
spec:deploymentName: example-fooreplicas: 1

它的CRD定义如下:

apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:name: foos.samplecontroller.k8s.io
spec:group: samplecontroller.k8s.ioversion: v1alpha1 #版本names:kind: Foo # kind类型plural: foos # API中使用的名称:/apis/<group>/<version>/<plural>scope: Namespaced # Namespaced/Cluster,表示该CRD是命令空间属性还是集群属性validation: # 对参数进行验证,应用openAPIV3Schema规则openAPIV3Schema: properties:spec:properties:# 定义了一个 replicas 字段,类型为integer ,并且在1-10的范围内replicas:        type: integerminimum: 1maximum: 10

更多关于crd定义规则可以参考官方文档:
https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions/

当把这个crd资源apply到集群中后,我们可以通过 kubectl get apiservice v1alpha1.samplecontroller.k8s.io -o yaml 命令看到注册这个 apiservice

代码编写

只需要将我们 Foo resource相关的struct,其余的类似自定义资源的 informers , listers , clientset 以及 deepcopy 的代码都可以通过工具code-generator自动生成。
以及编写我们自定义Controller的业务逻辑代码就好了

struct资源定义

type.go

package v1alpha1import (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
// FooSpec 定义
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"`
}

其中类似 +k8s: 的注释是代码生成来识别的。

register.go 中将 Foo , FooList 注册进入 scheme 中。

代码生成

代码生成能帮我处理大部分重复代码,主要通过 https://github.com/kubernetes/code-generator 这个包进行解析tag并生成。

全局tag

必须在目标包的doc.go文件中声明,典型路径是 pkg/apis///doc.go。
内容示例:

// 为包中任何类型生成深拷贝方法,可以在局部tag覆盖此默认行为
// +groupName=example.com
// +k8s:deepcopy-gen=package
// +groupName=samplecontroller.k8s.io  // groupName指定API组的全限定名// Package v1alpha1 is the v1alpha1 version of the API.
package v1alpha1 // import "k8s.io/sample-controller/pkg/apis/samplecontroller/v1alpha1"

局部tag

  • +genclient: 为这个 package 创建 client。

  • +genclient:noStatus: 当创建 client 时,不存储 status。

  • +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object: 为结构体生成 deepcopy 的代码,实现了 runtime.Object 的 Interface。

代码生成:

通过./hack/update-codegen.sh方法可以生成,client以及deepcopy代码。
包含:

sample-controller/pkg/apis/samplecontroller/v1alpha1/zz_generated.deepcopy.go
sample-controller/pkg/generated/clientset
sample-controller/pkg/generated/informers
sample-controller/pkg/generated/informers

_前提:code-generator 已经在vendor中,执行 go mod vendor _

update-codegen.sh内容如下

bash "${CODEGEN_PKG}"/generate-groups.sh "deepcopy,client,informer,lister" \k8s.io/sample-controller/pkg/generated k8s.io/sample-controller/pkg/apis \samplecontroller:v1alpha1 \--output-base "$(dirname "${BASH_SOURCE[0]}")/../../.." \--go-header-file "${SCRIPT_ROOT}"/hack/boilerplate.go.txt

完整使用说明:

Usage: generate-groups.sh <generators> <output-package> <apis-package> <groups-versions> ...<generators>        the generators comma separated to run (deepcopy,defaulter,client,lister,informer) or "all".<output-package>    the output package name (e.g. github.com/example/project/pkg/generated).<apis-package>      the external types dir (e.g. github.com/example/api or github.com/example/project/pkg/apis).<groups-versions>   the groups and their versions in the format "groupA:v1,v2 groupB:v1 groupC:v2", relativeto <api-package>....                 arbitrary flags passed to all generator binaries.Examples:generate-groups.sh all             github.com/example/project/pkg/client github.com/example/project/pkg/apis "foo:v1 bar:v1alpha1,v1beta1"generate-groups.sh deepcopy,client github.com/example/project/pkg/client github.com/example/project/pkg/apis "foo:v1 bar:v1alpha1,v1beta1"

interface{}处理

场景:如果我们需要一个通用的类型的object,如下:

  validation:openAPIV3Schema:properties:spec:properties:fields:type: object

我们在spec里面定义了一个fields字段,类型是object(即key ,value的形式),value的值可能是int也可能是string或者bool
在type定义的时候我是这么写的,定义为map[string]interface{}

// FooSpec is the spec for a Foo resource
type FooSpec struct {Fields map[string]interface{} `json:"fields"`
}

当在代码生成的时候,发现会报错:

Generating deepcopy funcs
F0518 17:53:33.568567   39986 deepcopy.go:750] DeepCopy of "interface{}" is unsupported. Instead, use named interfaces with DeepCopy<named-interface> as one of the methods.
goroutine 1 [running]:
k8s.io/klog/v2.stacks(0xc000132001, 0xc0002e9000, 0xad, 0xfd)/Users/silenceper/workspace/golang/pkg/mod/k8s.io/klog/v2@v2.0.0/klog.go:972 +0xb8......

遇到这种问题,需要自己实现深拷贝,例如这种:

type HelmReleaseSpec struct {HelmValues    `json:",inline"`
}// +k8s:deepcopy-gen=false
type HelmValues struct {helm.Values `json:"values,omitempty"`
}// helm.Values定义:
type Values map[string]interface{}// 自己实现深拷贝
func (in *HelmValues) DeepCopyInto(out *HelmValues) {if in == nil {return}b, err := yaml.Marshal(in.Values)if err != nil {return}var values helm.Valueserr = yaml.Unmarshal(b, &values)if err != nil {return}out.Values = values
}

Controller编写

在编写Controller之前需要了解client-go中的informer机制:

  • 黄色的部分是controller相关的框架,包括workqueue。

  • 蓝色部分是client-go的相关内容,包括informer, reflector(其实就是informer的封装), indexer。

  • 从流程上看,reflector从apiserver中通过list&watch机制接收事件变化,进入Delta FIFO队列中,由informer进行处理。

  • informer会将delta FIFO队列中的事件交给indexer组件,indexer组件会将事件持久化存储在本地的缓存中。

  • 之后,由于用户事先将为informer注册各种事件的回调函数,这些回调函数将针对不同的组件做不同的处理。

  • 例如在controller中,将把object放入workqueue中,之后由controller的业务逻辑中进行处理。处理的时候将从缓存中获取object的引用。即各组件对资源的处理仅限于本地缓存中,直到update资源的时候才与apiserver交互。

简单来讲通过list/watch机器提供了本地缓存避免每次去请求apiserver。
并且提供了Event Handler方法,在将数据保存进入cache时,通过调用自定义handler方法,增加自定义处理。
所以Controller 的代码结构,就是如下:

func NewController(kubeclientset kubernetes.Interface,sampleclientset clientset.Interface,deploymentInformer appsinformers.DeploymentInformer,fooInformer informers.FooInformer) *Controller {....fooInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{AddFunc: controller.enqueueFoo,UpdateFunc: func(old, new interface{}) {controller.enqueueFoo(new)},})....
}
func (c *Controller) Run(threadiness int, stopCh <-chan struct{}) error {// Launch two workers to process Foo resourcesfor i := 0; i < threadiness; i++ {go wait.Until(c.runWorker, time.Second, stopCh)}
}
func (c *Controller) runWorker() {for c.processNextWorkItem() {}
}
func (c *Controller) processNextWorkItem() bool {obj, shutdown := c.workqueue.Get()...err := func(obj interface{}) error {...// Run the syncHandler, passing it the namespace/name string of the// Foo resource to be synced.if err := c.syncHandler(key); err != nil {// Put the item back on the workqueue to handle any transient errors.c.workqueue.AddRateLimited(key)return fmt.Errorf("error syncing '%s': %s, requeuing", key, err.Error())}...}(obj)...
}
func (c *Controller) syncHandler(key string) error {// Convert the namespace/name string into a distinct namespace and namenamespace, name, err := cache.SplitMetaNamespaceKey(key)if err != nil {utilruntime.HandleError(fmt.Errorf("invalid resource key: %s", key))return nil}//TODO 处理逻辑
}
func (c *Controller) enqueueFoo(obj interface{}) {...c.workqueue.Add(key)
}
  • 在NewController中通过fooInformer添加 AddEventHandler ,执行 enqueueFoo

  • enqueueFoo 中将获取的变更放入队列

  • 启动一个或多个work从队列中取数据,最终通过syncHandler进行业务判断

  • 在sample-controller中同时还监听了deployment,在 handleObject 判断是否归属 Foo Kind ,同时执行 enqueueFoo  入队。

具体代码判断参考controller.go

在main.go方法中调用如下:


exampleClient, err := clientset.NewForConfig(cfg)
if err != nil {klog.Fatalf("Error building example clientset: %s", err.Error())
}
kubeInformerFactory := kubeinformers.NewSharedInformerFactory(kubeClient, time.Second*30)
exampleInformerFactory := informers.NewSharedInformerFactory(exampleClient, time.Second*30)controller := NewController(kubeClient, exampleClient,kubeInformerFactory.Apps().V1().Deployments(),exampleInformerFactory.Samplecontroller().V1alpha1().Foos())// notice that there is no need to run Start methods in a separate goroutine. (i.e. go kubeInformerFactory.Start(stopCh)
// Start method is non-blocking and runs all registered informers in a dedicated goroutine.
kubeInformerFactory.Start(stopCh)
exampleInformerFactory.Start(stopCh)   //启动informer

就是调用通过coge-gen生成代码创建informer并启动,创建client。

思考:为什么要通过队列来控制数据的变化? 
我觉得用队列一方面是解耦,因为往往一个Controller里面可能要通过informer监听各类资源对象,通过队列借助了各个informer的依赖。另一方便可以通过不同类型的队列比如限速队列,延迟队列达到不同的并发控制。

总结

  • 先定义crd,再实现Controller逻辑

  • 可以通过code-generator生成informer,client,listers代码

  • 注意针对interface{}类型需要自己实现deepCopy方法

  • 实现一个Operator,集成更多更复杂的Controller的话,我们一般使用Operator框架,比如kubebuilder

参考

  • https://juejin.im/post/5de097bbf265da05d849ab53

  • https://blog.fatedier.com/2019/04/02/k8s-custom-controller/

  • https://blog.gmem.cc/crd

阅读原文体验更加,文中代码支持跳转

如何在K8S中创建一个自定义Controller?相关推荐

  1. PS网页设计教程XV——如何在Photoshop中创建一个充满活力的作品集的网页设计

    作为编码者,美工基础是偏弱的.我们可以参考一些成熟的网页PS教程,提高自身的设计能力.套用一句话,"熟读唐诗三百首,不会作诗也会吟". 本系列的教程来源于网上的PS教程,都是国外的 ...

  2. 如何在Joomla中创建一个漂亮的单页网站

    我们都知道一个好的网站对于建立企业信誉的重要性.但是,在拥有统一的单页网站和多页网站的选项之间进行选择可能会很棘手.特别是如果您是 Joomla 和 Web 开发的新手. 随着移动和社交媒体用户的增加 ...

  3. 如何在WordPress中创建一个人格测验

    想知道如何在WordPress中创建一个人格测验吗? 首先将展示如何使用一个名为Thrive Quiz Builder的WordPress插件来建立人格测验.该工具允许用户从测验中获得尽可能多的流量. ...

  4. 如何在idea中创建一个SpringBoot项目(超详细教学)

    如何在idea中创建一个SpringBoot项目 目录 环境准备 1.1打开IDEA,点击New新建一个项目 1.2 使用Spring Initializr方式构建Spring Boot项目 1.3 ...

  5. 如何在Java中创建一个新的List

    本文翻译自:How to make a new List in Java We create a Set as: 我们创建一个Set为: Set myset = new HashSet() How d ...

  6. 怎么在java中创建一个自定义的collector

    文章目录 简介 Collector介绍 自定义Collector 总结 怎么在java中创建一个自定义的collector 简介 在之前的java collectors文章里面,我们讲到了stream ...

  7. c++中的new_怎么在java中创建一个自定义的collector

    简介 在之前的java collectors文章里面,我们讲到了stream的collect方法可以调用Collectors里面的toList()或者toMap()方法,将结果转换为特定的集合类. 今 ...

  8. 如何在xshell中创建一个SSH隧道

    之前讲过如何使用SSH协议连接远程,本集Xshell教程小编将深入讲解如何在xshell中创建一个SSH隧道. 在开始之前,请确保远程主机上的SSH服务器已经启用了X11 Forwarding.设置完 ...

  9. PS网页设计教程V——如何在Photoshop中创建一个商业网站布局

    作为编码者,美工基础是偏弱的.我们可以参考一些成熟的网页PS教程,提高自身的设计能力.套用一句话,"熟读唐诗三百首,不会作诗也会吟". 本系列的教程来源于网上的PS教程,都是国外的 ...

最新文章

  1. maven编译时出现读取XXX时出错invalid LOC header (bad signature)
  2. MyBatis:参数传递 [转]
  3. python mysqldb cursor_python中MySQLdb模块用法实例
  4. 永恒传奇三服务器维护中,仙境传说ro3月3日5点-10点停服维护
  5. poj 3348(求凸包面积)
  6. k8s创建pod的步骤
  7. 华科计算机考研2022年分数线,2022年华中科技大学软件工程考研分数线、参考书、上岸前辈初复试经验...
  8. windows环境下python 虚拟环境的创建和使用(virtualenvwrapper)
  9. 边缘计算应用场景_云计算与边缘计算协同九大应用场景(2019年)发布(附PPT解读)...
  10. [渲染层错误] multipolyline.styles: 样式id_【译】关于 SPA,你需要掌握的 4 层
  11. 使用socket模块,多线程扫描指定ip的端口开放情况
  12. 商品分析是什么?该怎么做(入门版)
  13. 给工具箱中的控件添加图标
  14. Android 如何优化开屏广告?
  15. Appscan漏洞之Authentication Bypass Using HTTP Verb Tampering
  16. 帝国cms7.2密码修改
  17. 从 WWDC17 看苹果图形技术的革新
  18. linux 光盘刻录命令,在Linux操作系统中使用命令进行光盘刻录
  19. Java实现 LeetCode 564 寻找最近的回文数(今天要GG在这道题了 头晕+题难(((φ(◎ロ◎;)φ))))...
  20. c 语言 蒙特卡洛程序,蒙特卡洛步骤学习(一)

热门文章

  1. android rtsp server or clinet work success
  2. Docker使用docker compose创建 轻量级私人云端相册 实时管理个人相片 控制相册访问权限 Lychee 部署搭建
  3. TS流讲解--什么是ts流
  4. MZ深度解读SAP常见财务问题-02-账套在哪里?
  5. 安装Adams 2019遇到的坑
  6. MySQL-定时任务
  7. kubernetes 教程 笔记
  8. 基于JAVA毕业设计的超市管理系统
  9. 直线特征提取算法:IEPF(iterative end point fit)
  10. 两个词之间的依存语义关系_相互依存的网络之间的级联中断-2003年停电的5个教训