Skip to content

Commit c6f2314

Browse files
committed
fix event ruler to not evaluate all payload
1 parent f8d3284 commit c6f2314

File tree

2 files changed

+92
-11
lines changed

2 files changed

+92
-11
lines changed

localstack-core/localstack/services/events/event_rule_engine.py

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def _evaluate_nested_event_pattern_on_dict(self, event_pattern, payload: dict) -
4343

4444
# TODO: maybe save/cache the flattened/expanded pattern?
4545
flat_pattern_conditions = self.flatten_pattern(event_pattern)
46-
flat_payloads = self.flatten_payload(payload)
46+
flat_payloads = self.flatten_payload(payload, flat_pattern_conditions)
4747

4848
return any(
4949
all(
@@ -59,9 +59,6 @@ def _evaluate_nested_event_pattern_on_dict(self, event_pattern, payload: dict) -
5959
for flat_pattern in flat_pattern_conditions
6060
)
6161

62-
def _build_event_branches(self, pattern, event):
63-
pass
64-
6562
def _evaluate_condition(self, value, condition, field_exists: bool):
6663
if not isinstance(condition, dict):
6764
return field_exists and value == condition
@@ -150,7 +147,7 @@ def _evaluate_cidr(condition: str, value: str) -> bool:
150147

151148
@staticmethod
152149
def _evaluate_wildcard(condition: str, value: str) -> bool:
153-
return re.match(re.escape(condition).replace("\\*", ".+") + "$", value)
150+
return bool(re.match(re.escape(condition).replace("\\*", ".+") + "$", value))
154151

155152
@staticmethod
156153
def _evaluate_numeric_condition(conditions: list, value: t.Any) -> bool:
@@ -240,18 +237,24 @@ def _traverse_event_pattern(obj, array=None, parent_key=None) -> list:
240237
return _traverse_event_pattern(nested_dict)
241238

242239
@staticmethod
243-
def flatten_payload(nested_dict: dict) -> list[dict]:
240+
def flatten_payload(payload: dict, patterns: list[dict]) -> list[dict]:
244241
"""
245242
Takes a dictionary as input and will output the dictionary on a single level.
246243
The dictionary can have lists containing other dictionaries, and one root level entry will be created for every
247-
item in a list.
244+
item in a list if it corresponds to the entries of the patterns.
248245
Input:
246+
payload:
249247
`{"field1": {
250248
"field2: [
251249
{"field3: "val1", "field4": "val2"},
252250
{"field3: "val3", "field4": "val4"},
253251
}
254252
]}`
253+
patterns:
254+
`[
255+
"field1.field2.field3": <condition>,
256+
"field1.field2.field4": <condition>,
257+
]`
255258
Output:
256259
`[
257260
{
@@ -263,16 +266,25 @@ def flatten_payload(nested_dict: dict) -> list[dict]:
263266
"field1.field2.field4": "val4"
264267
},
265268
]`
266-
:param nested_dict: a (nested) dictionary
269+
:param payload: a (nested) dictionary, the event payload
270+
:param patterns: the flattened patterns from the EventPattern (see flatten_pattern)
267271
:return: flatten_dict: a dictionary with no nested dict inside, flattened to a single level
268272
"""
273+
patterns_keys = {key for keys in patterns for key in keys}
274+
275+
def _is_key_in_patterns(key: str) -> bool:
276+
return key is None or any(pattern_key.startswith(key) for pattern_key in patterns_keys)
269277

270278
def _traverse(_object: dict, array=None, parent_key=None) -> list:
271279
if isinstance(_object, dict):
272280
for key, values in _object.items():
273-
# We update the parent key do that {"key1": {"key2": ""}} becomes "key1.key2"
281+
# We update the parent key so that {"key1": {"key2": ""}} becomes "key1.key2"
274282
_parent_key = f"{parent_key}.{key}" if parent_key else key
275-
array = _traverse(values, array, _parent_key)
283+
284+
# we make sure that we are building only the relevant parts of the payload related to the pattern
285+
# the payload could be very complex, and the pattern only applies to part of it
286+
if _is_key_in_patterns(_parent_key):
287+
array = _traverse(values, array, _parent_key)
276288

277289
elif isinstance(_object, list):
278290
if not _object:
@@ -282,7 +294,7 @@ def _traverse(_object: dict, array=None, parent_key=None) -> list:
282294
array = [{**item, parent_key: _object} for item in array]
283295
return array
284296

285-
return _traverse(nested_dict, array=[{}], parent_key=None)
297+
return _traverse(payload, array=[{}], parent_key=None)
286298

287299

288300
class EventPatternCompiler:
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import pytest
2+
3+
from localstack.services.events.event_rule_engine import EventRuleEngine
4+
5+
6+
class TestEventRuler:
7+
@pytest.mark.parametrize(
8+
"input_pattern,flat_patterns",
9+
[
10+
(
11+
{"filter": [{"anything-but": {"prefix": "type"}}]},
12+
[{"filter": [{"anything-but": {"prefix": "type"}}]}],
13+
),
14+
(
15+
{"field1": {"field2": {"field3": "val1", "field4": "val2"}}},
16+
[{"field1.field2.field3": "val1", "field1.field2.field4": "val2"}],
17+
),
18+
(
19+
{"$or": [{"field1": "val1"}, {"field2": "val2"}], "field3": "val3"},
20+
[{"field1": "val1", "field3": "val3"}, {"field2": "val2", "field3": "val3"}],
21+
),
22+
],
23+
ids=["simple", "simple-with-dots", "$or-pattern"],
24+
)
25+
def test_flatten_patterns(self, input_pattern, flat_patterns):
26+
engine = EventRuleEngine()
27+
assert engine.flatten_pattern(input_pattern) == flat_patterns
28+
29+
@pytest.mark.parametrize(
30+
"input_payload,flat_patterns,flat_payload",
31+
[
32+
(
33+
{"field1": "val1", "field3": "val3"},
34+
[{"field1": "val1", "field3": "val3"}, {"field2": "val2", "field3": "val3"}],
35+
[{"field1": "val1", "field3": "val3"}],
36+
),
37+
(
38+
{"f1": {"f2": {"f3": "v3"}}, "f4": "v4"},
39+
[{"f4": "test1"}],
40+
[{"f4": "v4"}],
41+
),
42+
(
43+
{"f1": {"f2": {"f3": {"f4": [{"f5": "v5"}]}, "f6": [{"f8": "v8"}]}}},
44+
[{"f1.f2.f3": "val1", "f1.f2.f4": "val2"}],
45+
[{}],
46+
),
47+
(
48+
{"f1": {"f2": {"f3": {"f4": [{"f5": "v5"}]}, "f6": [{"f7": "v7"}]}}},
49+
[{"f1.f2.f3.f4.f5": "val1", "f1.f2.f4": "val2"}],
50+
[{"f1.f2.f3.f4.f5": "v5"}],
51+
),
52+
(
53+
{"f1": {"f2": {"f3": {"f4": [{"f5": "v5"}]}, "f6": [{"f7": "v7"}]}}},
54+
[{"f1.f2.f3.f4.f5": "test1", "f1.f2.f6.f7": "test2"}],
55+
[{"f1.f2.f3.f4.f5": "v5", "f1.f2.f6.f7": "v7"}],
56+
),
57+
],
58+
ids=[
59+
"simple-with-or-pattern-flat",
60+
"simple-pattern-filter",
61+
"nested-payload-no-result",
62+
"nested-payload-1-match",
63+
"nested-payload-2-match",
64+
],
65+
)
66+
def test_flatten_payload(self, input_payload, flat_patterns, flat_payload):
67+
engine = EventRuleEngine()
68+
69+
assert engine.flatten_payload(input_payload, flat_patterns) == flat_payload

0 commit comments

Comments
 (0)