基於 kubebuilder 的 operators 的 webhook 設計&二次開發


前情提要

之前博客已經完成了一個Operator的設計、開發、部署、驗證過程,實戰中刻意跳過了一個重要的知識點:webhook,如今是時候學習它了,這是個很重要的功能;
什么是AdmissionWebhook,就要先了解K8S中的admission controller, 按照官方的解釋是: admission controller是攔截(經過身份驗證)API Server請求的網關,並且可以修改請求對象或拒絕請求。
啟動引導令牌是一種簡單的 bearer token ,這種令牌是在新建集群或者在現有集群中添加新加新節點時使用的。 它被設計成能支持 kubeadm,但是也可以被用在其他上下文中以便用戶在 不使用 kubeadm 的情況下啟動cluster。它也被設計成可以通過 RBAC 策略,結合Kubelet TLS Bootstrapping 系統進行工作。

簡而言之,它可以認為是攔截器,類似web框架中的middleware。

webhook 簡介

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

Operator中的webhook,其作用與上述過濾器類似,外部對CRD資源的變更,在Controller處理之前都會交給webhook提前處理,流程如下圖,該圖來自《Getting Started with Kubernetes | Operator and Operator Framework》:

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

kubebuilder為我們提供了生成webhook的基礎文件和代碼的工具,與制作API的工具類似,極大地簡化了工作量,咱們只需聚焦業務實現即可;
基於kubebuilder制作的webhook和controller,如果是同一個資源,那么它們在同一個進程中;

場景設計

為之前的elasticweb項目上增加需求,讓webhook發揮實際作用;

1 如果用戶忘記輸入總QPS,系統webhook負責設置默認值1300,操作如下圖:

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

准備工作

和controller類似,webhook既能在kubernetes環境中運行,也能在kubernetes環境之外運行;
如果webhook在kubernetes環境之外運行,是有些麻煩的,需要將證書放在所在環境,默認地址是:

# 上一篇博文,已經說明,需要提前的將配置文件和證書文件拷貝到指定目錄下,方便進行開發測試。
/tmp/k8s-webhook-server/serving-certs/tls.{crt,key}

為了讓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為例如下:

生成webhook

進入elasticweb工程下,執行以下命令創建webhook:

kubebuilder create webhook \
--group elasticweb \
--version v1 \
--kind ElasticWeb \
--defaulting \
--programmatic-validation

上述命令執行完畢后,先去看看main.go文件,如下圖紅框1所示,自動增加了一段代碼,作用是讓webhook生效:

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

/*


Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package v1

import (
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/util/validation/field"
	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.
	// 如果創建的時候沒有輸入總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)
	}
}

// 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 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()
}

// 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
}

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
	}
}

上述代碼有兩處需要注意,第一處和填寫默認值有關,如下圖:

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

要實現的業務需求就是通過修改上述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()
}

編碼完成,可見非常簡單,接下來,咱們把以前實戰遺留的東西清理一下,再開始新的部署和驗證;

清理工作

# 1 刪除elasticweb資源對象:
kubectl delete -f config/samples/elasticweb_v1_elasticweb.yaml

# 2 刪除controller
kustomize build config/default | kubectl delete -f -

# 3 刪除CRD
make uninstall

接下來就可以部署webhook了;

部署

# 1 部署CRD
make install
# 2 構建鏡像並推送到倉庫
make docker-build docker-push IMG=12589/elasticweb:001
# 3 部署集成了webhook功能的controller:
make deploy IMG=12589/elasticweb:001
# 4 查看pod,確認啟動成功:
kubectl get pods --all-namespaces

驗證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.xxx.xx: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開發提供參考;

閱讀拓展

參考文章: https://xinchen.blog.csdn.net/article/details/113922328

Watches 如何監控:
elasticweb_controller.go 文件原內容:

func (r *ElasticWebReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&elasticwebv1.ElasticWeb{}).
		Complete(r)
}

elasticweb_controller.go 文件更新后內容:

func (r *ElasticWebReconciler) SetupWithManager(mgr ctrl.Manager) error {
	//return ctrl.NewControllerManagedBy(mgr).
	//	For(&elasticwebv1.ElasticWeb{}).
	//	Complete(r)

	// 追加 pod 個數的 Watches 監控
	return ctrl.NewControllerManagedBy(mgr).
		For(&appsv1.Deployment{}).Watches(&source.Kind{Type: &corev1.Pod{}}, &handler.EnqueueRequestForObject{}).
		Complete(r)
}

作用說明,這樣我們通過 deployment 部署的資源,我們就可以很好的監控起來了 pod 個數的變化,並且把他矯正為正常的,資源值。


免責聲明!

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



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