Ramblings of a Goon

My personal ramblings

Kubernetes Custom Controller - Part 2

Writing the Mutating Webhook

4 minutes read

In this post we are going to look at the mutating webhook in order to inject a watcher sidecar container from Part 1. The code can be found here.

Most of the code is boilerplate, the main code is in internal/injector/webhook/mutator.go

The important function is Mutate. It handles validating the mutation request and sending the important response to Kubernetes.

/*Mutate function performs the actual mutation of pod spec*/
func (mutator Mutator) Mutate(req []byte) ([]byte, error) {
	admissionReviewResp := v1beta1.AdmissionReview{}
	admissionReviewReq := v1beta1.AdmissionReview{}
	var admissionResponse *v1beta1.AdmissionResponse

	_, _, err := deserializer.Decode(req, nil, &admissionReviewReq)

	if err == nil && admissionReviewReq.Request != nil {
		watchers, watchErr := loadWatchers(mutator.Namespace)
		if watchErr != nil {
			message := "Failed to find watchers"
			glog.Error(watchErr)

			admissionResponse = &v1beta1.AdmissionResponse{
				Result: &metav1.Status{
					Message: message,
				},
			}
			admissionReviewResp.Response = admissionResponse
			return json.Marshal(admissionReviewResp)
		}
		admissionResponse = mutate(&admissionReviewReq, watchers)
	} else {
		message := "Failed to decode request"

		if err != nil {
			message = fmt.Sprintf("message: %s err: %v", message, err)
		}

		admissionResponse = &v1beta1.AdmissionResponse{
			Result: &metav1.Status{
				Message: message,
			},
		}
	}

	admissionReviewResp.Response = admissionResponse
	return json.Marshal(admissionReviewResp)
}

It calls another function mutate. That actual performs the mutation to the incoming Pod spec.

func mutate(ar *v1beta1.AdmissionReview, watchers map[string]*watcher.Watcher) *v1beta1.AdmissionResponse {
	req := ar.Request

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

	pod, err := unMarshall(req)
	if err != nil {
		return errorResponse(ar.Request.UID, err)
	}
	glog.Infof("POD: %v\n", pod.ObjectMeta)

	if watcherNames, ok := shouldMutate(systemNameSpaces, &pod.ObjectMeta); ok {
		annotations := map[string]string{watcherInjectionStatusAnnotation: injectedValue}
		patchBytes, err := createPatch(&pod, watcherNames, watchers, annotations)
		if err != nil {
			return errorResponse(req.UID, err)
		}

		glog.Infof("AdmissionResponse: Patch: %v\n", string(patchBytes))
		if patchBytes != nil {
			pt := v1beta1.PatchTypeJSONPatch
			return &v1beta1.AdmissionResponse{
				UID:       req.UID,
				Allowed:   true,
				Patch:     patchBytes,
				PatchType: &pt,
			}
		}
	}

	return &v1beta1.AdmissionResponse{
		Allowed: true,
	}
}

It gets all the watchers in Pod’s namespace and checks to see if it is already injected. If not it creates a Patch to the Pod Spec.



func shouldMutate(ignoredList []string, metadata *metav1.ObjectMeta) ([]string, bool) {
	for _, namespace := range ignoredList {
		if metadata.Namespace == namespace {
			glog.Infof("Skipping mutation for [%v] in special namespace: [%v]", metadata.Name, metadata.Namespace)
			return nil, false
		}
	}

	annotations := metadata.GetAnnotations()
	if annotations == nil {
		annotations = map[string]string{}
	}

	if status, ok := annotations[watcherInjectionStatusAnnotation]; ok && strings.ToLower(status) == injectedValue {
		glog.Infof("Skipping mutation for [%v]. Has been mutated already", metadata.Name)
		return nil, false
	}

	if watchers, ok := annotations[watcherInjectionAnnotation]; ok {
		parts := strings.Split(watchers, ",")
		for i := range parts {
			parts[i] = strings.Trim(parts[i], " ")
		}

		if len(parts) > 0 {
			glog.Infof("watcher injection for %v/%v: watchers: %v", metadata.Namespace, metadata.Name, watchers)
			return parts, true
		}
	}

	glog.Infof("Skipping mutation for [%v]. No action required", metadata.Name)
	return nil, false
}

func createPatch(pod *corev1.Pod, watcherNames []string, watchers map[string]*watcher.Watcher, annotations map[string]string) ([]byte, error) {

	var patch []utils.PatchOperation
	var containers []corev1.Container
	var volumes []corev1.Volume
	var imagePullSecrets []corev1.LocalObjectReference
	count := 0

	for _, name := range watcherNames {
		if watcher, ok := watchers[name]; ok {
			sideCarCopy := watcher.DeepCopy()

			// copies all annotations in the pod with watcherNamespace as env
			// in the injected sidecar containers
			envVariables := getEnvToInject(pod.Annotations)
			if len(envVariables) > 0 {
				sideCarCopy.Spec.Container.Env = append(sideCarCopy.Spec.Container.Env, envVariables...)
			}

			if sideCarCopy.Spec.ConfigData != nil || sideCarCopy.Spec.ConfigBinaryData != nil {
				sideCarCopy.Spec.Container.EnvFrom = append(sideCarCopy.Spec.Container.EnvFrom, corev1.EnvFromSource{
					ConfigMapRef: &corev1.ConfigMapEnvSource{
						LocalObjectReference: corev1.LocalObjectReference{
							Name: sideCarCopy.Status.ConfigMapName,
						},
					},
				})
			}

			containers = append(containers, sideCarCopy.Spec.Container)
			if sideCarCopy.Spec.Volume != (interface{})(nil) {
				volumes = append(volumes, sideCarCopy.Spec.Volume)
			}
			// if sideCarCopy.Spec.ImagePullSecrets != (interface{})(nil) {
			// 	imagePullSecrets = append(imagePullSecrets, sideCarCopy.Spec.ImagePullSecrets)
			// }

			count++
		}
	}
	glog.Infof("Volumes: %d\n", len(volumes))
	if len(watcherNames) == count {
		patch = append(patch, utils.AddContainer(pod.Spec.Containers, containers, "/spec/containers")...)
		patch = append(patch, utils.AddVolume(pod.Spec.Volumes, volumes, "/spec/volumes")...)
		patch = append(patch, utils.AddImagePullSecrets(pod.Spec.ImagePullSecrets, imagePullSecrets, "/spec/imagePullSecrets")...)
		patch = append(patch, utils.UpdateAnnotation(pod.Annotations, annotations)...)

		return json.Marshal(patch)
	}

	return nil, nil // Just let the pod go through when no watchers found
}

The rest is simple helper functions you can check out in the code. You can also check out deployment YAMLs and other stuff there as well.

Recent posts

Categories

About