DEV Community

Muaaz Saleem
Muaaz Saleem

Posted on • Originally published at muaazsaleem.com

Parsing Admission Requests in a Validating Admission Webhook

Last time I introduced the 2 main parts of a Validating Admission Webhook. The Webserver and the ValidatingAdmissionWebhookConfiguration.

This time I'll expand a bit on the Webserver. Specifically, parsing Admission Requests in a Validating Admission Webhook.

The HTTP Handler

The webserver just needs a POST endpoint, where the API Server can post Admission Requests to.

handler.Handle("/routegroups", admission.Handler())
Enter fullscreen mode Exit fullscreen mode

admission.Handler is just a go func

func Handler() http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // ...
    }
}
Enter fullscreen mode Exit fullscreen mode

Having a function that returns a http.HandlerFunc is helpful because this way I can also parameterize admission.Handler for each resource I want to validate e.g:

handler.Handle("/deployments", admission.Handler("deployments"))
Enter fullscreen mode Exit fullscreen mode

More on this in a future blog post.

Parsing an Admission Request

Admission requests are just HTTP Post requests with a JSON body of the form:

{
  "apiVersion": "admission.k8s.io/v1",
  "kind": "AdmissionReview",
  "request": {
    // ...
  }
}
Enter fullscreen mode Exit fullscreen mode

Therefore, we can just use json.Unmarshal to decode them:

import (
    "encoding/json"
    admissionsv1 "k8s.io/api/admission/v1"
)
// ...
review := admissionsv1.AdmissionReview{}
err = json.Unmarshal(body, &review)
if err != nil {
    log.Errorf("Failed to parse request: %v", err)
    w.WriteHeader(http.StatusBadRequest)
    return
}
Enter fullscreen mode Exit fullscreen mode

Note how I am just using the upstream AdmissionReview type from k8s.io/api/admission/v1 here. You can find other Kubernetes types in the k8s.io/api repo as well.

Responding to an Admission Request i.e encoding Admission.Response

Encoding is just a json.Marshal as well.

import (
// ...
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

    resp, err := json.Marshal(admissionsv1.AdmissionReview{
        TypeMeta: metav1.TypeMeta{
            Kind:       "AdmissionReview",
            APIVersion: "admission.k8s.io/v1",
        },
        Response: response,
    })
Enter fullscreen mode Exit fullscreen mode

There are a couple important things to keep in mind here.

The response UID must match the request

response = &admissionsv1.AdmissionResponse{
    UID:     &review.UID,
// ...
}
Enter fullscreen mode Exit fullscreen mode

Otherwise, the API Server won't know which admission request my response belongs to.

The TypeMeta fields must be present in the response

admissionsv1.AdmissionReview{
        TypeMeta: metav1.TypeMeta{
            Kind:       "AdmissionReview",
            APIVersion: "admission.k8s.io/v1",
        },
        Response: response,
    })
Enter fullscreen mode Exit fullscreen mode

I learnt this the hard way. If the Kind and APIVersion aren't specified for v1 resources i.e v1.AdmissionReview, the Admission requests will just fail.

Util Funcs

I found it useful to have writeResponse and errorResponse functions.

Writing Response

