Operator-SDK:Controller数据写入自定义CRD并实现自动更新

Operator-SDK:Controller数据写入自定义CRD并实现自动更新

之前写了一个使用Operator-SDK自定义CRD实现Node的request信息收集的demo,现在要将其采集到的数据写入K8s,实现在集群内直接使用kubectl get命令就可以查询到信息。

参考资料:

kubebuilder2.0学习笔记——进阶使用 - SegmentFault 思否

Operator-SDK:自定义CRD实现Node的request信息收集

Operator-SDK:结合基于GO构建的官方样例相关文档阅读及部分命令解释

创建 API 和 Controller

创建一个CRD API,其中组为cache,版本为v1alpha1,类型为Noderequest。出现提示时,输入y来创建resource和controller。

$ operator-sdk create api --group cache --version v1alpha1 --kind Noderequest --resource --controller
Writing scaffold for you to edit...
api/v1alpha1/noderequest_types.go
controllers/noderequest_controller.go
...

这将会构建一个 Noderequest 资源的 API 在目录 api/v1alpha1/noderequest_types.go ,控制器 controller 在目录 controllers/noderequest_controller.go。创建出来的项目目录结构如下:

image-20220215144011863

控制器的核心逻辑在先前的文档中有过分析:Operator-SDK:自定义CRD实现Node的request信息收集

查看noderequest-operator/api/v1alpha1文件下的noderequest_types.go文件,这个是自定义CRD的结构体文件:

// NoderequestSpec defines the desired state of Noderequest
type NoderequestSpec struct {
	// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
	// Important: Run "make" to regenerate code after modifying this file

	// Foo is an example field of Noderequest. Edit noderequest_types.go to remove/update
	Foo string `json:"foo,omitempty"`
}

// NoderequestStatus defines the observed state of Noderequest
type NoderequestStatus struct {
	// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
	// Important: Run "make" to regenerate code after modifying this file
}

//+kubebuilder:object:root=true
//+kubebuilder:subresource:status

// Noderequest is the Schema for the noderequests API
type Noderequest struct {
	metav1.TypeMeta   `json:",inline"`
	metav1.ObjectMeta `json:"metadata,omitempty"`

	Spec   NoderequestSpec   `json:"spec,omitempty"`
	Status NoderequestStatus `json:"status,omitempty"`
}

这里,SpecStatus均是Noderequest的成员变量,Status并不像Pod.Status一样,是PodsubResource子资源。因此,如果在controller的代码中调用到Status().Update(),会触发panic,并报错:the server could not find the requested resource

如果我们想像k8s中的设计那样,那么就要遵循k8s中status subresource的使用规范:

  • 用户只能指定一个CRD实例的spec部分;
  • CRD实例的status部分由控制器进行变更。

设计subResource风格的Status

需要在Noderequest的注释中添加一行// +kubebuilder:subresource:status,在上述的代码中已经添加:

//+kubebuilder:object:root=true
//+kubebuilder:subresource:status

// Noderequest is the Schema for the noderequests API
type Noderequest struct {
	metav1.TypeMeta   `json:",inline"`
	metav1.ObjectMeta `json:"metadata,omitempty"`

	Spec   NoderequestSpec   `json:"spec,omitempty"`
	Status NoderequestStatus `json:"status,omitempty"`
}

创建Noderequest资源时,即便我们填入了非空的status结构,也不会更新到apiserver中。Status只能通过对应的client进行更新。比如在controller中:

if Noderequest.Status.Progress == 0 {
      Noderequest.Status.Progress = 1
      err := r.Status().Update(ctx, &Noderequest)
      if err != nil {
          return ctrl.Result{}, err
      }
  }

这样,只要Noderequest实例的status.Progress为0时(比如我们创建一个Noderequest实例时,由于status.Progress无法配置,故初始化为默认值,即0),controller就会帮我们将它变更为1。

在type.go中设置scope标记

k8s中node、pv等资源是集群级别的,它们没有namespace字段,因此查询node资源时也无需规定要从哪个namespace查。

我们在进行k8s operator时经常也需要设计这样的字段。默认情况下,kubebuilder会给我们创建namespace scope的crd资源,可以手动在GO中的types.go文件中增加和改变kubebuilder scope marker来设置我们资源的scope。这个文件在api/<version>/<kind>_types.go或者api/<group>/<version>/<kind>_types.go位置。

