文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

生成CRD与自定义控制器的方法

2023-06-30 14:18

关注

这篇文章主要介绍“生成CRD与自定义控制器的方法”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“生成CRD与自定义控制器的方法”文章能帮助大家解决问题。

介绍

我们可以使用code-generator 以及controller-tools来进行代码自动生成,通过代码自动生成可以帮我们自动生成 CRD 资源对象,以及客户端访问的 ClientSet、Informer、Lister 等工具包,接下来我们就来了解下如何编写一个自定义的控制器。

CRD定义

首先初始化项目:

$ mkdir operator-crd && cd operator-crd$ go mod init operator-crd$ mkdir -p pkg/apis/example.com/v1

在该文件夹下新建doc.go文件,内容如下所示:

// +k8s:deepcopy-gen=package// +groupName=example.compackage v1

根据 CRD 的规范定义,这里我们定义的 group 为example.com,版本为v1,在顶部添加了一个代码自动生成的deepcopy-gen的 tag,为整个包中的类型生成深拷贝方法。

然后就是非常重要的资源对象的结构体定义,新建types.go文件,types.go内容可以使用type-scaffpld自动生成,具体文件内容如下:

package v1import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"// BarSpec defines the desired state of Bartype BarSpec struct {    // INSERT ADDITIONAL SPEC FIELDS -- desired state of cluster    DeploymentName string `json:"deploymentName"`    Image          string `json:"image"`    Replicas       *int32 `json:"replicas"`}// BarStatus defines the observed state of Bar.// It should always be reconstructable from the state of the cluster and/or outside world.type BarStatus struct {    // INSERT ADDITIONAL STATUS FIELDS -- observed state of cluster}// 下面这个一定不能少,少了的话不能生成 lister 和 informer// +genclient  // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object// Bar is the Schema for the bars API// +k8s:openapi-gen=truetype Bar struct {    metav1.TypeMeta   `json:",inline"`    metav1.ObjectMeta `json:"metadata,omitempty"`    Spec   BarSpec   `json:"spec,omitempty"`    Status BarStatus `json:"status,omitempty"`}// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object// BarList contains a list of Bartype BarList struct {    metav1.TypeMeta `json:",inline"`    metav1.ListMeta `json:"metadata,omitempty"`    Items           []Bar `json:"items"`}

然后可以参考系统内置的资源对象,还需要提供 AddToScheme 与 Resource 两个变量供 client 注册,新建 register.go 文件,内容如下所示:

