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。Containerif 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的不同,返回不同的bytespatch, 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//對新物件一頓修改操作。。。//轉換jsonbytes, 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: FilednsPolicy: ClusterFirstinitContainers:- command: - /bin/sh - -c - ‘ echo ’‘init’‘ && sleep 100 ’ image: busybox name: init resources: requests: cpu: 200m memory: 400MirestartPolicy: AlwaysschedulerName: default-schedulersecurityContext: {}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/v1kind: CustomResourceDefinitionmetadata: # 名字必需與下面的 spec 欄位匹配,並且格式為 ‘<名稱的複數形式>。<組名>’ name: qoss。stable。example。comspec: # 組名稱,用於 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: QoSmetadata: name: qos-default-policyspec: cpu: 100 memory: 100

定義MutatingWebhookConfiguration

相應的MutatingWebhookConfiguration也需要修改,增加對我們自定義資源的修改,建立,刪除,更新QoS的時候都要觸發Webhook

apiVersion: admissionregistration。k8s。io/v1beta1kind: MutatingWebhookConfigurationmetadata: name: mutating-webhook-example-cfg-debug labels: app: admission-webhook-example-debugwebhooks: - 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 maintype 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) //設定QoSfunc 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關於資源的設定是否正確