Skip to content

Instantly share code, notes, and snippets.

@patrick-east
Created May 28, 2020 18:02
Show Gist options
  • Select an option

  • Save patrick-east/01c67afcab9aaf59c4603fb2d41355fb to your computer and use it in GitHub Desktop.

Select an option

Save patrick-east/01c67afcab9aaf59c4603fb2d41355fb to your computer and use it in GitHub Desktop.
issue/2438
package library.kubernetes.admission.mutating
###########################################################################
# Implementation of the k8s admission control external webhook interface,
# combining validating and mutating admission controllers
###########################################################################
main = {
"apiVersion": "admission.k8s.io/v1beta1",
"kind": "AdmissionReview",
"response": response,
}
default response = {"allowed": true}
# non-patch response i.e. validation response
response = x {
count(patch) == 0
x := {
"allowed": false,
"status": {"reason": reason},
}
reason = concat(", ", deny)
reason != ""
}
# patch response i.e. mutating respone
response = x {
count(patch) > 0
# if there are missing leaves e.g. trying to add a label to something that doesn't
# yet have any, we need to create the leaf nodes as well
fullPatches := ensureParentPathsExist(cast_array(patch))
x := {
"allowed": true,
"patchType": "JSONPatch",
"patch": base64.encode(json.marshal(fullPatches)),
}
}
isValidRequest {
# not sure if this might be a race condition, it might get called before
# all the validation rules have been run
count(deny) == 0
}
isCreateOrUpdate {
isCreate
}
isCreateOrUpdate {
isUpdate
}
isCreate {
input.request.operation == "CREATE"
}
isUpdate {
input.request.operation == "UPDATE"
}
###########################################################################
# PATCH helpers
# Note: These rules assume that the input is an object
# not an AdmissionRequest, because labels and annotations
# can apply to various sub-objects within a request
# So from the context of an AdmissionRequest they need to
# be called like
# hasLabelValue("foo", "bar") with input as input.request.object
# or
# hasLabelValue("foo", "bar") with input as input.request.oldObject
###########################################################################
hasLabels(obj) {
obj.metadata.labels
}
hasLabel(obj, label) {
obj.metadata.labels[label]
}
hasLabelValue(obj, key, val) {
obj.metadata.labels[key] = val
}
hasAnnotations(obj) {
obj.metadata.annotations
}
hasAnnotation(obj, annotation) {
obj.metadata.annotations[annotation]
}
hasAnnotationValue(obj, key, val) {
obj.metadata.annotations[key] = val
}
###########################################################################
# makeLabelPatch creates a label patch
# Labels can exist on numerous child objects e.g. Deployment.template.metadata
# Use pathPrefix to specify a lower level object, or pass "" to select the
# top level object
# Note: pathPrefix should have a leading '/' but no trailing '/'
###########################################################################
makeSpecPatch(op, key, value, pathPrefix) = patchCode {
patchCode = {
"op": op,
"path": concat("/", [pathPrefix, "spec/template/spec/containers/0/", replace(key, "/", "~1")]),
"value": value,
}
}
makeLabelPatch(op, key, value, pathPrefix) = patchCode {
patchCode = {
"op": op,
"path": concat("/", [pathPrefix, "metadata/labels", replace(key, "/", "~1")]),
"value": value,
}
}
makeAnnotationPatch(op, key, value, pathPrefix) = patchCode {
patchCode = {
"op": op,
"path": concat("/", [pathPrefix, "metadata/annotations", replace(key, "/", "~1")]),
"value": value,
}
}
# Given array of JSON patches create and prepend new patches that create missing paths.
ensureParentPathsExist(patches) = result {
# Convert patches to a set
paths := {p.path | p := patches[_]}
# Compute all missing subpaths.
# Iterate over all paths and over all subpaths
# If subpath doesn't exist, add it to the set after making it a string
missingPaths := {sprintf("/%s", [concat("/", prefixPath)]) |
paths[path]
pathArray := split(path, "/")
pathArray[i] # walk over path
i > 0 # skip initial element
# array of all elements in path up to i
prefixPath := [pathArray[j] | pathArray[j]; j < i; j > 0] # j > 0: skip initial element
walkPath := [toWalkElement(x) | x := prefixPath[_]]
not inputPathExists(walkPath) with input as input.request.object
}
# Sort paths, to ensure they apply in correct order
ordered_paths := sort(missingPaths)
# Return new patches prepended to original patches.
# Don't forget to prepend all paths with a /
new_patches := [{"op": "add", "path": p, "value": {}} |
p := ordered_paths[_]
]
result := array.concat(new_patches, patches)
}
# Check that the given @path exists as part of the input object.
inputPathExists(path) {
walk(input, [path, _])
}
toWalkElement(str) = str {
not re_match("^[0-9]+$", str)
}
toWalkElement(str) = x {
re_match("^[0-9]+$", str)
x := to_number(str)
}
############################################################
# DENY rules
############################################################
# Don't allow container images with no version tag, or a version tag that doesn't contain at least one digit
missingImageVersion(imageName) {
not re_match(`.*:.*\d.*$`, imageName)
}
deny[msg] {
input.request.kind.kind = "Deployment"
badImages = {image |
image = input.request.object.spec.template.spec.containers[_].image
missingImageVersion(image, true)
}
count(badImages) > 0
names = concat(", ", badImages)
msg = sprintf("Container images must specify a version (%s)", [names])
}
############################################################
# PATCH rules
#
# Note: All patch rules should start with `isValidRequest` and `isCreateOrUpdate`
############################################################
patch[patchCode] {
isValidRequest
isCreateOrUpdate
input.request.kind.kind == "Deployment"
hasAnnotationValue(input.request.object, "test-mutation", "true")
patchCode = makeSpecPatch("add", "command/0", "test1", "")
}
package test.library.kubernetes.admission.mutating
test_mutating_cmd {
# Define some test input to use
test_input := {
"apiVersion": "admission.k8s.io/v1beta1",
"kind": "AdmissionReview",
"request": {
"dryRun": false,
"kind": {
"group": "apps",
"kind": "Deployment",
"version": "v1",
},
"name": "nginx-deployment",
"namespace": "default",
"object": {
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": {
"creationTimestamp": null,
"labels": {"app": "nginx"},
"name": "nginx-deployment",
"namespace": "default",
},
"spec": {
"progressDeadlineSeconds": 600,
"replicas": 1,
"revisionHistoryLimit": 10,
"selector": {"matchLabels": {"app": "nginx"}},
"strategy": {
"rollingUpdate": {
"maxSurge": "25%",
"maxUnavailable": "25%",
},
"type": "RollingUpdate",
},
"template": {
"metadata": {
"creationTimestamp": null,
"labels": {"app": "nginx"},
},
"spec": {
"containers": [{
"image": "nginx:1.14.2",
"imagePullPolicy": "IfNotPresent",
"name": "nginx",
"ports": [{
"containerPort": 80,
"protocol": "TCP",
}],
"resources": {},
"terminationMessagePath": "/dev/termination-log",
"terminationMessagePolicy": "File",
}],
"dnsPolicy": "ClusterFirst",
"restartPolicy": "Always",
"schedulerName": "default-scheduler",
"securityContext": {},
"terminationGracePeriodSeconds": 30,
},
},
},
"status": {},
},
"oldObject": null,
"operation": "CREATE",
"options": {
"apiVersion": "meta.k8s.io/v1",
"kind": "CreateOptions",
},
"requestKind": {
"group": "apps",
"kind": "Deployment",
"version": "v1",
},
"requestResource": {
"group": "apps",
"resource": "deployments",
"version": "v1",
},
"resource": {
"group": "apps",
"resource": "deployments",
"version": "v1",
},
"uid": "735a3b6e-ff36-43f3-b25d-f32706e808a0",
"userInfo": {
"groups": [
"system:masters",
"system:authenticated",
],
"username": "kubernetes-admin",
},
},
}
# Evaluate with our test input
patches := data.library.kubernetes.admission.mutating.patch with input as test_input
expected := {
"op": "add",
"path": "/spec/template/spec/containers/0//command~10",
"value": "test1",
}
# Assert that the expected patch is in the set of patches
patches[expected]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment