路漫漫其修远兮
吾将上下而求索

准入控制器:MutatingAdmissionWebhook 实现

本文介绍如何实现一个 Mutate Admission Webhook。

前面我们已经实现了一个校验的准入控制器,本节我们来尝试开发一个用于 Mutate 的准入控制器,这两个控制器其实都是我们自己通过 Webhook 去实现,而且他们接收和返回的响应数据结构都是 AdmissionReview,唯一不同的是对于 Mutating 的 Webhook 在处理了资源对象后返回的时候需要我们拼接一个 JSONPatch 的数据。

https://bxdc-static.oss-cn-beijing.aliyuncs.com/images/20210123111237.png

示例

接下来我们在前面的镜像白名单的 Webhook 基础之上新增 mutate 的支持,该项目中我们也预留了 mutate 的入口,通过 /mutate 路径的请求进行 mutate 操作。

比如我们的需求是当我们的资源对象(Deployment 或 Service)中包含一个需要 mutate 的 annotation 注解后,通过这个 Webhook 后我们就给这个对象添加上一个执行了 mutate 操作的注解。

逻辑实现

首先在 WebhookServ 的 Serve 函数中新增 mutate 的逻辑入口函数:

// 序列化成功,也就是说获取到了请求的 AdmissionReview 的数据
if request.URL.Path == "/mutate" {
	admissionResponse = s.mutate(&requestedAdmissionReview)
} else if request.URL.Path == "/validate" {
	admissionResponse = s.validate(&requestedAdmissionReview)
}

然后我们的 mutate 逻辑就在下面的函数中去实现了:

func (s *WebhookServer) mutate(ar *admissionv1.AdmissionReview) *admissionv1.AdmissionResponse {
	req := ar.Request
	var (
		objectMeta                      *metav1.ObjectMeta
		resourceNamespace, resourceName string
	)

	klog.Infof("AdmissionReview for Kind=%v, Namespace=%v Name=%v UID=%v Operation=%v",
		req.Kind.Kind, req.Namespace, req.Name, req.UID, req.Operation)

	switch req.Kind.Kind {
	case "Deployment":
		var deployment appsv1.Deployment
		if err := json.Unmarshal(req.Object.Raw, &deployment); err != nil {
			klog.Errorf("Could not unmarshal raw object: %v", err)
			return &admissionv1.AdmissionResponse{
				Result: &metav1.Status{
					Code:    http.StatusBadRequest,
					Message: err.Error(),
				},
			}
		}
		resourceName, resourceNamespace, objectMeta = deployment.Name, deployment.Namespace, &deployment.ObjectMeta
	case "Service":
		var service corev1.Service
		if err := json.Unmarshal(req.Object.Raw, &service); err != nil {
			klog.Errorf("Could not unmarshal raw object: %v", err)
			return &admissionv1.AdmissionResponse{
				Result: &metav1.Status{
					Code:    http.StatusBadRequest,
					Message: err.Error(),
				},
			}
		}
		resourceName, resourceNamespace, objectMeta = service.Name, service.Namespace, &service.ObjectMeta
	default:
		return &admissionv1.AdmissionResponse{
			Result: &metav1.Status{
				Code:    http.StatusBadRequest,
				Message: fmt.Sprintf("Can't handle this kind(%s) object", req.Kind.Kind),
			},
		}
	}

	if !mutationRequired(objectMeta) {
		klog.Infof("Skipping validation for %s/%s due to policy check", resourceNamespace, resourceName)
		return &admissionv1.AdmissionResponse{
			Allowed: true,
		}
	}

	annotations := map[string]string{AnnotationStatusKey: "mutated"}

	var patch []patchOperation
	patch = append(patch, updateAnnotation(objectMeta.GetAnnotations(), annotations)...)

	patchBytes, err := json.Marshal(patch)
	if err != nil {
		return &admissionv1.AdmissionResponse{
			Result: &metav1.Status{
				Message: err.Error(),
			},
		}
	}

	klog.Infof("AdmissionResponse: patch=%v\\n", string(patchBytes))
	return &admissionv1.AdmissionResponse{
		Allowed: true,
		Patch:   patchBytes,
		PatchType: func() *admissionv1.PatchType {
			pt := admissionv1.PatchTypeJSONPatch
			return &pt
		}(),
	}
}

在这个函数中我们针对 Deployment 和 Service 两种资源类型进行处理,首先通过 mutationRequired 函数来判断当前资源对象是否需要执行 mutate 操作。

func mutationRequired(metadata *metav1.ObjectMeta) bool {
	annotations := metadata.GetAnnotations()
	if annotations == nil {
		annotations = map[string]string{}
	}

	var required bool
	switch strings.ToLower(annotations[AnnotationMutateKey]) {
	default:
		required = true
	case "n", "no", "false", "off":
		required = false
	}
	status := annotations[AnnotationStatusKey]

	if strings.ToLower(status) == "mutated" {
		required = false
	}

	klog.Infof("Mutation policy for %v/%v: required:%v", metadata.Namespace, metadata.Name, required)
	return required
}

如果资源对象中包含的 AnnotationMutateKey 这个 annotation 对应的值为 "n"、"no"、"false"、"off" 中的任何一个则不需要执行 mutate 操作,或者AnnotationStatusKey 这个 annotation 对应的值已经是 mutated 了则也不需要,否则就需要执行 mutate 操作。

如果需要执行 mutate 操作,则需要我们自己创建 Patch 操作,将 {AnnotationStatusKey: "mutated"} 这个 annotation Patch 到资源中去:

