Skip to content

Commit 5d21f87

Browse files
Ability to parse expressions that can filter and/or map a list
These expressions have the following format: ``` <reference> (("|" "->") <expr>) ``` <reference> is a reference to a list in the yaml that we're processing. What we have on the right hand side is a filter ("|") or a map ("->"). The <expr> is the function used to filter or map the list in the left hand side. We can have multiple filters and/or maps on the right. For example, we can see if all the side containers request less than 1 cpu using the following expression: ``` all: .spec.containers | .name != "main" -> .requests.cpu < 1 ```
1 parent 9d8235b commit 5d21f87

File tree

5 files changed

+113
-7
lines changed

5 files changed

+113
-7
lines changed

generic_k8s_webhook/config_parser/entrypoint.py

+1
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ def _parse_v1beta1(self, raw_list_webhook_config: dict) -> list[Webhook]:
110110
op_parser.ForEachParser,
111111
op_parser.MapParser,
112112
op_parser.ContainParser,
113+
op_parser.FilterParser,
113114
op_parser.ConstParser,
114115
op_parser.GetValueParser,
115116
],

generic_k8s_webhook/config_parser/expr_parser.py

+30-7
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,16 @@
77
from generic_k8s_webhook import utils
88

99
GRAMMAR_V1 = r"""
10-
?start: expr
10+
?start: expr | list_filter_map
11+
12+
?list_filter_map: reference filter_expr -> filterr
13+
| reference map_expr -> mapp
14+
| list_filter_map filter_expr -> filterr
15+
| list_filter_map map_expr -> mapp
16+
17+
?filter_expr: "|" expr
18+
19+
?map_expr: "->" expr
1120
1221
?expr: or
1322
@@ -33,12 +42,17 @@
3342
| product "*" atom -> mul
3443
| product "/" atom -> div
3544
36-
?atom: SIGNED_NUMBER -> number
37-
| ESCAPED_STRING -> const_string
38-
| REF -> ref
39-
| BOOL -> boolean
45+
?atom: signed_number
46+
| escaped_string
47+
| reference
48+
| bool
4049
| "(" expr ")"
4150
51+
signed_number: SIGNED_NUMBER -> number
52+
escaped_string: ESCAPED_STRING -> const_string
53+
reference: REF -> ref
54+
bool: BOOL -> boolean
55+
4256
BOOL: "true" | "false"
4357
REF: "$"? ("."(CNAME|"*"|INT))+
4458
@@ -113,6 +127,14 @@ def boolean(self, items):
113127
elem_bool = elem == "true"
114128
return op.Const(elem_bool)
115129

130+
def filterr(self, items):
131+
elems, operator = items
132+
return op.Filter(elems, operator)
133+
134+
def mapp(self, items):
135+
elems, operator = items
136+
return op.ForEach(elems, operator)
137+
116138

117139
def parse_ref(ref: str) -> op.GetValue:
118140
"""Parses a string that is a reference to some element within a json payload
@@ -186,12 +208,13 @@ def get_transformer(cls) -> Transformer:
186208
def main():
187209
parser = Lark(GRAMMAR_V1)
188210
# print(parser.parse('.key != "some string"').pretty())
189-
tree = parser.parse('"true" != "false"')
211+
tree = parser.parse('.spec.containers | .name != "main" -> .requests.cpu * 0.75')
190212
print(tree.pretty())
191213
trans = MyTransformerV1()
192214
new_op = trans.transform(tree)
193215
print(new_op)
194-
print(new_op.get_value([]))
216+
context = {"spec": {"containers": [{"name": "main"}, {"name": "side", "requests": {"cpu": 2}}]}}
217+
print(new_op.get_value([context]))
195218

196219

197220
if __name__ == "__main__":

generic_k8s_webhook/config_parser/operator_parser.py

+18
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,24 @@ def get_name(cls) -> str:
239239
return "map"
240240

241241

242+
class FilterParser(OperatorParser):
243+
@classmethod
244+
def get_name(cls) -> str:
245+
return "filter"
246+
247+
def parse(self, op_inputs: dict | list, path_op: str) -> operators.Filter:
248+
raw_elements = utils.must_get(op_inputs, "elements", f"In {path_op}, required 'elements'")
249+
elements = self.meta_op_parser.parse(raw_elements, f"{path_op}.elements")
250+
251+
raw_op = utils.must_get(op_inputs, "op", f"In {path_op}, required 'op'")
252+
op = self.meta_op_parser.parse(raw_op, f"{path_op}.op")
253+
254+
try:
255+
return operators.Filter(elements, op)
256+
except TypeError as e:
257+
raise ParsingException(f"Error when parsing {path_op}") from e
258+
259+
242260
class ContainParser(OperatorParser):
243261
@classmethod
244262
def get_name(cls) -> str:

generic_k8s_webhook/operators.py

+24
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,30 @@ def return_type(self) -> type | None:
273273
return list[self.op.return_type()]
274274

275275

276+
class Filter(Operator):
277+
def __init__(self, elements: Operator, op: Operator) -> None:
278+
self.elements = elements
279+
self.op = op
280+
281+
def get_value(self, contexts: list):
282+
elements = self.elements.get_value(contexts)
283+
if elements is None:
284+
return []
285+
286+
result_list = []
287+
for elem in elements:
288+
mapped_elem = self.op.get_value(contexts + [elem])
289+
if mapped_elem:
290+
result_list.append(elem)
291+
return result_list
292+
293+
def input_type(self) -> type | None:
294+
return None
295+
296+
def return_type(self) -> type | None:
297+
return list[self.op.return_type()]
298+
299+
276300
class Contain(Operator):
277301
def __init__(self, elements: Operator, elem: Operator) -> None:
278302
self.elements = elements

tests/conditions_test.yaml

+40
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,22 @@ test_suites:
236236
- maxCPU: 1
237237
- maxCPU: 2
238238
expected_result: false
239+
- name: FILTER
240+
tests:
241+
- schemas: [v1beta1]
242+
cases:
243+
- condition:
244+
filter:
245+
elements:
246+
getValue: .containers
247+
op: .maxCPU < 2
248+
context:
249+
- containers:
250+
- name: container1
251+
maxCPU: 1
252+
- name: container2
253+
maxCPU: 2
254+
expected_result: [{ name: container1, maxCPU: 1 }]
239255
- name: RAW_STR_EXPR
240256
tests:
241257
- schemas: [v1beta1]
@@ -258,3 +274,27 @@ test_suites:
258274
- maxCPU: 1
259275
- maxCPU: 2
260276
expected_result: true
277+
- name: LIST_FILTER_MAP_EXPR
278+
tests:
279+
- schemas: [v1beta1]
280+
cases:
281+
- condition: .containers | .name != "main"
282+
context:
283+
- containers:
284+
- name: main
285+
- name: istio
286+
expected_result: [name: istio]
287+
- condition: ".containers -> .maxCPU * 2"
288+
context:
289+
- containers:
290+
- maxCPU: 1
291+
- maxCPU: 2
292+
expected_result: [2, 4]
293+
- condition: .containers | .name != "main" -> .maxCPU > 1
294+
context:
295+
- containers:
296+
- name: main
297+
maxCPU: 1
298+
- name: istio
299+
maxCPU: 2
300+
expected_result: [true]

0 commit comments

Comments
 (0)