-
Notifications
You must be signed in to change notification settings - Fork 33
/
main.rego
200 lines (160 loc) · 4.89 KB
/
main.rego
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
package library.kubernetes.admission.mutating
###########################################################################
# Implementation of the k8s admission control external webhook interface,
# combining validating and mutating admission controllers
###########################################################################
default apiVersion = "admission.k8s.io/v1beta1"
apiVersion = input.apiVersion
# missing uid defaults to empty string
# this will produce a warning in kube apiserver logs
default response_uid = ""
response_uid = input.request.uid
main = {
"apiVersion": apiVersion,
"kind": "AdmissionReview",
"response": response,
}
# non-patch response i.e. validation response
response = x {
count(patch) == 0
x := {
"allowed": false,
"uid": response_uid,
"status": {"reason": reason},
}
reason = concat(", ", deny)
reason != ""
}
# patch response i.e. mutating respone
else = 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,
"uid": response_uid,
"patchType": "JSONPatch",
"patch": base64.encode(json.marshal(fullPatches)),
}
}
# default response
else = x {
x := {
"allowed": true,
"uid": response_uid,
}
}
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 '/'
###########################################################################
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)
}
# Dummy deny and patch to please the compiler
deny[msg] {
input.request.kind == "AdmissionReview"
msg = "Input must be Kubernetes AdmissionRequest"
}
patch[patchCode] {
input.kind == "ThisHadBetterNotBeARealKind"
patchCode = {}
}