kubebuilder實戰之七:webhook


歡迎訪問我的GitHub

https://github.com/zq2599/blog_demos

內容:所有原創文章分類匯總及配套源碼,涉及Java、Docker、Kubernetes、DevOPS等;

本篇概覽

  • 本文是《kubebuilder實戰》系列的第七篇,之前的文章咱們完成了一個Operator的設計、開發、部署、驗證過程,為了讓整個過程保持簡潔並且篇幅不膨脹,實戰中刻意跳過了一個重要的知識點:webhook,如今是時候學習它了,這是個很重要的功能;
  • 本篇由以下部分構成:
  1. 介紹webhook;
  2. 結合前面的elasticweb項目,設計一個使用webhook的場景;
  3. 准備工作
  4. 生成webhook
  5. 開發(配置)
  6. 開發(編碼)
  7. 部署
  8. 驗證Defaulter(添加默認值)
  9. 驗證Validator(合法性校驗)

關於webhook

  • 熟悉java開發的讀者大多知道過濾器(Servlet Filter),如下圖,外部請求會先到達過濾器,做一些統一的操作,例如轉碼、校驗,然后才由真正的業務邏輯處理請求:

在這里插入圖片描述

在這里插入圖片描述

  • 再來看看webhook具體做了哪些事情,如下圖,kubernetes官方博客明確指出webhook可以做兩件事:修改(mutating)和驗證(validating)
    在這里插入圖片描述

  • kubebuilder為我們提供了生成webhook的基礎文件和代碼的工具,與制作API的工具類似,極大地簡化了工作量,咱們只需聚焦業務實現即可;

  • 基於kubebuilder制作的webhook和controller,如果是同一個資源,那么它們在同一個進程中

設計實戰場景

  • 為了讓實戰有意義,咱們為前面的elasticweb項目上增加需求,讓webhook發揮實際作用;
  1. 如果用戶忘記輸入總QPS,系統webhook負責設置默認值1300,操作如下圖:

在這里插入圖片描述

  1. 為了保護系統,給單個pod的QPS設置上限1000,如果外部輸入的singlePodQPS值超過1000,就創建資源對象失敗,如下圖所示:

在這里插入圖片描述

源碼下載

名稱 鏈接 備注
項目主頁 https://github.com/zq2599/blog_demos 該項目在GitHub上的主頁
git倉庫地址(https) https://github.com/zq2599/blog_demos.git 該項目源碼的倉庫地址,https協議
git倉庫地址(ssh) git@github.com:zq2599/blog_demos.git 該項目源碼的倉庫地址,ssh協議
  • 這個git項目中有多個文件夾,kubebuilder相關的應用在kubebuilder文件夾下,如下圖紅框所示:

在這里插入圖片描述

  • kubebuilder文件夾下有多個子文件夾,本篇對應的源碼在elasticweb目錄下,如下圖紅框所示:

在這里插入圖片描述

准備工作

  • 和controller類似,webhook既能在kubernetes環境中運行,也能在kubernetes環境之外運行;
  • 如果webhook在kubernetes環境之外運行,是有些麻煩的,需要將證書放在所在環境,默認地址是:
/tmp/k8s-webhook-server/serving-certs/tls.{crt,key}
  • 為了省事兒,也為了更接近生產環境的用法,接下來的實戰的做法是將webhook部署在kubernetes環境中
  • 為了讓webhook在kubernetes環境中運行,咱們要做一點准備工作安裝cert manager,執行以下操作:
kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.2.0/cert-manager.yaml
  • 上述操作完成后會新建很多資源,如namespace、rbac、pod等,以pod為例如下:
[root@hedy ~]# kubectl get pods --all-namespaces
NAMESPACE        NAME                                       READY   STATUS    RESTARTS   AGE
cert-manager     cert-manager-6588898cb4-nvnz8              1/1     Running   1          5d14h
cert-manager     cert-manager-cainjector-7bcbdbd99f-q645r   1/1     Running   1          5d14h
cert-manager     cert-manager-webhook-5fd9f9dd86-98tm9      1/1     Running   1          5d14h
...
  • 操作完成后,准備工作結束,可以開始實戰了;

