跳转至

创建webhook

创建 webhook

通过命令创建相关的脚手架代码和 api

kubebuilder create webhook \
  --group devops \
  --version v1 \
  --kind KBDev \
  --defaulting \
  --programmatic-validation

执行之后可以看到多了一些 webhook 相关的文件和配置

  ├── api
     └── v1
         ├── groupversion_info.go
  ...
+        ├── nodepool_webhook.go # 在这里实现 webhook 的相关接口
+        ├── webhook_suite_test.go # webhook 测试
         └── zz_generated.deepcopy.go
  ├── bin
  ├── config
+    ├── certmanager # 用于部署证书
     ├── crd
  ...
     ├── default
        ├── kustomization.yaml
        ├── manager_auth_proxy_patch.yaml
        ├── manager_config_patch.yaml
+       ├── manager_webhook_patch.yaml
+       └── webhookcainjection_patch.yaml
     ├── manager
     ├── prometheus
     ├── rbac
  ...
+    └── webhook # webhook 部署配置
  ├── controllers
  ├── main.go

webhook 的入口

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 kbdevlog = logf.Log.WithName("kbdev-resource")

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

//+kubebuilder:webhook:path=/mutate-devops-aipaas-io-v1-kbdev,mutating=true,failurePolicy=fail,sideEffects=None,groups=devops.aipaas.io,resources=kbdevs,verbs=create;update,versions=v1,name=mkbdev.kb.io,admissionReviewVersions=v1

var _ webhook.Defaulter = &KBDev{}

const (
    DefaultImage = "dockerstacks/rocky-kbdev:master_latest"
)

// Default implements webhook.Defaulter so a webhook will be registered for the type
func (r *KBDev) Default() {

    kbdevlog.Info("default", "name", r.Name)

}

//+kubebuilder:webhook:path=/validate-devops-aipaas-io-v1-kbdev,mutating=false,failurePolicy=fail,sideEffects=None,groups=devops.aipaas.io,resources=kbdevs,verbs=create;update,versions=v1,name=vkbdev.kb.io,admissionReviewVersions=v1

var _ webhook.Validator = &KBDev{}

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

    return nil
}

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

    return nil
}

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

    return nil
}

webhook 注册的原理

调用 ctrl.NewWebhookManagedBy(mgr).For(r).Complete() 注册 webhook

代码:"sigs.k8s.io/controller-runtime/pkg/builder/webhook/webhook.go"

// Complete builds the webhook.
func (blder *WebhookBuilder) Complete() error {
    // Set the Config
    blder.loadRestConfig()

    // Set the Webhook if needed
    return blder.registerWebhooks()
}

func (blder *WebhookBuilder) registerWebhooks() error {
    typ, err := blder.getType()
    if err != nil {
        return err
    }

    // Create webhook(s) for each type
    blder.gvk, err = apiutil.GVKForObject(typ, blder.mgr.GetScheme())
    if err != nil {
        return err
    }

    blder.registerDefaultingWebhook() // 注册 mutate webhook
    blder.registerValidatingWebhook() // 注册 validate webhook

    err = blder.registerConversionWebhook() // 注册 conversion webhook
    if err != nil {
        return err
    }
    return nil
}

注册 webhook 的服务路径

在 mutate 和 validate webhook 的注册中,controller-runtime 通过以下规则生成 path

func generateMutatePath(gvk schema.GroupVersionKind) string {
    return "/mutate-" + strings.ReplaceAll(gvk.Group, ".", "-") + "-" +
        gvk.Version + "-" + strings.ToLower(gvk.Kind)
}

func generateValidatePath(gvk schema.GroupVersionKind) string {
    return "/validate-" + strings.ReplaceAll(gvk.Group, ".", "-") + "-" +
        gvk.Version + "-" + strings.ToLower(gvk.Kind)
}

在 conversion webhook 的注册中,controller-runtime 通过以下规则生成 path

即 conversion webhook 必须使用 /convert 作为服务路径;同时如果有多个资源需要 conversion,仅需要注册一次即可

func (blder *WebhookBuilder) registerConversionWebhook() error {
    ok, err := conversion.IsConvertible(blder.mgr.GetScheme(), blder.apiType)
    if err != nil {
        log.Error(err, "conversion check failed", "GVK", blder.gvk)
        return err
    }
    if ok {
        if !blder.isAlreadyHandled("/convert") {
            blder.mgr.GetWebhookServer().Register("/convert", &conversion.Webhook{})
        }
        log.Info("Conversion webhook enabled", "GVK", blder.gvk)
    }

    return nil
}

注册的具体逻辑

代码:"sigs.k8s.io/controller-runtime/pkg/webhook/server.go"

// Register marks the given webhook as being served at the given path.
// It panics if two hooks are registered on the same path.
func (s *Server) Register(path string, hook http.Handler) {
    s.mu.Lock()
    defer s.mu.Unlock()

    s.defaultingOnce.Do(s.setDefaults)
    if _, found := s.webhooks[path]; found {
        panic(fmt.Errorf("can't register duplicate path: %v", path))
    }
    // TODO(directxman12): call setfields if we've already started the server
    s.webhooks[path] = hook
    s.WebhookMux.Handle(path, metrics.InstrumentedHook(path, hook))

    regLog := log.WithValues("path", path)
    regLog.Info("Registering webhook")

    // we've already been "started", inject dependencies here.
    // Otherwise, InjectFunc will do this for us later.
    if s.setFields != nil {
        if err := s.setFields(hook); err != nil {
            // TODO(directxman12): swallowing this error isn't great, but we'd have to
            // change the signature to fix that
            regLog.Error(err, "unable to inject fields into webhook during registration")
        }

        baseHookLog := log.WithName("webhooks")

        // NB(directxman12): we don't propagate this further by wrapping setFields because it's
        // unclear if this is how we want to deal with log propagation.  In this specific instance,
        // we want to be able to pass a logger to webhooks because they don't know their own path.
        if _, err := inject.LoggerInto(baseHookLog.WithValues("webhook", path), hook); err != nil {
            regLog.Error(err, "unable to logger into webhook during registration")
        }
    }
}

启用 webhook

然后在 main.go 中需要匹配正确的 webhook 入口:

    if os.Getenv("ENABLE_WEBHOOKS") != "false" {
        if err = (&devopsv1.KBDev{}).SetupWebhookWithManager(mgr); err != nil {
            setupLog.Error(err, "unable to create webhook", "webhook", "KBDev")
            os.Exit(1)
        }
    }

实现逻辑

实现 MutatingAdmissionWebhook 接口

这个只需要实现 Default 方法就行

// Default implements webhook.Defaulter so a webhook will be registered for the type
func (r *KBDev) Default() {

    kbdevlog.Info("default", "name", r.Name)

}

实现 ValidatingAdmissionWebhook 接口

实现 ValidatingAdmissionWebhook也是一样只需要实现对应的方法就行了,默认是注册了 Create 和 Update 事件的校验

//+kubebuilder:webhook:path=/validate-devops-aipaas-io-v1-kbdev,mutating=false,failurePolicy=fail,sideEffects=None,groups=devops.aipaas.io,resources=kbdevs,verbs=create;update,versions=v1,name=vkbdev.kb.io,admissionReviewVersions=v1

var _ webhook.Validator = &KBDev{}

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

    return nil
}

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

    return nil
}

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

    return nil
}