annotations := map[string]string{AnnotationStatusKey: "mutated"}

var patch []patchOperation
patch = append(patch, updateAnnotation(objectMeta.GetAnnotations(), annotations)...)

这里要执行 Patch 操作,需要定义一个如下所示的 patchOperation 的结构体:

type patchOperation struct {
	Op    string      `json:"op"`
	Path  string      `json:"path"`
	Value interface{} `json:"value,omitempty"`
}

然后在 updateAnnotation 函数中来创建更新 annotation 的 Patch:

func mutateAnnotation(target map[string]string, added map[string]string) (patch []patchOperation) {
	for key, value := range added {
		if target == nil || target[key] == "" {
			target = map[string]string{}
			patch = append(patch, patchOperation{
				Op:   "add",
				Path: "/metadata/annotations",
				Value: map[string]string{
					key: value,
				},
			})
		} else {
			patch = append(patch, patchOperation{
				Op:    "replace",
				Path:  "/metadata/annotations/" + key,
				Value: value,
			})
		}
	}
	return patch
}

最后需要将创建的 Patch 进行序列化,通过 AdmissionResponse 返回给 APIServer 即可:

patchBytes, err := json.Marshal(patch)
	if err != nil {
		return &admissionv1.AdmissionResponse{
			Result: &metav1.Status{
				Message: err.Error(),
			},
		}
	}

	klog.Infof("AdmissionResponse: patch=%v\\n", string(patchBytes))
	return &admissionv1.AdmissionResponse{
		Allowed: true,
		Patch:   patchBytes,
		PatchType: func() *admissionv1.PatchType {
			pt := admissionv1.PatchTypeJSONPatch
			return &pt
		}(),
	}

业务逻辑实现完成后,当然同样还是重新构建打包镜像,重新部署 Webhook 服务,然后同样还需要将这个 Webhook 进行注册,创建一个如下所示的资源对象:

apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
  name: admission-registry-mutate
webhooks:
- name: io.ydzs.admission-registry-mutate
  clientConfig:
    service:
      namespace: default
      name: admission-registry
      path: "/mutate"
    caBundle: CA_BUNDLE
  rules:
    - operations: [ "CREATE" ]
      apiGroups: ["apps", ""]
      apiVersions: ["v1"]
      resources: ["deployments","services"]
  admissionReviewVersions: [ "v1" ]
  sideEffects: None

测试

接下来我们首先创建如下所示的两个资源对象:

# test-deploy1.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-deploy1
spec:
  selector:
    matchLabels:
      app: test1-mutate
  template:
    metadata:
      labels:
        app: test1-mutate
    spec:
      containers:
        - name: mutate
          image: docker.io/nginx:1.7.9
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: test-svc1
spec:
  selector:
    app: test1-mutate
  ports:
    - port: 80
      targetPort: 80
  type: ClusterIP

由于上面的 Deployment 和 Service 没有添加任何的 annotation,所以正常通过上面我们的 mutate 这个准入控制器过后会被添加上一个 annotion:

$ kubectl apply -f test-deploy1.yaml
deployment.apps/test-deploy1 created
service/test-svc1 created
$ kubectl get deploy test-deploy1 -o yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    deployment.kubernetes.io/revision: "1"
    io.ydzs.admission-registry/status: mutated
  creationTimestamp: "2021-01-25T08:27:28Z"
  ......
$ kubectl get svc test-svc1 -o yaml
apiVersion: v1
kind: Service
metadata:
  annotations:
    io.ydzs.admission-registry/status: mutated
  creationTimestamp: "2021-01-25T08:27:28Z"
......

可以看到创建的 Deployment 和 Service 都被添加了一个 io.ydzs.admission-registry/status: mutated 的 annotation。

接下来再创建一个如下所示的资源对象:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-deploy2
  annotations:
    io.ydzs.admission-registry/mutate: "no"
spec:
  selector:
    matchLabels:
      app: test2-mutate
  template:
    metadata:
      labels:
        app: test2-mutate
    spec:
      containers:
        - name: mutate
          image: docker.io/nginx:1.7.9
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 80

由于该资源对象中本身就包含一个 io.ydzs.admission-registry/mutate: "no" 的 annotation,所以正常创建后不会被添加新的 annotation 了:

$ kubectl apply -f test-deploy2.yaml
deployment.apps/test-deploy2 created
$ kubectl get deploy test-deploy2 -o yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    deployment.kubernetes.io/revision: "1"
    io.ydzs.admission-registry/mutate: "no"
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"apps/v1","kind":"Deployment","metadata":{"annotations":{"io.ydzs.admission-registry/mutate":"no"},"name":"test-deploy2","namespace":"default"},
      "spec":{"selector":{"matchLabels":{"app":"test2-mutate"}},"template":{"metadata":{"labels":{"app":"test2-mutate"}},"spec":{"containers":
      [{"image":"docker.io/nginx:1.7.9","imagePullPolicy":"IfNotPresent","name":"mutate","ports":[{"containerPort":80}]}]}}}}
  creationTimestamp: "2021-01-25T09:15:56Z"
  ......

到这里就成功验证了 Mutate 这个准入控制器,很多时候可能不只是单纯的添加一个 annotation,很有可能是添加一个容器,添加一个环境变量,或者 volumes,这些实现方式都是一样的了。

未经允许不得转载:江哥架构师笔记 » 准入控制器:MutatingAdmissionWebhook 实现

分享到:更多 ()

评论 抢沙发

评论前必须登录!