生成webhook

  • 進入elasticweb工程下,執行以下命令創建webhook:
kubebuilder create webhook \
--group elasticweb \
--version v1 \
--kind ElasticWeb \
--defaulting \
--programmatic-validation
  • 上述命令執行完畢后,先去看看main.go文件,如下圖紅框1所示,自動增加了一段代碼,作用是讓webhook生效:

在這里插入圖片描述

  • 上圖紅框2中的elasticweb_webhook.go就是新增文件,內容如下:
package v1

import (
	"k8s.io/apimachinery/pkg/runtime"
	ctrl "sigs.k8s.io/controller-runtime"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/webhook"
)

// log is for logging in this package.
var elasticweblog = logf.Log.WithName("elasticweb-resource")

func (r *ElasticWeb) SetupWebhookWithManager(mgr ctrl.Manager) error {
	return ctrl.NewWebhookManagedBy(mgr).
		For(r).
		Complete()
}

// EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!

// +kubebuilder:webhook:path=/mutate-elasticweb-com-bolingcavalry-v1-elasticweb,mutating=true,failurePolicy=fail,groups=elasticweb.com.bolingcavalry,resources=elasticwebs,verbs=create;update,versions=v1,name=melasticweb.kb.io

var _ webhook.Defaulter = &ElasticWeb{}

// Default implements webhook.Defaulter so a webhook will be registered for the type
func (r *ElasticWeb) Default() {
	elasticweblog.Info("default", "name", r.Name)

	// TODO(user): fill in your defaulting logic.
}

// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.
// +kubebuilder:webhook:verbs=create;update,path=/validate-elasticweb-com-bolingcavalry-v1-elasticweb,mutating=false,failurePolicy=fail,groups=elasticweb.com.bolingcavalry,resources=elasticwebs,versions=v1,name=velasticweb.kb.io

var _ webhook.Validator = &ElasticWeb{}

// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
func (r *ElasticWeb) ValidateCreate() error {
	elasticweblog.Info("validate create", "name", r.Name)

	// TODO(user): fill in your validation logic upon object creation.
	return nil
}

// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
func (r *ElasticWeb) ValidateUpdate(old runtime.Object) error {
	elasticweblog.Info("validate update", "name", r.Name)

	// TODO(user): fill in your validation logic upon object update.
	return nil
}

// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
func (r *ElasticWeb) ValidateDelete() error {
	elasticweblog.Info("validate delete", "name", r.Name)

	// TODO(user): fill in your validation logic upon object deletion.
	return nil
}
  • 上述代碼有兩處需要注意,第一處和填寫默認值有關,如下圖:

在這里插入圖片描述

  • 第二處和校驗有關,如下圖:

在這里插入圖片描述

  • 咱們要實現的業務需求就是通過修改上述elasticweb_webhook.go的內容來實現,不過代碼稍后再寫,先把配置都改好;

開發(配置)

  • 打開文件config/default/kustomization.yaml,下圖四個紅框中的內容原本都被注釋了,現在請將注釋符號都刪掉,使其生效:

在這里插入圖片描述

  • 還是文件config/default/kustomization.yaml,節點vars下面的內容,原本全部被注釋了,現在請全部放開,放開后的效果如下圖:

在這里插入圖片描述

  • 配置已經完成,可以編碼了;

開發(編碼)

  • 打開文件elasticweb_webhook.go

  • 新增依賴:

apierrors "k8s.io/apimachinery/pkg/api/errors"
  • 找到Default方法,改成如下內容,可見代碼很簡單,判斷TotalQPS是否存在,若不存在就寫入默認值,另外還加了兩行日志:
