First up, quick refresher – what is a mutating admission controller?
Well it’s a nice feature in Kubernetes which lets you intercept objects when they’re created and make changes to them before they are deployed into the cluster.
Cool right? All those fiddly bits of YAML or hard to enforce company policies around network access, image stores you can and can’t use, they can all be enforced and FIXED automagically! (Like all magic caution is advised, choose wisely – queue Monty python gif)
So what’s the catch? Well without Open Policy Agent (OPA) you had to build out a web api to do the magic of changing the object then build/push an image and go through maintaining the solution. While you can write them quite easily now with solutions like KubeBuilder, or if you really love node I build one using that too, I wanted to see if OPA made things easier.
So say you want something more dynamic, flexible and a little easier to look after?
Today I’ve been having a play with it to work out if I could build a controller which would set a certain
nodeSelector on pods based on which
namespace they are deployed in.
I’ll go over this very broadly I highly recommend looking at the docs in detail before diving in, I lost quite a bit of time to not reading things properly before starting.
I won’t lie, getting used to the DSL (
rego) was painful for me, mainly because I came at it thinking it was going to be really like Golang. It does look quite like it but that’s where the similarity ends, it’s more functional/pattern matching and better suited to tersely making decisions based on data.
To counter the learning curve of
rego I have to say, as I’ve raised issues and contributions the maintainers have been super responsive and helpful (even when I’ve made some silly mistakes) and the docs are great with runnable samples to get started.
Lets talk more about what I built out.
Health warning: I’m new to this and still learning, I may get some of this wrong. Over the next few days I’ll be doing more testing and loop back to fix things up.
First up you need to process the input from the request
input and output a
main object which will be the response sent to the K8s API.
response on line 10 is the default which is returned if nothing else takes over.
response is a mix between and object definition and a set of
rules lines 18->25 (think assertions). If the
rules all match then the assignment takes place and
response becomes the
output object on line 49. After the assertions there is some plumbing which builds up the
response with the JSON patches K8s is looking, these set the
So how do these rules work then? Well they’re like little functions that check certain things.
isPod we do a simple equality check on the
kind of the request.
hasNodeSelector we check if the pod already has a node selector by first checking if the field exists, if it doesn’t the next check doesn’t happen, then checking how many items are in it.
getPoolForNamespace is a special case it takes an input
namespace then loops through the array defined as
namespaceToAgentPool and sees if any pools match the namespace of the pod. The magic happens here with the
namespaceToAgentPool[_] which means, roughly, check the rules against all of the items in that array.
So how do you know this stuff works? Well it’s got a nice testing framework you can use to check things are working how you expect. I found the
trace command super useful when running
opa test *.rego -v --explain full as it would prove the value of the items passed to it along with other information about the execution.
So all together now this looks like: https://github.com/open-policy-agent/contrib/pull/93
Hopefully this is useful. For more advanced stuff there is a library of shared helpers that can be pulled into
rego which is well worth a look: https://github.com/open-policy-agent/library/tree/master/kubernetes/mutating-admission nearly everything I’ve done here is largely based on simplifying those funcs and adding more comments.