Fix Exists
pattern matching with None values (#5134)
Exists matching works on the presence or absence of a field in the JSON of the event. The value of the field is not considered when evaluating a match.[1] * Add sentinel to distinguish between missing fields and existing fields with None values * Update event pattern matching tests to cover this specific use-case. * Add integrated test to cover this scenario within a larger context. [1]: https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html
This commit is contained in:
parent
f7c4d779c5
commit
30c2aeab29
@ -33,6 +33,9 @@ from uuid import uuid4
|
|||||||
|
|
||||||
from .utils import PAGINATION_MODEL
|
from .utils import PAGINATION_MODEL
|
||||||
|
|
||||||
|
# Sentinel to signal the absence of a field for `Exists` pattern matching
|
||||||
|
UNDEFINED = object()
|
||||||
|
|
||||||
|
|
||||||
class Rule(CloudFormationModel):
|
class Rule(CloudFormationModel):
|
||||||
Arn = namedtuple("Arn", ["service", "resource_type", "resource_id"])
|
Arn = namedtuple("Arn", ["service", "resource_type", "resource_id"])
|
||||||
@ -827,7 +830,7 @@ class EventPattern:
|
|||||||
return self._does_event_match(event, self._pattern)
|
return self._does_event_match(event, self._pattern)
|
||||||
|
|
||||||
def _does_event_match(self, event, pattern):
|
def _does_event_match(self, event, pattern):
|
||||||
items_and_filters = [(event.get(k), v) for k, v in pattern.items()]
|
items_and_filters = [(event.get(k, UNDEFINED), v) for k, v in pattern.items()]
|
||||||
nested_filter_matches = [
|
nested_filter_matches = [
|
||||||
self._does_event_match(item, nested_filter)
|
self._does_event_match(item, nested_filter)
|
||||||
for item, nested_filter in items_and_filters
|
for item, nested_filter in items_and_filters
|
||||||
@ -856,7 +859,7 @@ class EventPattern:
|
|||||||
filter_name, filter_value = list(pattern.items())[0]
|
filter_name, filter_value = list(pattern.items())[0]
|
||||||
if filter_name == "exists":
|
if filter_name == "exists":
|
||||||
is_leaf_node = not isinstance(item, dict)
|
is_leaf_node = not isinstance(item, dict)
|
||||||
leaf_exists = is_leaf_node and item is not None
|
leaf_exists = is_leaf_node and item is not UNDEFINED
|
||||||
should_exist = filter_value
|
should_exist = filter_value
|
||||||
return leaf_exists if should_exist else not leaf_exists
|
return leaf_exists if should_exist else not leaf_exists
|
||||||
if filter_name == "prefix":
|
if filter_name == "prefix":
|
||||||
|
@ -23,6 +23,7 @@ def test_event_pattern_with_nested_event_filter():
|
|||||||
def test_event_pattern_with_exists_event_filter():
|
def test_event_pattern_with_exists_event_filter():
|
||||||
foo_exists = EventPattern.load(json.dumps({"detail": {"foo": [{"exists": True}]}}))
|
foo_exists = EventPattern.load(json.dumps({"detail": {"foo": [{"exists": True}]}}))
|
||||||
assert foo_exists.matches_event({"detail": {"foo": "bar"}})
|
assert foo_exists.matches_event({"detail": {"foo": "bar"}})
|
||||||
|
assert foo_exists.matches_event({"detail": {"foo": None}})
|
||||||
assert not foo_exists.matches_event({"detail": {}})
|
assert not foo_exists.matches_event({"detail": {}})
|
||||||
# exists filters only match leaf nodes of an event
|
# exists filters only match leaf nodes of an event
|
||||||
assert not foo_exists.matches_event({"detail": {"foo": {"bar": "baz"}}})
|
assert not foo_exists.matches_event({"detail": {"foo": {"bar": "baz"}}})
|
||||||
@ -31,6 +32,7 @@ def test_event_pattern_with_exists_event_filter():
|
|||||||
json.dumps({"detail": {"foo": [{"exists": False}]}})
|
json.dumps({"detail": {"foo": [{"exists": False}]}})
|
||||||
)
|
)
|
||||||
assert not foo_not_exists.matches_event({"detail": {"foo": "bar"}})
|
assert not foo_not_exists.matches_event({"detail": {"foo": "bar"}})
|
||||||
|
assert not foo_not_exists.matches_event({"detail": {"foo": None}})
|
||||||
assert foo_not_exists.matches_event({"detail": {}})
|
assert foo_not_exists.matches_event({"detail": {}})
|
||||||
assert foo_not_exists.matches_event({"detail": {"foo": {"bar": "baz"}}})
|
assert foo_not_exists.matches_event({"detail": {"foo": {"bar": "baz"}}})
|
||||||
|
|
||||||
|
@ -249,3 +249,72 @@ def test_send_to_sqs_queue_with_custom_event_bus():
|
|||||||
|
|
||||||
body = json.loads(response["Messages"][0]["Body"])
|
body = json.loads(response["Messages"][0]["Body"])
|
||||||
body["detail"].should.equal({"key": "value"})
|
body["detail"].should.equal({"key": "value"})
|
||||||
|
|
||||||
|
|
||||||
|
@mock_events
|
||||||
|
@mock_logs
|
||||||
|
def test_moto_matches_none_value_with_exists_filter():
|
||||||
|
pattern = {
|
||||||
|
"source": ["test-source"],
|
||||||
|
"detail-type": ["test-detail-type"],
|
||||||
|
"detail": {
|
||||||
|
"foo": [{"exists": True}],
|
||||||
|
"bar": [{"exists": True}],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
logs_client = boto3.client("logs", region_name="eu-west-1")
|
||||||
|
log_group_name = "test-log-group"
|
||||||
|
logs_client.create_log_group(logGroupName=log_group_name)
|
||||||
|
|
||||||
|
events_client = boto3.client("events", region_name="eu-west-1")
|
||||||
|
event_bus_name = "test-event-bus"
|
||||||
|
events_client.create_event_bus(Name=event_bus_name)
|
||||||
|
|
||||||
|
rule_name = "test-event-rule"
|
||||||
|
events_client.put_rule(
|
||||||
|
Name=rule_name,
|
||||||
|
State="ENABLED",
|
||||||
|
EventPattern=json.dumps(pattern),
|
||||||
|
EventBusName=event_bus_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
events_client.put_targets(
|
||||||
|
Rule=rule_name,
|
||||||
|
EventBusName=event_bus_name,
|
||||||
|
Targets=[
|
||||||
|
{
|
||||||
|
"Id": "123",
|
||||||
|
"Arn": f"arn:aws:logs:eu-west-1:{ACCOUNT_ID}:log-group:{log_group_name}",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
events_client.put_events(
|
||||||
|
Entries=[
|
||||||
|
{
|
||||||
|
"EventBusName": event_bus_name,
|
||||||
|
"Source": "test-source",
|
||||||
|
"DetailType": "test-detail-type",
|
||||||
|
"Detail": json.dumps({"foo": "123", "bar": "123"}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"EventBusName": event_bus_name,
|
||||||
|
"Source": "test-source",
|
||||||
|
"DetailType": "test-detail-type",
|
||||||
|
"Detail": json.dumps({"foo": None, "bar": "123"}),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
events = sorted(
|
||||||
|
logs_client.filter_log_events(logGroupName=log_group_name)["events"],
|
||||||
|
key=lambda x: x["eventId"],
|
||||||
|
)
|
||||||
|
event_details = [json.loads(x["message"])["detail"] for x in events]
|
||||||
|
|
||||||
|
event_details.should.equal(
|
||||||
|
[
|
||||||
|
{"foo": "123", "bar": "123"},
|
||||||
|
{"foo": None, "bar": "123"},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user