package v1import (    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"    "k8s.io/apimachinery/pkg/runtime"    "k8s.io/apimachinery/pkg/runtime/schema")// SchemeGroupVersion 注册自己的自定义资源var SchemeGroupVersion = schema.GroupVersion{Group: "example.com", Version: "v1"}// Kind takes an unqualified kind and returns back a Group qualified GroupKindfunc Kind(kind string) schema.GroupKind {    return SchemeGroupVersion.WithKind(kind).GroupKind()}// Resource takes an unqualified resource and returns a Group qualified GroupResourcefunc 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 {    // 添加 Bar 与 BarList这两个资源到 scheme    scheme.AddKnownTypes(SchemeGroupVersion,        &Bar{},        &BarList{},    )    metav1.AddToGroupVersion(scheme, SchemeGroupVersion)    return nil}

使用controller-gen生成crd:

$ controller-gen crd paths=./... output:crd:dir=crd

生成example.com_bars.yaml文件如下所示:

---apiVersion: apiextensions.k8s.io/v1kind: CustomResourceDefinitionmetadata:  annotations:    controller-gen.kubebuilder.io/version: (devel)  creationTimestamp: null  name: bars.example.comspec:  group: example.com  names:    kind: Bar    listKind: BarList    plural: bars    singular: bar  scope: Namespaced  versions:  - name: v1    schema:      openAPIV3Schema:        description: Bar is the Schema for the bars API        properties:          apiVersion:            description: 'APIVersion defines the versioned schema of this representation              of an object. Servers should convert recognized schemas to the latest              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'            type: string          kind:            description: 'Kind is a string value representing the REST resource this              object represents. Servers may infer this from the endpoint the client              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'            type: string          metadata:            type: object          spec:            description: BarSpec defines the desired state of Bar            properties:              deploymentName:                description: INSERT ADDITIONAL SPEC FIELDS -- desired state of cluster                type: string              image:                type: string              replicas:                format: int32                type: integer            required:            - deploymentName            - image            - replicas            type: object          status:            description: BarStatus defines the observed state of Bar. It should always              be reconstructable from the state of the cluster and/or outside world.            type: object        type: object    served: true    storage: true

最终项目结构如下所示:

$ tree.├── crd│   └── example.com_bars.yaml├── go.mod├── go.sum└── pkg    └── apis        └── example.com            └── v1                ├── doc.go                ├── register.go                └── types.go5 directories, 6 files

生成客户端相关代码

上面我们准备好资源的 API 资源类型后,就可以使用开始生成 CRD 资源的客户端使用的相关代码了。

首先创建生成代码的脚本,下面这些脚本均来源于sample-controller提供的示例:

$ mkdir hack && cd hack

在该目录下面新建 tools.go 文件,添加 code-generator 依赖,因为在没有代码使用 code-generator 时,go module 默认不会为我们依赖此包。文件内容如下所示:

// +build tools// 建立 tools.go 来依赖 code-generator// 因为在没有代码使用 code-generator 时,go module 默认不会为我们依赖此包.package toolsimport _ "k8s.io/code-generator"

然后新建 update-codegen.sh 脚本,用来配置代码生成的脚本:

#!/usr/bin/env bashset -o errexitset -o nounsetset -o pipefailSCRIPT_ROOT=$(dirname "${BASH_SOURCE[0]}")/..CODEGEN_PKG=${CODEGEN_PKG:-$(cd "${SCRIPT_ROOT}"; ls -d -1 ./vendor/k8s.io/code-generator 2>/dev/null || echo ../code-generator)}bash "${CODEGEN_PKG}"/generate-groups.sh "deepcopy,client,informer,lister" \  operator-crd/pkg/client operator-crd/pkg/apis example.com:v1 \  --output-base "${SCRIPT_ROOT}"/../ \  --go-header-file "${SCRIPT_ROOT}"/hack/boilerplate.go.txt# To use your own boilerplate text append:#   --go-header-file "${SCRIPT_ROOT}"/hack/custom-boilerplate.go.txt

同样还有 verify-codegen.sh 脚本,用来校验生成的代码是否是最新的:

#!/usr/bin/env bashset -o errexitset -o nounsetset -o pipefailSCRIPT_ROOT=$(dirname "${BASH_SOURCE[0]}")/..DIFFROOT="${SCRIPT_ROOT}/pkg"TMP_DIFFROOT="${SCRIPT_ROOT}/_tmp/pkg"_tmp="${SCRIPT_ROOT}/_tmp"cleanup() {  rm -rf "${_tmp}"}trap "cleanup" EXIT SIGINTcleanupmkdir -p "${TMP_DIFFROOT}"cp -a "${DIFFROOT}"

接下来我们就可以来执行代码生成的脚本了,首先将依赖包放置到 vendor 目录中去:

$ go mod vendor

然后执行脚本生成代码:

$ chmod +x ./hack/update-codegen.sh$./hack/update-codegen.sh Generating deepcopy funcsGenerating clientset for example.com:v1 at operator-crd/pkg/client/clientsetGenerating listers for example.com:v1 at operator-crd/pkg/client/listersGenerating informers for example.com:v1 at operator-crd/pkg/client/informers

代码生成后,整个项目的 pkg 包变成了下面的样子:

$  tree pkg                pkg├── apis│   └── example.com│       └── v1│           ├── doc.go│           ├── register.go│           ├── types.go│           └── zz_generated.deepcopy.go└── client    ├── clientset    │   └── versioned    │       ├── clientset.go    │       ├── doc.go    │       ├── fake    │       │   ├── clientset_generated.go    │       │   ├── doc.go    │       │   └── register.go    │       ├── scheme    │       │   ├── doc.go    │       │   └── register.go    │       └── typed    │           └── example.com    │               └── v1    │                   ├── bar.go    │                   ├── doc.go    │                   ├── example.com_client.go    │                   ├── fake    │                   │   ├── doc.go    │                   │   ├── fake_bar.go    │                   │   └── fake_example.com_client.go    │                   └── generated_expansion.go    ├── informers    │   └── externalversions    │       ├── example.com    │       │   ├── interface.go    │       │   └── v1    │       │       ├── bar.go    │       │       └── interface.go    │       ├── factory.go    │       ├── generic.go    │       └── internalinterfaces    │           └── factory_interfaces.go    └── listers        └── example.com            └── v1                ├── bar.go                └── expansion_generated.go20 directories, 26 files

仔细观察可以发现pkg/apis/example.com/v1目录下面多了一个zz_generated.deepcopy.go文件,在pkg/client文件夹下生成了 clientset和 informers 和 listers 三个目录,有了这几个自动生成的客户端相关操作包,我们就可以去访问 CRD 资源了,可以和使用内置的资源对象一样去对 Bar 进行 List 和 Watch 操作了。

编写控制器

首先要先获取访问资源对象的 ClientSet,在项目根目录下面新建 main.go 文件。

package mainimport (    "k8s.io/client-go/tools/clientcmd"   "k8s.io/klog/v2"    clientset "operator-crd/pkg/client/clientset/versioned"    "operator-crd/pkg/client/informers/externalversions"    "time"    "os"    "os/signal"    "syscall")var (    onlyOneSignalHandler = make(chan struct{})    shutdownSignals      = []os.Signal{os.Interrupt, syscall.SIGTERM})// 注册 SIGTERM 和 SIGINT 信号// 返回一个 stop channel, 该通道在捕获到第一个信号时被关闭// 如果捕获到第二个信号,程序直接退出func setupSignalHandler() (stopCh <-chan struct{}) {    // 当调用两次的时候 panics    close(onlyOneSignalHandler)    stop := make(chan struct{})    c := make(chan os.Signal, 2)    // Notify 函数让 signal 包将输入信号转发到c    // 如果没有列出要传递的信号,会将所有输入信号传递到 c; 否则只会传递列出的输入信号    signal.Notify(c, shutdownSignals...)    go func() {        <-c        close(stop)        <-c        os.Exit(1) // 第二个信号直接退出    }()    return stop}func main() {    stopCh := setupSignalHandler()    // 获取config    config, err := clientcmd.BuildConfigFromFlags("", clientcmd.RecommendedHomeFile)    if err != nil {        klog.Fatalln(err)    }    // 通过config构建clientSet    // 这里的clientSet 是 Bar 的    clientSet, err := clientset.NewForConfig(config)    if err != nil {        klog.Fatalln(err)    }    // informerFactory 工厂类, 这里注入我们通过代码生成的 client    // client 主要用于和 API Server 进行通信,实现 ListAndWatch    factory := externalversions.NewSharedInformerFactory(clientSet, time.Second*30)    // 实例化自定义控制器    controller := NewController(factory.Example().V1().Bars())    // 启动 informer,开始list 和 watch    go factory.Start(stopCh)    // 启动控制器    if err = controller.Run(2, stopCh); err != nil {        klog.Fatalf("Error running controller: %s", err.Error())    }}

首先初始化一个用于访问 Bar 资源的 ClientSet 对象,然后同样新建一个 Bar 的 InformerFactory 实例,通过这个工厂实例可以去启动 Informer 开始对 Bar 的 List 和 Watch 操作,然后同样我们要自己去封装一个自定义的控制器,在这个控制器里面去实现一个控制循环,不断对 Bar 的状态进行调谐。

在项目根目录下新建controller.go文件,内容如下所示:

package mainimport (    "fmt"    "k8s.io/apimachinery/pkg/api/errors"    "k8s.io/apimachinery/pkg/util/runtime"    "k8s.io/apimachinery/pkg/util/wait"    "k8s.io/client-go/tools/cache"    "k8s.io/client-go/util/workqueue"    "k8s.io/klog/v2"    v1 "operator-crd/pkg/apis/example.com/v1"    "time"    informers "operator-crd/pkg/client/informers/externalversions/example.com/v1")type Controller struct {    informer  informers.BarInformer    workqueue workqueue.RateLimitingInterface}func NewController(informer informers.BarInformer) *Controller {    controller := &Controller{        informer: informer,        // WorkQueue 的实现,负责同步 Informer 和控制循环之间的数据        workqueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "bar"),    }    klog.Info("Setting up Bar event handlers")    // informer 注册了三个 Handler(AddFunc、UpdateFunc 和 DeleteFunc)    // 分别对应 API 对象的“添加”“更新”和“删除”事件。    // 而具体的处理操作,都是将该事件对应的 API 对象加入到工作队列中    informer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{        AddFunc:    controller.addBar,        UpdateFunc: controller.updateBar,        DeleteFunc: controller.deleteBar,    })    return controller}func (c *Controller) Run(thread int, stopCh <-chan struct{}) error {    defer runtime.HandleCrash()    defer c.workqueue.ShuttingDown()    // 记录开始日志    klog.Info("Starting Bar control loop")    klog.Info("Waiting for informer caches to sync")    // 等待缓存同步数据    if ok := cache.WaitForCacheSync(stopCh, c.informer.Informer().HasSynced); !ok {        return fmt.Errorf("failed to wati for caches to sync")    }    klog.Info("Starting workers")    for i := 0; i < thread; i++ {        go wait.Until(c.runWorker, time.Second, stopCh)    }    klog.Info("Started workers")    <-stopCh    klog.Info("Shutting down workers")    return nil}// runWorker 是一个不断运行的方法,并且一直会调用 c.processNextWorkItem 从 workqueue读取消息func (c *Controller) runWorker() {    for c.processNExtWorkItem() {    }}// 从workqueue读取和读取消息func (c *Controller) processNExtWorkItem() bool {    // 获取 item    item, shutdown := c.workqueue.Get()    if shutdown {        return false    }    if err := func(item interface{}) error {        // 标记以及处理        defer c.workqueue.Done(item)        var key string        var ok bool        if key, ok = item.(string); !ok {            // 判读key的类型不是字符串,则直接丢弃            c.workqueue.Forget(item)            runtime.HandleError(fmt.Errorf("expected string in workqueue but got %#v", item))            return nil        }        if err := c.syncHandler(key); err != nil {            return fmt.Errorf("error syncing '%s':%s", item, err.Error())        }        c.workqueue.Forget(item)        return nil    }(item); err != nil {        runtime.HandleError(err)        return false    }    return true}// 尝试从 Informer 维护的缓存中拿到了它所对应的 Bar 对象func (c *Controller) syncHandler(key string) error {    namespace, name, err := cache.SplitMetaNamespaceKey(key)    if err != nil {        runtime.HandleError(fmt.Errorf("invalid respirce key:%s", key))        return err    }    bar, err := c.informer.Lister().Bars(namespace).Get(name)    if err != nil {        if errors.IsNotFound(err) {            // 说明是在删除事件中添加进来的            return nil        }        runtime.HandleError(fmt.Errorf("failed to get bar by: %s/%s", namespace, name))        return err    }    fmt.Printf("[BarCRD] try to process bar:%#v ...", bar)    // 可以根据bar来做其他的事。    // todo    return nil}func (c *Controller) addBar(item interface{}) {    var key string    var err error    if key, err = cache.MetaNamespaceKeyFunc(item); err != nil {        runtime.HandleError(err)        return    }    c.workqueue.AddRateLimited(key)}func (c *Controller) deleteBar(item interface{}) {    var key string    var err error    if key, err = cache.DeletionHandlingMetaNamespaceKeyFunc(item); err != nil {        runtime.HandleError(err)        return    }    fmt.Println("delete crd")    c.workqueue.AddRateLimited(key)}func (c *Controller) updateBar(old, new interface{}) {    oldItem := old.(*v1.Bar)    newItem := new.(*v1.Bar)    // 比较两个资源版本,如果相同,则不处理    if oldItem.ResourceVersion == newItem.ResourceVersion {        return    }    c.workqueue.AddRateLimited(new)}

我们这里自定义的控制器只封装了一个 Informer 和一个限速队列,我们当然也可以在里面添加一个用于访问本地缓存的 Indexer,但实际上 Informer 中已经包含了 Lister,对于 List 和 Get 操作都会去通过 Indexer 从本地缓存中获取数据,所以只用一个 Informer 也是完全可行的。

同样在 Informer 中注册了3个事件处理器,将监听的事件获取到后送入 workqueue 队列,然后通过控制器的控制循环不断从队列中消费数据,根据获取的 key 来获取数据判断对象是需要删除还是需要进行其他业务处理,这里我们同样也只是打印出了对应的操作日志,对于实际的项目则进行相应的业务逻辑处理即可。

到这里一个完整的自定义 API 对象和它所对应的自定义控制器就编写完毕了。

测试

接下来我们直接运行我们的main函数:

I0512 16:51:33.922138   39032 controller.go:29] Setting up Bar event handlersI0512 16:51:33.922255   39032 controller.go:47] Starting Bar control loopI0512 16:51:33.922258   39032 controller.go:48] Waiting for informer caches to syncI0512 16:51:34.023108   39032 controller.go:55] Starting workersI0512 16:51:34.023153   39032 controller.go:60] Started workers

现在我们创建一个Bar资源对象:

# bar.yamlapiVersion: example.com/v1kind: Barmetadata:  name: bar-demo  namespace: defaultspec:  image: "nginx:1.17.1"  deploymentName: example-bar  replicas: 2

直接创建上面的对象,注意观察控制器的日志:

I0512 16:51:33.922138   39032 controller.go:29] Setting up Bar event handlersI0512 16:51:33.922255   39032 controller.go:47] Starting Bar control loopI0512 16:51:33.922258   39032 controller.go:48] Waiting for informer caches to syncI0512 16:51:34.023108   39032 controller.go:55] Starting workersI0512 16:51:34.023153   39032 controller.go:60] Started workers[BarCRD] try to process bar:"bar-demo" ...

可以看到,我们上面创建 bar.yaml 的操作,触发了 EventHandler 的添加事件,从而被放进了工作队列。然后控制器的控制循环从队列里拿到这个对象,并且打印出了正在处理这个 bar 对象的日志信息。

同样我们删除这个资源的时候,也会有对应的提示。

关于“生成CRD与自定义控制器的方法”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识,可以关注编程网行业资讯频道,小编每天都会为大家更新不同的知识点。

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