func (r *ElasticWeb) Default() {
	elasticweblog.Info("default", "name", r.Name)

	// TODO(user): fill in your defaulting logic.
	// 如果創建的時候沒有輸入總QPS,就設置個默認值
	if r.Spec.TotalQPS == nil {
		r.Spec.TotalQPS = new(int32)
		*r.Spec.TotalQPS = 1300
		elasticweblog.Info("a. TotalQPS is nil, set default value now", "TotalQPS", *r.Spec.TotalQPS)
	} else {
		elasticweblog.Info("b. TotalQPS exists", "TotalQPS", *r.Spec.TotalQPS)
	}
}
  • 接下來開發校驗功能,咱們把校驗功能封裝成一個validateElasticWeb方法,然后在新增和修改的時候各調用一次,如下,可見最終是調用apierrors.NewInvalid生成錯誤實例的,而此方法接受的是多個錯誤,因此要為其准備切片做入參,當然了,如果是多個參數校驗失敗,可以都放入切片中:
func (r *ElasticWeb) validateElasticWeb() error {
	var allErrs field.ErrorList

	if *r.Spec.SinglePodQPS > 1000 {
		elasticweblog.Info("c. Invalid SinglePodQPS")

		err := field.Invalid(field.NewPath("spec").Child("singlePodQPS"),
			*r.Spec.SinglePodQPS,
			"d. must be less than 1000")

		allErrs = append(allErrs, err)

		return apierrors.NewInvalid(
			schema.GroupKind{Group: "elasticweb.com.bolingcavalry", Kind: "ElasticWeb"},
			r.Name,
			allErrs)
	} else {
		elasticweblog.Info("e. SinglePodQPS is valid")
		return nil
	}
}
  • 再找到新增和修改資源對象時被調用的方法,在里面調用validateElasticWeb:
// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
func (r *ElasticWeb) ValidateCreate() error {
	elasticweblog.Info("validate create", "name", r.Name)

	// TODO(user): fill in your validation logic upon object creation.

	return r.validateElasticWeb()
}

// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
func (r *ElasticWeb) ValidateUpdate(old runtime.Object) error {
	elasticweblog.Info("validate update", "name", r.Name)

	// TODO(user): fill in your validation logic upon object update.
	return r.validateElasticWeb()
}
  • 編碼完成,可見非常簡單,接下來,咱們把以前實戰遺留的東西清理一下,再開始新的部署和驗證;

清理工作

  • 如果您是隨着《kubebuilder實戰》系列一路操作下來,此時系統上應該積攢了之前遺留的內容,可以通過以下步驟完成清理:
  1. 刪除elasticweb資源對象:
kubectl delete -f config/samples/elasticweb_v1_elasticweb.yaml
  1. 刪除controller
kustomize build config/default | kubectl delete -f -
  1. 刪除CRD
make uninstall
  • 現在萬事俱備,可以部署webhook了;

部署

  1. 部署CRD
make install
  1. 構建鏡像並推送到倉庫(我終於受夠了hub.docker.com的龜速,改為阿里雲鏡像倉庫):
make docker-build docker-push IMG=registry.cn-hangzhou.aliyuncs.com/bolingcavalry/elasticweb:001
  1. 部署集成了webhook功能的controller:
make deploy IMG=registry.cn-hangzhou.aliyuncs.com/bolingcavalry/elasticweb:001
  1. 查看pod,確認啟動成功:
zhaoqin@zhaoqindeMBP-2 ~ % kubectl get pods --all-namespaces
NAMESPACE           NAME                                             READY   STATUS    RESTARTS   AGE
cert-manager        cert-manager-6588898cb4-nvnz8                    1/1     Running   1          5d21h
cert-manager        cert-manager-cainjector-7bcbdbd99f-q645r         1/1     Running   1          5d21h
cert-manager        cert-manager-webhook-5fd9f9dd86-98tm9            1/1     Running   1          5d21h
elasticweb-system   elasticweb-controller-manager-7dcbfd4675-898gb   2/2     Running   0          20s

