<aside> ⏰ 本文介绍如何实现一个 Mutate Admission Webhook。

</aside>

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

示例

接下来我们在前面的镜像白名单的 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: