Skip to content

Webhook 准入控制

Webhook 类型

请求 → API Server

    ├── MutatingAdmissionWebhook(变更准入)
    │   修改对象(注入 Sidecar、设置默认值)

    ├── ValidatingAdmissionWebhook(验证准入)
    │   验证对象(拒绝不合规的请求)

    └── 写入 etcd

kubebuilder 实现 Webhook

go
// api/v1alpha1/myapp_webhook.go

// +kubebuilder:webhook:path=/mutate-apps-mycompany-io-v1alpha1-myapp,mutating=true,failurePolicy=fail,sideEffects=None,groups=apps.mycompany.io,resources=myapps,verbs=create;update,versions=v1alpha1,name=mmyapp.kb.io,admissionReviewVersions=v1

type MyAppCustomDefaulter struct{}

func (d *MyAppCustomDefaulter) Default(ctx context.Context, obj runtime.Object) error {
    myApp := obj.(*MyApp)

    // 设置默认值
    if myApp.Spec.Replicas == 0 {
        myApp.Spec.Replicas = 1
    }
    if myApp.Labels == nil {
        myApp.Labels = make(map[string]string)
    }
    myApp.Labels["managed-by"] = "my-operator"

    return nil
}

// +kubebuilder:webhook:path=/validate-apps-mycompany-io-v1alpha1-myapp,mutating=false,failurePolicy=fail,sideEffects=None,groups=apps.mycompany.io,resources=myapps,verbs=create;update,versions=v1alpha1,name=vmyapp.kb.io,admissionReviewVersions=v1

type MyAppCustomValidator struct{}

func (v *MyAppCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
    myApp := obj.(*MyApp)
    return v.validate(myApp)
}

func (v *MyAppCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
    myApp := newObj.(*MyApp)
    return v.validate(myApp)
}

func (v *MyAppCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
    return nil, nil
}

func (v *MyAppCustomValidator) validate(myApp *MyApp) (admission.Warnings, error) {
    var errs field.ErrorList

    if myApp.Spec.Replicas > 100 {
        errs = append(errs, field.Invalid(
            field.NewPath("spec", "replicas"),
            myApp.Spec.Replicas,
            "副本数不能超过 100",
        ))
    }

    if myApp.Spec.Image == "" {
        errs = append(errs, field.Required(
            field.NewPath("spec", "image"),
            "镜像不能为空",
        ))
    }

    if len(errs) > 0 {
        return nil, apierrors.NewInvalid(
            schema.GroupKind{Group: "apps.mycompany.io", Kind: "MyApp"},
            myApp.Name,
            errs,
        )
    }
    return nil, nil
}

Webhook 配置

yaml
# MutatingWebhookConfiguration
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
  name: my-operator-mutating
  annotations:
    cert-manager.io/inject-ca-from: my-operator-system/my-operator-serving-cert
webhooks:
- name: mmyapp.kb.io
  admissionReviewVersions: ["v1"]
  clientConfig:
    service:
      name: my-operator-webhook-service
      namespace: my-operator-system
      path: /mutate-apps-mycompany-io-v1alpha1-myapp
  rules:
  - apiGroups: ["apps.mycompany.io"]
    apiVersions: ["v1alpha1"]
    operations: ["CREATE", "UPDATE"]
    resources: ["myapps"]
  failurePolicy: Fail
  sideEffects: None

Sidecar 注入示例

go
// 自动注入日志收集 Sidecar
func (d *PodDefaulter) Default(ctx context.Context, obj runtime.Object) error {
    pod := obj.(*corev1.Pod)

    // 检查是否需要注入
    if pod.Labels["inject-logging"] != "true" {
        return nil
    }

    // 注入 Sidecar
    sidecar := corev1.Container{
        Name:  "log-collector",
        Image: "fluentd:v1.16",
        VolumeMounts: []corev1.VolumeMount{
            {Name: "app-logs", MountPath: "/var/log/app"},
        },
    }
    pod.Spec.Containers = append(pod.Spec.Containers, sidecar)

    // 注入 Volume
    pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{
        Name:         "app-logs",
        VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}},
    })

    return nil
}

本站内容由 褚成志 整理编写,仅供学习参考