驗證Defaulter(添加默認值)

  • 修改文件config/samples/elasticweb_v1_elasticweb.yaml,修改后的內容如下,可見totalQPS字段已經被注釋掉了:
apiVersion: v1
kind: Namespace
metadata:
  name: dev
  labels:
    name: dev
---
apiVersion: elasticweb.com.bolingcavalry/v1
kind: ElasticWeb
metadata:
  namespace: dev
  name: elasticweb-sample
spec:
  # Add fields here
  image: tomcat:8.0.18-jre8
  port: 30003
  singlePodQPS: 500
  # totalQPS: 600

  • 創建一個elasticweb資源對象:
kubectl apply -f config/samples/elasticweb_v1_elasticweb.yaml
  • 此時單個pod的QPS是500,如果webhook的代碼生效的話,總QPS就是1300,而對應的pod數應該是3個,接下來咱們看看是否符合預期;
  • 先看elasticweb、deployment、pod等資源對象是否正常,如下所示,全部符合預期:
zhaoqin@zhaoqindeMBP-2 ~ % kubectl get elasticweb -n dev                                                                 
NAME                AGE
elasticweb-sample   89s
zhaoqin@zhaoqindeMBP-2 ~ % kubectl get deployments -n dev
NAME                READY   UP-TO-DATE   AVAILABLE   AGE
elasticweb-sample   3/3     3            3           98s
zhaoqin@zhaoqindeMBP-2 ~ % kubectl get service -n dev    
NAME                TYPE       CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
elasticweb-sample   NodePort   10.105.125.125   <none>        8080:30003/TCP   106s
zhaoqin@zhaoqindeMBP-2 ~ % kubectl get pod -n dev    
NAME                                 READY   STATUS    RESTARTS   AGE
elasticweb-sample-56fc5848b7-5tkxw   1/1     Running   0          113s
elasticweb-sample-56fc5848b7-blkzg   1/1     Running   0          113s
elasticweb-sample-56fc5848b7-pd7jg   1/1     Running   0          113s
  • kubectl describe命令查看elasticweb資源對象的詳情,如下所示,TotalQPS字段被webhook設置為1300,RealQPS也計算正確:
zhaoqin@zhaoqindeMBP-2 ~ % kubectl describe elasticweb elasticweb-sample -n dev
Name:         elasticweb-sample
Namespace:    dev
Labels:       <none>
Annotations:  <none>
API Version:  elasticweb.com.bolingcavalry/v1
Kind:         ElasticWeb
Metadata:
  Creation Timestamp:  2021-02-27T16:07:34Z
  Generation:          2
  Managed Fields:
    API Version:  elasticweb.com.bolingcavalry/v1
    Fields Type:  FieldsV1
    fieldsV1:
      f:metadata:
        f:annotations:
          .:
          f:kubectl.kubernetes.io/last-applied-configuration:
      f:spec:
        .:
        f:image:
        f:port:
        f:singlePodQPS:
    Manager:      kubectl-client-side-apply
    Operation:    Update
    Time:         2021-02-27T16:07:34Z
    API Version:  elasticweb.com.bolingcavalry/v1
    Fields Type:  FieldsV1
    fieldsV1:
      f:status:
        f:realQPS:
    Manager:         manager
    Operation:       Update
    Time:            2021-02-27T16:07:34Z
  Resource Version:  687628
  UID:               703de111-d859-4cd2-b3c4-1d201fb7bd7d
Spec:
  Image:           tomcat:8.0.18-jre8
  Port:            30003
  Single Pod QPS:  500
  Total QPS:       1300
Status:
  Real QPS:  1500
Events:      <none>
  • 再來看看controller的日志,其中的webhook部分是否符合預期,如下圖紅框所示,發現TotalQPS字段為空,就將設置為默認值,並且在檢測的時候SinglePodQPS的值也沒有超過1000:

在這里插入圖片描述

  • 最后別忘了用瀏覽器驗證web服務是否正常,我這里的完整地址是:http://192.168.50.75:30003/
  • 至此,咱們完成了webhook的Defaulter驗證,接下來驗證Validator

驗證Validator

  • 接下來該驗證webhook的參數校驗功能了,先驗證修改時的邏輯;
  • 編輯文件config/samples/update_single_pod_qps.yaml,值如下:
spec:
  singlePodQPS: 1100
  • 用patch命令使之生效:
kubectl patch elasticweb elasticweb-sample \
-n dev \
--type merge \
--patch "$(cat config/samples/update_single_pod_qps.yaml)"
  • 此時,控制台會輸出錯誤信息:
Error from server (ElasticWeb.elasticweb.com.bolingcavalry "elasticweb-sample" is invalid: spec.singlePodQPS: Invalid value: 1100: d. must be less than 1000): admission webhook "velasticweb.kb.io" denied the request: ElasticWeb.elasticweb.com.bolingcavalry "elasticweb-sample" is invalid: spec.singlePodQPS: Invalid value: 1100: d. must be less than 1000
  • 再用kubectl describe命令查看elasticweb資源對象的詳情,如下圖紅框,依然是500,可見webhook已經生效,阻止了錯誤的發生:

在這里插入圖片描述

  • 再去看controller日志,如下圖紅框所示,和代碼對應上了:

在這里插入圖片描述

  • 接下來再試試webhook在新增時候的校驗功能;
  • 清理前面創建的elastic資源對象,執行命令:
kubectl delete -f config/samples/elasticweb_v1_elasticweb.yaml
  • 修改文件,如下圖紅框所示,咱們將singlePodQPS的值改為超過1000,看看webhook是否能檢查到這個錯誤,並阻止資源對象的創建:

在這里插入圖片描述

  • 執行以下命令開始創建elasticweb資源對象:
kubectl apply -f config/samples/elasticweb_v1_elasticweb.yaml
  • 控制台提示以下信息,包含了咱們代碼中寫入的錯誤描述,證明elasticweb資源對象創建失敗,證明webhook的Validator功能已經生效:
namespace/dev created
Error from server (ElasticWeb.elasticweb.com.bolingcavalry "elasticweb-sample" is invalid: spec.singlePodQPS: Invalid value: 1500: d. must be less than 1000): error when creating "config/samples/elasticweb_v1_elasticweb.yaml": admission webhook "velasticweb.kb.io" denied the request: ElasticWeb.elasticweb.com.bolingcavalry "elasticweb-sample" is invalid: spec.singlePodQPS: Invalid value: 1500: d. must be less than 1000
  • 不放心的話執行kubectl get命令檢查一下,發現空空如也:
zhaoqin@zhaoqindeMBP-2 elasticweb % kubectl get elasticweb -n dev       
No resources found in dev namespace.
zhaoqin@zhaoqindeMBP-2 elasticweb % kubectl get deployments -n dev
No resources found in dev namespace.
zhaoqin@zhaoqindeMBP-2 elasticweb % kubectl get service -n dev
No resources found in dev namespace.
zhaoqin@zhaoqindeMBP-2 elasticweb % kubectl get pod -n dev
No resources found in dev namespace.
  • 還要看下controller日志,如下圖紅框所示,符合預期:

在這里插入圖片描述

  • 至此,operator的webhook的開發、部署、驗證咱們就完成了,整個elasticweb也算是基本功能齊全,希望能為您的operator開發提供參考;

你不孤單,欣宸原創一路相伴

  1. Java系列
  2. Spring系列
  3. Docker系列
  4. kubernetes系列
  5. 數據庫+中間件系列
  6. DevOps系列

歡迎關注公眾號:程序員欣宸

微信搜索「程序員欣宸」,我是欣宸,期待與您一同暢游Java世界...
https://github.com/zq2599/blog_demos


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM