Created
May 28, 2020 18:02
-
-
Save patrick-east/01c67afcab9aaf59c4603fb2d41355fb to your computer and use it in GitHub Desktop.
issue/2438
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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", "") | |
| } | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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