在执行kubebuilder create api ****后,我们在生成的资源的*_types.go文件中,找到资源的主结构体,增加一条注释kubebuilder:resource:scope=Cluster,在这个项目中,是上文中的noderequest_types.go文件:

//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
//+kubebuilder:resource:scope=Cluster

// Noderequest is the Schema for the noderequests API
type Noderequest struct {
	metav1.TypeMeta   `json:",inline"`
	metav1.ObjectMeta `json:"metadata,omitempty"`

	Spec   NoderequestSpec   `json:"spec,omitempty"`
	Status NoderequestStatus `json:"status,omitempty"`
}

如果要设置为命名空间范围,要将标记替换为//+kubebuilder:resource:scope=Namespace

需要注意的是,// +kubebuilder:resource:scope=Cluster这句注释,必须放在结构体的上方、且必须是最靠近该结构体的一条kubebuilder注释,否则会失效。

编写结构体并更新资源类型

将Node的request中的CPU和内存数据写回CRD,所以需要三个字段分别用来存储Node结点名称、CPU资源、内存资源,由于计算出来的资源需要查看百分比,所以CPU和内存再分别添加一个Rate来存储所占用总资源的百分比。同时使用json自动补全时,给出的json名称是下划线格式的,手动修改为Pascal格式:

// NoderequestStatus defines the observed state of Noderequest
type NoderequestStatus struct {
	// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
	// Important: Run "make" to regenerate code after modifying this file

	NodeName string `json:"NodeName"`
	// CPU, in cores. (500m = .5 cores)
	NodeCpu string `json:"NodeCpu"`
	// CPU, in %
	NodeCpuRate string `json:"NodeCpuRate"`
	// Memory, in bytes. (500Gi = 500GiB = 500 * 1024 * 1024 * 1024)
	NodeMem string `json:"NodeMem"`
	// Memory, in %
	NodeMemRate string `json:"NodeMemRate"`
}

修改 *_types.go 文件后,必须要运行以下命令以更新该资源类型的生成代码:

make generate

上面的makefile目标将调用 controller-gen 实用程序来更新生成的 api/v1alpha1/zz_generated.deepcopy.go 文件,以确保API的Go类型定义可以实现应用在Kind类型上的 runtime.Object 接口。

生成 CRD manifests

使用 spec/status 字段和 CRD 验证 markers定义API以后,可以使用以下命令生成和更新 CRD manifests:

$ make manifests

这个 makefile 目标将会调用 controller-genconfig/crd/bases/cache.example.com_memcacheds.yaml 处生成 CRD manifests:

$ make generate 
/home/shark/go-project/noderequest-operator/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."

$ make manifests 
/home/shark/go-project/noderequest-operator/bin/controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases

最后执行make install将CRD注册到集群中,相当于执行了kubectl apply -f命令:

$ make install
/home/shark/go-project/noderequest-operator/bin/controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
/home/shark/go-project/noderequest-operator/bin/kustomize build config/crd | kubectl apply -f -
customresourcedefinition.apiextensions.k8s.io/noderequests.noderequest.example.com created

如果需要卸载CRD,则执行make uninstall

生成的YAML文件中的status字段如下:

status:
            description: NoderequestStatus defines the observed state of Noderequest
            properties:
              NodeCpu:
                description: CPU, in cores. (500m = .5 cores)
                type: string
              NodeCpuRate:
                description: CPU, in %
                type: string
              NodeMem:
                description: Memory, in bytes. (500Gi = 500GiB = 500 * 1024 * 1024
                  * 1024)
                type: string
              NodeMemRate:
                description: Memory, in %
                type: string
              NodeName:
                type: string
            required:
            - NodeCpu
            - NodeCpuRate
            - NodeMem
            - NodeMemRate
            - NodeName
            type: object

Controller代码

获取CR

首先获取到所有的CR实例,由于创建的时候是设置scope=Cluster集群范围的CRD,所以命名空间为空时可以获取到所有的CR:

nodeRequestList := &noderequestv1.NoderequestList{}
	nodeOpts := []client.ListOption{
		client.InNamespace(""),
	}
	err = r.Client.List(ctx, nodeRequestList, nodeOpts...)
	if err != nil {
		fmt.Println("ERROR[GetNoderequest]:", err)
		return
	}

获取到当前所有的CR是为了判断集群内是否已经存在对应的监控资源,如果不存在需要Create,存在则需要进行更新(Patch或者Update)。

Patch

先定义一个Noderequest资源类型的实例,将其Name设置为当前监控的集群结点的Node的name。

