K8S_Webhook结合CRD进阶玩法

本篇非K8S Webhook入门,入门请参考之前的文章:2021-08-29_从0到1开发K8S_Webhook最佳实践

本章主要讲两个内容

1:对资源进行复杂修改后,怎么快速得到Json Patch

2:结合CRD自定义资源实现Webhook动态读取数据

所有代码都在(V2版本):https://github.com/scriptwang/admission-webhook-example

背景

在之前的文章2021-08-29_从0到1开发K8S_Webhook最佳实践实现了对资源进行简单修改,也就是添加几个label,核心代码如下

func updateLabels(target map[string]string, added map[string]string) (patch []patchOperation) {
	values := make(map[string]string)
	for key, value := range added {
		if target == nil || target[key] == "" {
			values[key] = value
		}
	}
	patch = append(patch, patchOperation{
		Op:    "add",
		Path:  "/metadata/labels",
		Value: values,
	})
	return patch
}

added = map[string]string{
    "app.kubernetes.io/name":     "not_available" ,
    "app.kubernetes.io/instance":  "not_available" ,
}

逻辑很简单,判断target,也就是已经有的里面是否包含本次需要添加的(added中的值),包含则不添加,反之则添加,最后封装成了patchOperation切片,patchOperation也就是一个简单的结构体

type patchOperation struct {
	Op    string      `json:"op"`
	Path  string      `json:"path"`
	Value interface{} `json:"value,omitempty"`
}

其中的Path: "/metadata/labels"是需要自己判断的,为啥要这么写Patch?这其实是json相关内容了,详情参考:http://jsonpatch.com/

那么问题来了,简单的Patch可以这样操作,那么复杂的Patch呢?比如增加一个initContainer,Patch例子如下

[
    {
        "op": "add",
        "path": "/spec/template/spec/initContainers",
        "value": [
            {
                "command": [
                    "/bin/sh",
                    "-c",
                    " echo 'init' \u0026\u0026 sleep 100 "
                ],
                "image": "busybox",
                "name": "init",
                "resources": {
                    "requests": {
                        "cpu": "200m",
                        "memory": "400Mi"
                    }
                }
            }
        ]
    }
]

难道要手动拼接?拼接这个Json Patch就够喝一壶了,有没有啥简单办法?答案就是jsondiff

jsondiff找不同?

github:https://github.com/wI2L/jsondiff

jsondiff is a Go package for computing the diff between two JSON documents as a series of RFC6902 (JSON Patch) operations, which is particularly suitable to create the patch response of a Kubernetes Mutating Webhook for example.

简介都说了非常适合用来创建Kubernetes Webhook返回的Json Patch

使用方式非常的人性,下面以添加一个initContainer为例

//旧对象
newDeploy := deploy.DeepCopy()
//深克隆一个新对象
newPodSpec := &newDeploy.Spec.Template.Spec

//下面是对新对象进行一顿操作
//如果没有initContainer则新加一个
var initContainer *corev1.Container
if len(newPodSpec.InitContainers) == 0 {
    newPodSpec.InitContainers = []corev1.Container{
        {
            Name:    "init",
            Image:   "busybox",
            Command: []string{"/bin/sh", "-c", " echo 'init' && sleep 100 "},
        },
    }
    initContainer = &newPodSpec.InitContainers[0]
    log.WriteString("\nmutate add initContainer sucess!")		
}

/********************************************************* 结束修改操作 */

// 比较新旧deploy的不同,返回不同的bytes
patch, err := jsondiff.Compare(deploy, newDeploy)
if err != nil {
    ...//错误处理
}

//打patch,patchBytes就是我们需要的了
patchBytes, err := json.MarshalIndent(patch, "", "    ")
if err != nil {
    ...//错误处理
}

//打印出来看一下
fmt.Println(string(patchBytes))

打印出来就是下面这样的

