Skip to content

Commit

Permalink
Ability to parse expressions that can filter and/or map a list
Browse files Browse the repository at this point in the history
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
```
  • Loading branch information
jordipiqueselles committed May 4, 2024
1 parent 9d8235b commit 5d21f87
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 7 deletions.
1 change: 1 addition & 0 deletions generic_k8s_webhook/config_parser/entrypoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ def _parse_v1beta1(self, raw_list_webhook_config: dict) -> list[Webhook]:
op_parser.ForEachParser,
op_parser.MapParser,
op_parser.ContainParser,
op_parser.FilterParser,
op_parser.ConstParser,
op_parser.GetValueParser,
],
Expand Down
37 changes: 30 additions & 7 deletions generic_k8s_webhook/config_parser/expr_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,16 @@
from generic_k8s_webhook import utils

GRAMMAR_V1 = r"""
?start: expr
?start: expr | list_filter_map
?list_filter_map: reference filter_expr -> filterr
| reference map_expr -> mapp
| list_filter_map filter_expr -> filterr
| list_filter_map map_expr -> mapp
?filter_expr: "|" expr
?map_expr: "->" expr
?expr: or
Expand All @@ -33,12 +42,17 @@
| product "*" atom -> mul
| product "/" atom -> div
?atom: SIGNED_NUMBER -> number
| ESCAPED_STRING -> const_string
| REF -> ref
| BOOL -> boolean
?atom: signed_number
| escaped_string
| reference
| bool
| "(" expr ")"
signed_number: SIGNED_NUMBER -> number
escaped_string: ESCAPED_STRING -> const_string
reference: REF -> ref
bool: BOOL -> boolean
BOOL: "true" | "false"
REF: "$"? ("."(CNAME|"*"|INT))+
Expand Down Expand Up @@ -113,6 +127,14 @@ def boolean(self, items):
elem_bool = elem == "true"
return op.Const(elem_bool)

def filterr(self, items):
elems, operator = items
return op.Filter(elems, operator)

def mapp(self, items):
elems, operator = items
return op.ForEach(elems, operator)


def parse_ref(ref: str) -> op.GetValue:
"""Parses a string that is a reference to some element within a json payload
Expand Down Expand Up @@ -186,12 +208,13 @@ def get_transformer(cls) -> Transformer:
def main():
parser = Lark(GRAMMAR_V1)
# print(parser.parse('.key != "some string"').pretty())
tree = parser.parse('"true" != "false"')
tree = parser.parse('.spec.containers | .name != "main" -> .requests.cpu * 0.75')
print(tree.pretty())
trans = MyTransformerV1()
new_op = trans.transform(tree)
print(new_op)
print(new_op.get_value([]))
context = {"spec": {"containers": [{"name": "main"}, {"name": "side", "requests": {"cpu": 2}}]}}
print(new_op.get_value([context]))


if __name__ == "__main__":
Expand Down
18 changes: 18 additions & 0 deletions generic_k8s_webhook/config_parser/operator_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,24 @@ def get_name(cls) -> str:
return "map"


class FilterParser(OperatorParser):
@classmethod
def get_name(cls) -> str:
return "filter"

def parse(self, op_inputs: dict | list, path_op: str) -> operators.Filter:
raw_elements = utils.must_get(op_inputs, "elements", f"In {path_op}, required 'elements'")
elements = self.meta_op_parser.parse(raw_elements, f"{path_op}.elements")

raw_op = utils.must_get(op_inputs, "op", f"In {path_op}, required 'op'")
op = self.meta_op_parser.parse(raw_op, f"{path_op}.op")

try:
return operators.Filter(elements, op)
except TypeError as e:
raise ParsingException(f"Error when parsing {path_op}") from e


class ContainParser(OperatorParser):
@classmethod
def get_name(cls) -> str:
Expand Down
24 changes: 24 additions & 0 deletions generic_k8s_webhook/operators.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,30 @@ def return_type(self) -> type | None:
return list[self.op.return_type()]


class Filter(Operator):
def __init__(self, elements: Operator, op: Operator) -> None:
self.elements = elements
self.op = op

def get_value(self, contexts: list):
elements = self.elements.get_value(contexts)
if elements is None:
return []

result_list = []
for elem in elements:
mapped_elem = self.op.get_value(contexts + [elem])
if mapped_elem:
result_list.append(elem)
return result_list

def input_type(self) -> type | None:
return None

def return_type(self) -> type | None:
return list[self.op.return_type()]


class Contain(Operator):
def __init__(self, elements: Operator, elem: Operator) -> None:
self.elements = elements
Expand Down
40 changes: 40 additions & 0 deletions tests/conditions_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,22 @@ test_suites:
- maxCPU: 1
- maxCPU: 2
expected_result: false
- name: FILTER
tests:
- schemas: [v1beta1]
cases:
- condition:
filter:
elements:
getValue: .containers
op: .maxCPU < 2
context:
- containers:
- name: container1
maxCPU: 1
- name: container2
maxCPU: 2
expected_result: [{ name: container1, maxCPU: 1 }]
- name: RAW_STR_EXPR
tests:
- schemas: [v1beta1]
Expand All @@ -258,3 +274,27 @@ test_suites:
- maxCPU: 1
- maxCPU: 2
expected_result: true
- name: LIST_FILTER_MAP_EXPR
tests:
- schemas: [v1beta1]
cases:
- condition: .containers | .name != "main"
context:
- containers:
- name: main
- name: istio
expected_result: [name: istio]
- condition: ".containers -> .maxCPU * 2"
context:
- containers:
- maxCPU: 1
- maxCPU: 2
expected_result: [2, 4]
- condition: .containers | .name != "main" -> .maxCPU > 1
context:
- containers:
- name: main
maxCPU: 1
- name: istio
maxCPU: 2
expected_result: [true]

0 comments on commit 5d21f87

Please sign in to comment.