import (
// ...
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func writeResponse(
    writer http.ResponseWriter, 
    response *admissionsv1.AdmissionResponse,
    ) {
    resp, err := json.Marshal(admissionsv1.AdmissionReview{
        TypeMeta: metav1.TypeMeta{
            Kind:       "AdmissionReview",
            APIVersion: "admission.k8s.io/v1",
        },
        Response: response,
    })
    if err != nil {
        log.Errorf("failed to serialize response: %v", err)
        writer.WriteHeader(http.StatusInternalServerError)
        return
    }
    if _, err := writer.Write(resp); err != nil {
        log.Errorf("failed to write response: %v", err)
    }
}
Enter fullscreen mode Exit fullscreen mode

Writing errors

import (
// ...
    "k8s.io/apimachinery/pkg/types"
)

func errorResponse(uid types.UID, err error) *admissionsv1.AdmissionResponse {
    return &admissionsv1.AdmissionResponse{
        Allowed: false,
        UID:     uid,
        Result: &metav1.Status{
            Message: err.Error(),
        },
    }
}
Enter fullscreen mode Exit fullscreen mode

When I started writing a validating admission webhook for the first
time, I was very confused about the various moving parts. Hopefully this post shows that in essence, a validating admission webhook is just a go webserver.

Further Resources

You can find an abridged version of admission.Handler under "Reference". For a complete example of a validating admission webhook being used in production, checkout the validating admission webhook in Skipper, our HTTP reverse proxy.

Thanks to my teammates Arpad & Sandor for helping me contribute a validating admission webhook to Skipper!

Reference

The following is just an abridged version of admission.go from the validating admission webhook in Skipper.

package admission

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
    "net/http"
    "time"

    log "github.com/sirupsen/logrus"
    "github.com/zalando/skipper/dataclients/kubernetes/definitions"
    admissionsv1 "k8s.io/api/admission/v1"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/apimachinery/pkg/types"
)

func Handler(admitter Admitter) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        admitterName := admitter.Name()

        if r.Method != "POST" || r.Header.Get("Content-Type") != "application/json" {
            w.WriteHeader(http.StatusMethodNotAllowed)
            return
        }

        body, err := ioutil.ReadAll(r.Body)
        if err != nil {
            log.Errorf("Failed to read request: %v", err)
            w.WriteHeader(http.StatusInternalServerError)
            return
        }
        log.Debugln("request received", r.Method, r.URL.Path, r.Header, string(body))

        review := admissionsv1.AdmissionReview{}
        err = json.Unmarshal(body, &review)
        if err != nil {
            log.Errorf("Failed to parse request: %v", err)
            w.WriteHeader(http.StatusBadRequest)
            return
        }

        operationInfo := fmt.Sprintf(
            "%s %s %s/%s",
            review.Request.Operation,
            review.Request.Kind,
            review.Request.Namespace,
            extractName(review.Request),
        )

        gvr := review.Request.Resource
        group := gvr.Group
        if group == "" {
            group = "zalando.org"
        }

        start := time.Now()
        defer admissionDuration.With(labelValues).
            Observe(float64(time.Since(start)) / float64(time.Second))

        admResp, err := admitter.Admit(review.Request)
        if err != nil {
            log.Errorf("Rejected %s: %v", operationInfo, err)
            writeResponse(w, errorResponse(review.Request.UID, err))
            return
        }

        log.Debugf("Allowed %s", operationInfo)
        writeResponse(w, admResp)
    }
}

func writeResponse(writer http.ResponseWriter, response *admissionsv1.AdmissionResponse) {
    resp, err := json.Marshal(admissionsv1.AdmissionReview{
        TypeMeta: metav1.TypeMeta{
            Kind:       "AdmissionReview",
            APIVersion: "admission.k8s.io/v1",
        },
        Response: response,
    })
    if err != nil {
        log.Errorf("failed to serialize response: %v", err)
        writer.WriteHeader(http.StatusInternalServerError)
        return
    }
    if _, err := writer.Write(resp); err != nil {
        log.Errorf("failed to write response: %v", err)
    }
}

func errorResponse(uid types.UID, err error) *admissionsv1.AdmissionResponse {
    return &admissionsv1.AdmissionResponse{
        Allowed: false,
        UID:     uid,
        Result: &metav1.Status{
            Message: err.Error(),
        },
    }
}

func extractName(request *admissionsv1.AdmissionRequest) string {
    if request.Name != "" {
        return request.Name
    }

    obj := metav1.PartialObjectMetadata{}
    if err := json.Unmarshal(request.Object.Raw, &obj); err != nil {
        log.Warnf("failed to parse object: %v", err)
        return "<unknown>"
    }

    if obj.Name != "" {
        return obj.Name
    }
    if obj.GenerateName != "" {
        return obj.GenerateName + "<generated>"
    }
    return "<unknown>"
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)