[
    {
        "op": "add",
        "path": "/spec/template/spec/initContainers",
        "value": [
            {
                "command": [
                    "/bin/sh",
                    "-c",
                    " echo 'init' \u0026\u0026 sleep 100 "
                ],
                "image": "busybox",
                "name": "init",
                "resources": {
                    "requests": {
                        "cpu": "200m",
                        "memory": "400Mi"
                    }
                }
            }
        ]
    }
]

有感觉了么,不用手动拼接Json Patch,直接深克隆一个对象出来,直接修改新对象的值(这比手动拼接Json Patch爽多了?),然后对比新老对象,jsondiff会自动把不同找出来生产Json Patch,

json转yaml?

全是json看着不爽?我怎么知道我修改的地方转成yaml最后到底对不对?那就需要一个json和yaml互转的工具了,它就是yaml:https://github.com/ghodss/yaml

YAML marshaling and unmarshaling support for Go

上面不是有了深克隆并且添加了initContainer的新对象么,直接把该对象转换成json,然后在转换成yaml打印出来

//旧对象
newDeploy := deploy.DeepCopy()
//深克隆一个新对象
newPodSpec := &newDeploy.Spec.Template.Spec
//对新对象一顿修改操作
...
//转换json
bytes, err := json.Marshal(newPodSpec)
if err == nil {
    //转换yaml
    yamlStr, err := yaml.JSONToYAML(bytes)
    if err == nil {
        fmt.Println(string(yamlStr))
    }
}

打印出来如下,看到initContainers了吧,这看起来就非常直观了,比满屏幕的没有格式化的Json看起来舒服多了

containers:
- command:
  - /bin/sleep
  - infinity
  image: busybox
  imagePullPolicy: IfNotPresent
  name: sleep
  resources: {}
  terminationMessagePath: /dev/termination-log
  terminationMessagePolicy: File
dnsPolicy: ClusterFirst
initContainers:
- command:
  - /bin/sh
  - -c
  - ' echo ''init'' && sleep 100 '
  image: busybox
  name: init
  resources:
    requests:
      cpu: 200m
      memory: 400Mi
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
terminationGracePeriodSeconds: 30

CRD?

CRD全称为CustomResourceDefinition,即自定义资源

可参考:https://kubernetes.io/zh/docs/concepts/extend-kubernetes/api-extension/custom-resources/

那么什么是自定义资源?在K8S中。Pod、Deployment、Service都是内建资源,如果这些资源不满足我们的需求,那么就可以自定义资源,假设场景如下:

需要在Pod部署的时候添加一个initContainer,并且根据需要动态的设置该initContainer的资源(CPU、内存),所以资源设置不能再Webhook里面写死,需要从外部动态读取,这个时候就需要CRD(也可以从别的地方读取,比如数据库,本质上Webhook是一个第三方WebServer,既然都用K8S了,那就直接定义CRD吧,相当于变相从K8S的ETCD里面读取数据)

整体逻辑为:在创建CRD的时候同步设置Webhook中CPU、内存变量的值,修改同理,删除的时候变回默认值。这样在Deployment创建的时候根据CPU、内存的值去修改相应的资源

定义CRD

要实现上面的需求,那么需要定义一个全局的CRD叫QoS,如下,详细定义在K8S文档都有说明(上面的链接),此处不在赘述

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  # 名字必需与下面的 spec 字段匹配,并且格式为 '<名称的复数形式>.<组名>'
  name: qoss.stable.example.com
spec:
  # 组名称,用于 REST API: /apis/<组>/<版本>
  group: stable.example.com
  # 列举此 CustomResourceDefinition 所支持的版本
  versions:
    - name: v1
      # 每个版本都可以通过 served 标志来独立启用或禁止
      served: true
      # 其中一个且只有一个版本必需被标记为存储版本
      storage: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                cpu:
                  type: integer
                memory:
                  type: integer
  # 可以是 Namespaced 或 Cluster
  scope: Cluster
  names:
    # 名称的复数形式,用于 URL:/apis/<组>/<版本>/<名称的复数形式>
    plural: qoss
    # 名称的单数形式,作为命令行使用时和显示时的别名
    singular: qos
    # kind 通常是单数形式的驼峰编码(CamelCased)形式。你的资源清单会使用这一形式。
    kind: QoS
    # shortNames 允许你在命令行使用较短的字符串来匹配资源
    shortNames:
    - qos