定义布尔类型变量exist用来判断当前是否已经存在对该Node结点的监控资源,false为不存在,true为存在。默认值为false,使用循环判断nodeRequestList.Items是否已经存在一个实例的Name的值等于当前需要监控的Node结点的name的值,若存在,说明之前已经创建过,执行Patch操作:

nodeRequest := &noderequestv1.Noderequest{}
nodeRequest.Name = node.Name
exist := false
for _, item := range nodeRequestList.Items {
   if item.Status.NodeName == node.Name {
      exist = true
      // Ptach the CR
      patch := client.MergeFrom(nodeRequest.DeepCopy())
      nodeRequest.Status.NodeName = node.Name
      nodeRequest.Status.NodeCpu = cpuReqs.String()
      nodeRequest.Status.NodeCpuRate = strconv.FormatInt(int64(fractionCpuReqs), 10)
      nodeRequest.Status.NodeMem = memoryReqs.String()
      nodeRequest.Status.NodeMemRate = strconv.FormatInt(int64(fractionMemoryReqs), 10)
      err = r.Client.Status().Patch(ctx, nodeRequest, patch)
      if err != nil {
         fmt.Println("ERROR[Patch]:", err)
         return
      }
      fmt.Println("update: ", item.Status.NodeName)
      break
   }
}

Create & Patch

若不存在,需要进行创建Create并且Patch。

在上一个代码中,nodeRequest.Name = node.Name表示以当前Node结点的name值进行创建的CRD资源,所以创建出来的CRD资源名称将会跟随集群Node的名字。

if exist == false {
		err = r.Client.Create(ctx, nodeRequest)
		if err != nil {
			fmt.Println("ERROR[Create]:", err)
			return
		}
		// Ptach the CR
		patch := client.MergeFrom(nodeRequest.DeepCopy())
		nodeRequest.Status.NodeName = node.Name
		nodeRequest.Status.NodeCpu = cpuReqs.String()
		nodeRequest.Status.NodeCpuRate = strconv.FormatInt(int64(fractionCpuReqs), 10)
		nodeRequest.Status.NodeMem = memoryReqs.String()
		nodeRequest.Status.NodeMemRate = strconv.FormatInt(int64(fractionMemoryReqs), 10)
		err = r.Client.Status().Patch(ctx, nodeRequest, patch)
		if err != nil {
			fmt.Println("ERROR[Patch]:", err)
			return
		}
		fmt.Println("create: ", nodeRequest.Name)
	}

注意,Create时不会自动填入Status字段的内容,需要在Create之后进行Patch,将字段内容补全。

最终结果

在执行make install之前,集群内查不到这个类型的资源:

$ kubectl get noderequest
Error from server (NotFound): Unable to list "noderequest.example.com/v1alpha1, Resource=noderequests": the server could not find the requested resource (get noderequests.noderequest.example.com)

先执行make install命令,然后进行查询:

$ kubectl get noderequest
No resources found

当前集群内还未存在资源。将项目ControllerRun起来后,再查询资源:

$ kubectl get noderequest
NAME                    AGE
10.100.100.130-slave    3s
10.100.100.131-master   3s
10.100.100.144-slave    3s
10.100.100.147-slave    3s

查询下集群信息:

$ kubectl get node
NAME                    STATUS   ROLES                  AGE   VERSION
10.100.100.130-slave    Ready    <none>                 55d   v1.21.5-hc.1
10.100.100.131-master   Ready    control-plane,master   55d   v1.21.5-hc.1
10.100.100.144-slave    Ready    <none>                 55d   v1.21.5-hc.1
10.100.100.147-slave    Ready    <none>                 55d   v1.21.5-hc.1

看到资源和集群node是一一对应的。

再查询详细的信息,检查资源数据是否正确写入CR中:

$ kubectl get noderequest 10.100.100.131-master -oyaml
apiVersion: noderequest.example.com/v1alpha1
kind: Noderequest
metadata:
  creationTimestamp: "2022-02-16T02:19:09Z"
  generation: 1
  name: 10.100.100.131-master
  resourceVersion: "26005744"
  uid: 5b3b20c1-de86-46ce-8367-472eccf21152
spec: {}
status:
  NodeCpu: 2832m
  NodeCpuRate: "70"
  NodeMem: 2493Mi
  NodeMemRate: "33"
  NodeName: 10.100.100.131-master

字段数据正确写入。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值