本文介绍如何实现一个 Mutate Admission Webhook。
前面我们已经实现了一个校验的准入控制器,本节我们来尝试开发一个用于 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:
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,这些实现方式都是一样的了。
评论前必须登录!
注册