这相当于定义了一个类,然后我们创建一个该资源的实例

apiVersion: "stable.example.com/v1"
kind: QoS
metadata:
  name: qos-default-policy
spec:
  cpu: 100
  memory: 100

定义MutatingWebhookConfiguration

相应的MutatingWebhookConfiguration也需要修改,增加对我们自定义资源的修改,创建,删除,更新QoS的时候都要触发Webhook

apiVersion: admissionregistration.k8s.io/v1beta1
kind: MutatingWebhookConfiguration
metadata:
  name: mutating-webhook-example-cfg-debug
  labels:
    app: admission-webhook-example-debug
webhooks:
  - name: mutating-example.qikqiak.com.debug
    clientConfig:
      service:
        name: admission-webhook-example-svc-debug
        namespace: default
        path: "/mutate"
      caBundle: ${CA_BUNDLE}
    rules:
      - operations: [ "CREATE" ]
        apiGroups: ["apps", ""]
        apiVersions: ["v1"]
        resources: ["deployments"]
      # 主要在此处添加QoS的内容,创建,删除,更新的时候都要触发
      - operations: [ "CREATE","DETELE","UPDATE" ]
        apiGroups: ["stable.example.com", ""]
        apiVersions: ["v1"]
        resources: ["qoss"]
    namespaceSelector:
      matchLabels:
        admission-webhook-example: enabled

增加qos.go

增加一个文件代表CRD资源设置的值

package main

type QoS struct {
	Spec QoSpec `json:"spec,omitempty"`
}

type QoSpec struct {
	Cpu    int64 `json:"cpu,omitempty"`
	Memory int64 `json:"memory,omitempty"`
}

var (
	QoSInst = &QoSpec{}
)

func (qos *QoSpec) getQoSpec() *QoSpec {
	//判断空对象
	if (*QoSInst == QoSpec{}) {
		//默认值
		return &QoSpec{
			Cpu:    200,
			Memory: 400,
		}
	} else {
		return QoSInst
	}
}

func (qos *QoSpec) setQoSpec(qoSpec *QoSpec) {
	QoSInst = qoSpec
}

对QoS的设置

case "QoS":
		var qos QoS
		var raw []byte
		if req.Operation == "DELETE" {
			raw = req.OldObject.Raw
		} else {
			raw = req.Object.Raw
		}
		if err := json.Unmarshal(raw, &qos); err != nil {
			log.WriteString(fmt.Sprintf("\nCould not unmarshal raw object: %v", err))
			glog.Errorf(log.String())
			return &v1beta1.AdmissionResponse{
				Result: &metav1.Status{
					Message: err.Error(),
				},
			}
		}
		return mutateQoS(&qos, req.Operation, log)
		
//设置QoS
func mutateQoS(qos *QoS, operation v1beta1.Operation, log *bytes.Buffer) *v1beta1.AdmissionResponse {
	if operation == "DELETE" {
		//删除设置空对象
		QoSInst.setQoSpec(&QoSpec{})
        //其他的时候更新 
	} else {
		if qos != nil && (qos.Spec != QoSpec{}) {
			QoSInst.setQoSpec(&qos.Spec)
		}
	}
	return &v1beta1.AdmissionResponse{
		Allowed: true,
	}
}

验证

验证思路:新增、删除、修改QoS资源(debug/crd/QoS.yaml)后创建Deployment(debug/sleep.yaml),观察打印出来的Json Patch关于资源的设置是否正确