Kubernetes Custom Controller - Part 2
Writing the Mutating Webhook
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.