This repository aims to show you a basic boilerplate of an admission controller in go.
Kubernetes admission controllers
In a nutshell, Kubernetes admission controllers are plugins that govern and enforce how the cluster is used. They can be thought of as a gatekeeper that intercept (authenticated) API requests and may change the request object or deny the request altogether. The admission control process has two phases: the mutating phase is executed first, followed by the validating phase.
Kubernetes admission Controller Phases:
At the root level, we have defined some core structs.
In admission.go we have the main structs: AdmitFunc , Hook, and Result.
AdmitFunc is a function type that defines how to process an admission request. It is where you define
the validations or mutations for a specific request. You will see some examples in deployments and pods packages.
type AdmitFunc func(request *admission.AdmissionRequest) (*Result, error)Hook is representing the set of functions (AdmitFunc) for each operation in an admission webhook. When you create an admission
webhook, either validating or mutating, you have to define which operations you want to intervene.
type Hook struct {
Create AdmitFunc
Delete AdmitFunc
Update AdmitFunc
Connect AdmitFunc
}For example, you might want to create a validation webhook to apply a specific validation in the pods' creation.
For that, you have to create a ValidatingWebhookConfiguration as the following:
apiVersion: admissionregistration.k8s.io/v1beta1
kind: ValidatingWebhookConfiguration
...
webhooks:
- name: pod-validation.default.svc
clientConfig:
service:
...
rules:
- operations: ["CREATE"] # which operations you want to match.
...
resources: ["pods"]So, now you can create a Hook instance for that webhook, just setting the Create function. If your webhook handles
more operations, you should create the functions and set them for each operation.
You can see a better example in the deployments package.
// webhook with just one operation [CREATE]
hook := admissioncontroller.Hook{Create: myValidationFunction}
// webhook with multiple operations [CREATE,DELETE]
hook := admissioncontroller.Hook{Create: createValidation, Delete: deleteValidation}In patch.go we have the struct and function for JSON patch operation.
PatchOperation represents a JSON patch operation.
A mutating admission webhook may modify the incoming object in the request. This is done using the JSON patch format. See JSON patch documentation for more details.
You can see a better example in the function mutateCreate inside the pods package, where we use PatchOperation to set an annotation to the pod and
also, to add a sidecar container.
type PatchOperation struct {
Op string
Path string
From string
Value interface{}
}This package should contain all the validations and mutations for Pods resources.
For example, we have a function to reject a pod creation request if any of the pod's containers are using the latest tag.
func validateCreate() admissioncontroller.AdmitFunc {
return func(r *v1beta1.AdmissionRequest) (*admissioncontroller.Result, error) {
pod, err := parsePod(r.Object.Raw)
if err != nil {
return &admissioncontroller.Result{Msg: err.Error()}, nil
}
for _, c := range pod.Spec.Containers {
if strings.HasSuffix(c.Image, ":latest") {
return &admissioncontroller.Result{Msg: "You cannot use the tag 'latest' in a container."}, nil
}
}
return &admissioncontroller.Result{Allowed: true}, nil
}
}Also, we have the function mutateCreate, which is used in a MutatinWebhook, this function uses PatchOperations to
tell Kubernetes that have to make certain modifications to the pod creation, the mutations are:
- Using
JSON Patchoperation to add an annotations to the pod. - Using
JSON Patchoperation to replace the pod's containers adding a new container as a simple sidecar container. This is such a powerful feature. For example, Istio uses a similar approach to inject its sidecar containers into each pod.
func mutateCreate() admissioncontroller.AdmitFunc {
return func(r *v1beta1.AdmissionRequest) (*admissioncontroller.Result, error) {
var operations []admissioncontroller.PatchOperation
// ...
if pod.Namespace == "special" {
var containers []v1.Container
containers = append(containers, pod.Spec.Containers...)
sideC := v1.Container{
Name: "test-sidecar",
Image: "busybox:stable",
Command: []string{"sh", "-c", "while true; do echo 'I am a container injected by mutating webhook'; sleep 2; done"},
}
containers = append(containers, sideC)
operations = append(operations, admissioncontroller.ReplacePatchOperation("/spec/containers", containers))
}
metadata := map[string]string{"origin": "fromMutation"}
operations = append(operations, admissioncontroller.AddPatchOperation("/metadata/annotations", metadata))
return &admissioncontroller.Result{
Allowed: true,
PatchOps: operations,
}, nil
}
}The idea is that you can have a different package to handle one or multiple resources.
For example, you could have an annotations package to mutate or validate annotations cross resources such as Pod,
Deployments, DaemonSet, etc.
This package should contain all the validations and mutations for Deployments resources.
The current examples are:
- The
validateCreatefunction validates in a create operation if the deployment namespace isspecial. If it is, the function will reject the request. - The
validateDeletefunction validates in a delete operation if the deployment namespace isspecial-system. If it is, the function will reject the request.
Contains the http server and its handlers.
http.NewServer returns an HTTP server, and here we will register all our webhooks.
// NewServer creates and return a http.Server
func NewServer(port string) *http.Server {
// Instances hooks
podsValidation := pods.NewValidationHook()
deploymentValidation := deployments.NewValidationHook()
//....
// Routers
ah := newAdmissionHandler()
//...
mux.Handle("/validate/pods", ah.Serve(podsValidation))
mux.Handle("/validate/deployments", ah.Serve(deploymentValidation))
return &http.Server{
Addr: fmt.Sprintf(":%s", port),
Handler: mux,
}
}admissionHandler represents the HTTP handler for an admission webhook.
type admissionHandler struct {
decoder runtime.Decoder
}
// Serve returns an http.HandlerFunc for an admission webhook that contains all the
// logic to process an admission webhook request.
func (h *admissionHandler) Serve(hook admissioncontroller.Hook) http.HandlerFunc {
//...
}Contains all the files required to run a demo of this admission controller. Using the deploy.sh script, you can deploy
the admission controller in a k8s cluster.
Note: demo/deploy.sh is just for develop/test environment. It was not intended for production.
A cluster on which this example can be tested should have the admissionregistration.k8s.io/v1beta1 API enabled. You can verify that using the following command:
kubectl api-versions
...
admissionregistration.k8s.io/v1beta1
...
You should check that MutatingAdmissionWebhook and ValidatingAdmissionWebhook are activated in your cluster
inspecting the kube-apiserver
--enable-admission-plugins=..,MutatingAdmissionWebhook,ValidatingAdmissionWebhook.."
Run demo/deploy.sh will create a self-signed CA, a certificate and private for the server and the webhooks, also
will create the following resources: tls secret, Deployment, and all the Admission webhooks.
You can see all the created resources:
kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
admission-server ClusterIP 10.43.120.27 <none> 443/TCP 1h
kubectl get deployment
NAME READY UP-TO-DATE AVAILABLE AGE
admission-server 1/1 0 1 1h
kubectl get secret
NAME TYPE DATA AGE
admission-tls kubernetes.io/tls 2 1h
kubectl get mutatingwebhookconfigurations
NAME WEBHOOKS AGE
pod-mutation 1 1h
kubectl get validatingwebhookconfigurations
NAME WEBHOOKS AGE
deployment-validation 1 1h
pod-validation 1 1hThen we can use the different manifests inside demo/pods and demo/deployments to test the validations and mutations
that we have registered.
