8- kubebuilder 進階: webhook
在前面的文章當中我們已經完成了 NodePool Operator 的基本功能開發與測試,但是有時候我們會有這種需求,例如創建或者刪除資源的時候需要對資源進行一些檢查的操作,如果校驗不成功就不通過。或者是需要在完成實際的創建之前做一些其他操作,例如我創建一個 pod 之前對 pod 的資源做一些調整等。這些都可以通過准入控制的 WebHook 來實現。
准入控制存在兩種 WebHook,變更准入控制 MutatingAdmissionWebhook,和驗證准入控制 ValidatingAdmissionWebhook,執行的順序是先執行 MutatingAdmissionWebhook 再執行 ValidatingAdmissionWebhook。
創建 webhook
我們通過命令創建相關的腳手架代碼和 api
kubebuilder create webhook --group nodes --version v1 --kind NodePool --defaulting --programmatic-validation
執行之後可以看到多了一些 webhook 相關的文件和配置
├── api
│ └── v1
│ ├── groupversion_info.go
│ ├── nodepool_types.go
+ │ ├── nodepool_webhook.go # 在這裏實現 webhook 的相關接口
+ │ ├── webhook_suite_test.go # webhook 測試
│ └── zz_generated.deepcopy.go
├── bin
├── config
+ │ ├── certmanager # 用於部署
│ ├── crd
│ │ ├── bases
│ │ │ └── nodes.lailin.xyz_nodepools.yaml
│ │ ├── kustomization.yaml
│ │ ├── kustomizeconfig.yaml
│ │ └── patches
│ │ ├── cainjection_in_nodepools.yaml
+ │ │ └── webhook_in_nodepools.yaml
│ ├── default
│ │ ├── kustomization.yaml
│ │ ├── manager_auth_proxy_patch.yaml
│ │ ├── manager_config_patch.yaml
+ │ │ ├── manager_webhook_patch.yaml
+ │ │ └── webhookcainjection_patch.yaml
│ ├── manager
│ ├── prometheus
│ ├── rbac
│ ├── samples
│ │ └── nodes_v1_nodepool.yaml
+ │ └── webhook # webhook 部署配置
├── controllers
├── main.go
實現邏輯
實現 MutatingAdmissionWebhook 接口
這個只需要實現 Default 方法就行
// Default implements webhook.Defaulter so a webhook will be registered for the type
func (r *NodePool) Default() {
nodepoollog.Info("default", "name", r.Name)
// 如果 labels 爲空,我們就給 labels 加一個默認值
if len(r.Labels) == 0 {
r.Labels["node-pool.lailin.xyz"] = r.Name
}
}
實現 ValidatingAdmissionWebhook 接口
實現 ValidatingAdmissionWebhook
也是一樣只需要實現對應的方法就行了,默認是註冊了 Create 和 Update 事件的校驗,我們這裏主要是限制 Labels 和 Taints 的 key 只能是滿足正則 ^node-pool.lailin.xyz/*[a-zA-z0-9]*$
的固定格式
// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.
//+kubebuilder:webhook:path=/validate-nodes-lailin-xyz-v1-nodepool,mutating=false,failurePolicy=fail,sideEffects=None,groups=nodes.lailin.xyz,resources=nodepools,verbs=create;update,versions=v1,name=vnodepool.kb.io,admissionReviewVersions={v1,v1beta1}
var _ webhook.Validator = &NodePool{}
// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
func (r *NodePool) ValidateCreate() error {
nodePoolLog.Info("validate create", "name", r.Name)
return r.validate()
}
// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
func (r *NodePool) ValidateUpdate(old runtime.Object) error {
nodePoolLog.Info("validate update", "name", r.Name)
return r.validate()
}
// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
func (r *NodePool) ValidateDelete() error {
nodePoolLog.Info("validate delete", "name", r.Name)
// TODO(user): fill in your validation logic upon object deletion.
return nil
}
// validate 驗證
func (r *NodePool) validate() error {
err := errors.Errorf("taint or label key must validatedy by %s", keyReg.String())
for k := range r.Spec.Labels {
if !keyReg.MatchString(k) {
return errors.WithMessagef(err, "label key: %s", k)
}
}
for _, taint := range r.Spec.Taints {
if !keyReg.MatchString(taint.Key) {
return errors.WithMessagef(err, "taint key: %s", taint.Key)
}
}
return nil
}
部署
實現了之後直接在 make run 是跑不起來的,因爲 webhook 註冊的地址不對,我們這裏先看一下如何進行部署運行,然後再來看如何對 WebHook 進行本地調試。
WebHook 的運行需要校驗證書,kubebuilder 官方建議我們使用 cert-manager 簡化對證書的管理,所以我們先部署一下 cert-manager 的服務
kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.3.1/cert-manager.yaml
然後我們 build 鏡像並且將鏡像 load 到集羣中
make docker-build
kind load docker-image --name kind --nodes kind-worker controller:latest
然後查看一下 config/default/kustomization.yaml
文件,確認 webhook 相關的配置沒有被註釋掉
# Adds namespace to all resources.
namespace: node-pool-operator-system
# Value of this field is prepended to the
# names of all resources, e.g. a deployment named
# "wordpress" becomes "alices-wordpress".
# Note that it should also match with the prefix (text before '-') of the namespace
# field above.
namePrefix: node-pool-operator-
# Labels to add to all resources and selectors.
#commonLabels:
# someName: someValue
bases:
- ../crd
- ../rbac
- ../manager
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
# crd/kustomization.yaml
- ../webhook
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required.
- ../certmanager
# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'.
#- ../prometheus
patchesStrategicMerge:
# Protect the /metrics endpoint by putting it behind auth.
# If you want your controller-manager to expose the /metrics
# endpoint w/o any authn/z, please comment the following line.
- manager_auth_proxy_patch.yaml
# Mount the controller config file for loading manager configurations
# through a ComponentConfig type
#- manager_config_patch.yaml
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
# crd/kustomization.yaml
- manager_webhook_patch.yaml
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'.
# Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks.
# 'CERTMANAGER' needs to be enabled to use ca injection
- webhookcainjection_patch.yaml
# the following config is for teaching kustomize how to do var substitution
vars:
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix.
- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR
objref:
kind: Certificate
group: cert-manager.io
version: v1
name: serving-cert # this name should match the one in certificate.yaml
fieldref:
fieldpath: metadata.namespace
- name: CERTIFICATE_NAME
objref:
kind: Certificate
group: cert-manager.io
version: v1
name: serving-cert # this name should match the one in certificate.yaml
- name: SERVICE_NAMESPACE # namespace of the service
objref:
kind: Service
version: v1
name: webhook-service
fieldref:
fieldpath: metadata.namespace
- name: SERVICE_NAME
objref:
kind: Service
version: v1
name: webhook-service
檢查一下 manager/manager.yaml
是否存在 imagePullPolicy: IfNotPresent
不存在要加上
然後執行部署命令即可
make deploy
# 檢查 pod 是否正常啓動
▶ kubectl -n node-pool-operator-system get pods
NAME READY STATUS RESTARTS AGE
node-pool-operator-controller-manager-66bd747899-lf7xb 0/2 ContainerCreating 0 7s
使用 yaml 文件測試一下
apiVersion: nodes.lailin.xyz/v1
kind: NodePool
metadata:
name: worker
spec:
labels:
"xxx": "10"
handler: runc
提交之後可以發現報錯,因爲 label key 不滿足我們的要求
▶ kubectl apply -f config/samples/
Error from server (label key: xxx: taint or label key must validatedy by ^node-pool.lailin.xyz/*[a-zA-z0-9]*$): error when applying patch:
{"metadata":{"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"nodes.lailin.xyz/v1\",\"kind\":\"NodePool\",\"metadata\":{\"annotations\":{},\"name\":\"worker\"},\"spec\":{\"handler\":\"runc\",\"labels\":{\"xxx\":\"10\"}}}\n"}},"spec":{"labels":{"node-pool.lailin.xyz/worker":null,"xxx":"10"},"taints":null}}
to:
Resource: "nodes.lailin.xyz/v1, Resource=nodepools", GroupVersionKind: "nodes.lailin.xyz/v1, Kind=NodePool"
Name: "worker", Namespace: ""
for: "config/samples/nodes_v1_nodepool.yaml": admission webhook "vnodepool.kb.io" denied the request: label key: xxx: taint or label key must validatedy by ^node-pool.lailin.xyz/*[a-zA-z0-9]*$
再用一個正常的 yaml 測試
apiVersion: nodes.lailin.xyz/v1
kind: NodePool
metadata:
name: worker
spec:
labels:
"node-pool.lailin.xyz/xxx": "10"
handler: runc
可以正常提交
▶ kubectl apply -f config/samples/
nodepool.nodes.lailin.xyz/worker configured
本地調試
雖然 kubebuilder 已經爲我們做了很多事情將服務部署運行基本傻瓜化了,但是每次做一點點修改就需要重新編譯部署還是非常的麻煩,所以我們來看看如何在本地進行聯調。
PS: 這裏會用到之前 4. kustomize 簡明教程 講到的 kustomize 的特性構建開發環境,如果忘記了可以先看看之前的文章哦
我們先看看 config/webhook/manifests.yaml
這裏麪包含了兩個准入控制的信息,不過他們的配置類似,我們看一個就行了,這裏以 MutatingWebhookConfiguration
爲例
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
creationTimestamp: null
name: mutating-webhook-configuration
webhooks:
- admissionReviewVersions:
- v1
- v1beta1
clientConfig:
service:
name: webhook-service
namespace: system
path: /mutate-nodes-lailin-xyz-v1-nodepool
failurePolicy: Fail
name: mnodepool.kb.io
rules:
- apiGroups:
- nodes.lailin.xyz
apiVersions:
- v1
operations:
- CREATE
- UPDATE
resources:
- nodepools
sideEffects: None
主要是 clientConfig
的配置,如果想要本地聯調,我們需要將 clientConfig.service
刪掉,替換成
clientConfig:
url: https://host.docker.internal:9443/mutate-nodes-lailin-xyz-v1-nodepool
注意: host.docker.internal
是 docker desktop 的默認域名,通過這個可以調用到宿主機上的服務,url path mutate-nodes-lailin-xyz-v1-nodepool
需要和 service 中的 path
保持一致
然後再加上 caBundle
clientConfig:
caBundle: CA證書 base64 後的字符串
證書
想要本地聯調需要先生成證書,我們使用 openssl 來生成,先創建一個 config/cert
文件夾,我們把證書都放到這裏
首先創建一個 csr.conf
文件
[ req ]
default_bits = 2048
prompt = no
default_md = sha256
req_extensions = req_ext
distinguished_name = dn
[ dn ]
C = CN
ST = Guangzhou
L = Shenzhen
CN = host.docker.internal
[ req_ext ]
subjectAltName = @alt_names
[ alt_names ]
DNS.1 = host.docker.internal # 這裏由於我們直接訪問的是域名所以用 DNS
[ v3_ext ]
authorityKeyIdentifier=keyid,issuer:always
basicConstraints=CA:FALSE
keyUsage=keyEncipherment,dataEncipherment
extendedKeyUsage=serverAuth,clientAuth
subjectAltName=@alt_names
然後生成 CA 證書並且簽發本地證書
# 生成 CA 證書
openssl genrsa -out ca.key 2048
openssl req -x509 -new -nodes -key ca.key -subj "/CN=host.docker.internal" -days 10000 -out ca.crt
# 簽發本地證書
openssl genrsa -out tls.key 2048
openssl req -new -SHA256 -newkey rsa:2048 -nodes -keyout tls.key -out tls.csr -subj "/C=CN/ST=Shanghai/L=Shanghai/O=/OU=/CN=host.docker.internal"
openssl req -new -key tls.key -out tls.csr -config csr.conf
openssl x509 -req -in tls.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out tls.crt -days 10000 -extensions v3_ext -extfile csr.conf
配置變更
我們爲了和原本的開發體驗保持一致,所以利用 kustomize 的特性新建一個 config/dev
文件夾,包含兩個文件修改我們想要的配置
▶ tree config/dev
config/dev
├── kustomization.yaml
└── webhook_patch.yaml
先看一下 kustomization.yaml
,從 default 文件夾中繼承配置,然後使用 patches 修改一些配置,主要是分別給兩種准入控制 WebHook 添加 url 字段,然後使用 webhook_patch.yaml
對兩個文件做些統一的配置
resources:
- ../default
patches:
- patch: |
- op: "add"
path: "/webhooks/0/clientConfig/url"
value: "https://host.docker.internal:9443/mutate-nodes-lailin-xyz-v1-nodepool"
target:
kind: MutatingWebhookConfiguration
- patch: |
- op: "add"
path: "/webhooks/0/clientConfig/url"
value: "https://host.docker.internal:9443/validate-nodes-lailin-xyz-v1-nodepool"
target:
kind: ValidatingWebhookConfiguration
- path: webhook_patch.yaml
target:
group: admissionregistration.k8s.io
webhook_patch.yaml
這個主要是移除 cert-manager.io 的 annotation,本地調試不需要使用它進行證書注入,然後移除掉 service
並且添加 CA 證書
- op: "remove"
path: "/metadata/annotations/cert-manager.io~1inject-ca-from"
- op: "remove"
path: "/webhooks/0/clientConfig/service"
- op: "add"
path: "/webhooks/0/clientConfig/caBundle"
value: CA 證書 base64 後的值
CA 證書的值可以通過以下命令獲取
cat config/cert/ca.crt | base64 | tr -d '\n'
然後修改一下 main.go
將證書文件夾指定到我們剛剛生成好的文件目錄
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
MetricsBindAddress: metricsAddr,
Port: 9443,
HealthProbeBindAddress: probeAddr,
LeaderElection: enableLeaderElection,
LeaderElectionID: "97acaccf.lailin.xyz",
+ CertDir: "config/cert/", // 手動指定證書位置用於測試
})
爲了方便調試,在 makefile 中添加
dev: manifests kustomize
cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
$(KUSTOMIZE) build config/dev | kubectl apply -f -
最後執行一下 make dev
然後再執行 make run
就行了
總結
今天完成了准入控制 WebHook 的實現,雖然這個例子可能不太好,如果只需要校驗正則,直接配置一下//+kubebuilder:validation:Pattern=string
就行了,但是學習了這個之後其實可以做很多事情,例如給 pod 增加 sidecar 根據應用類型的不同注入不同的一些 agent 等等
kubebuilder 的功能也使用的差不多了,知其然也要知其所以然,我們下一篇來看看源碼
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/CFggTa7E91Rf1N1EZZpOBA