-
-
Notifications
You must be signed in to change notification settings - Fork 4.5k
Feature: Eventbridge v2: CRUD #10613
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
LocalStack Community integration with Pro 2 files ± 0 2 suites ±0 1h 31m 1s ⏱️ +26s Results for commit acc72c8. ± Comparison against base commit c860285. This pull request removes 6 and adds 25 tests. Note that renamed tests count towards both.♻️ This comment has been updated with latest results. |
joe4dev
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice foundation 👏 👏👏
Interesting new composite pattern to handle persistence while using high-level business objects. Looking forward to learning more about this experience 📈
I guess one of the main challenges is keeping the dataclasses and business objects perfectly synced (don't create, delete, or update one in some different way).
acee7ad to
6820b69
Compare
32ff150 to
1808090
Compare
| reason="V1 provider does not support this feature", | ||
| ) | ||
| @pytest.mark.parametrize("regions", [["us-east-1"], ["us-east-1", "us-west-1", "eu-central-1"]]) | ||
| def test_create_list_describe_delete_custom_event_buses( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These tests seem flaky and we should fix them: https://app.circleci.com/pipelines/github/localstack/localstack/24319/workflows/83b9939c-a5e2-4b19-becd-2c40030242d4/jobs/201263/tests
That's related to the previous concurrency comment, because snapshotting lists can be flaky when other tests don't clean up properly (as you guessed Max).
We can xFail them to move on but should fix them ultimately.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
v2 tests all clean up correctly so far, and this is a core test for the v2 functionality this is why I would strongly prefer to keep it in for the v2 provider, non the less it is already a part of the refactor tests backlog.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as discussed, we cannot reasonably assume that all current and future tests perform perfect cleanup, and therefore we should take pre-cautions such that these tests don't flake.
dfangl
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not really happy with the whole EventBusService and RuleService yet, but I get the workaround around the persistence restrictions while still getting a somewhat close cohesion between data and logic. I think we can continue to talk and reiterate over it after this is merged as well.
I have some additional concerns about the service principal set to the target service, and some additional comments. Sorry the review took so long, it is quite a long PR and a lot of related reading.
I think for further work on the provider, it might be nice getting smaller PRs, instead of these big bangs :)
| self._event_bus_services_store: EventBusServiceDict = {} | ||
| self._rule_services_store: RuleServiceDict = {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No state lives in this "Service" objects, why do we store them? For persistence, we would need to regenerate them on restore, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this is mostly preparation for the future implementation dealing with persistence. My goal is to add an additional abstraction on top of the AccountRegionBundle, that combines state that is persistent and logic into an object, so that the provider only sees this object.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As discussed, the question was less related to the necessity of the ...Service objects themselves, but whether we need to store them if there is no state in them. We can keep it for now, and see in a future iteration how they will be handled.
localstack/services/events/target.py
Outdated
| class KinesisTargetService(TargetService): | ||
| def send_event(self, event): | ||
| kinesis_client = self.client.kinesis.request_metadata( | ||
| service_principal=self.service, source_arn=self.rule_arn |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The service principal should always be events, it is the service principal making the request, not the target.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
localstack/services/events/target.py
Outdated
| role_arn=role_arn, service_principal=ServicePrincipal.events, region_name=region_name | ||
| ) | ||
| except ValueError as e: | ||
| LOG.debug(f"Could not connect with assumed role {role_arn}. Error: {e}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general, services get either an role passed, or use their service principal for these kind of requests. Some also support both, if you do not supply a role, it will use the service principal.
I am not sure what events supports, and it might differ by target, but we should definitely check that and only allow the right method.
| response = aws_client.events.list_rules( | ||
| Limit=int(count / 2) + 2, NextToken=response["NextToken"] | ||
| ) | ||
| snapshot.match("list-rules-limit-next-token", response) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess there are ways around it. I think the method supports filtering by nameprefix or event bus? If we make those unique, we can avoid concurrent / leftover requests making the tests flake, but still snapshot it.
| class EventBusService: | ||
| def __init__( | ||
| self, | ||
| name: EventBusName, | ||
| region: str, | ||
| account_id: str, | ||
| event_source_name: Optional[str] = None, | ||
| tags: Optional[TagList] = None, | ||
| policy: Optional[str] = None, | ||
| rules: Optional[RuleDict] = None, | ||
| ): | ||
| self.event_bus = EventBus( | ||
| name, | ||
| region, | ||
| account_id, | ||
| event_source_name, | ||
| tags, | ||
| policy, | ||
| rules, | ||
| ) | ||
|
|
||
| @property | ||
| def arn(self): | ||
| return self.event_bus.arn | ||
|
|
||
|
|
||
| EventBusServiceDict = dict[Arn, EventBusService] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What value does this class add? Is this a preparation for some future part of the provider, or just to match the RuleService?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this is mostly preparation for the future implementation dealing with persistence. My goal is to add an additional abstraction on top of the AccountRegionBundle, that combines state that is persistent and logic into an object, so that the provider only sees this object.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The question was more related to this specific class, since it does not feature any additional logic other than holding the event bus state object. As discussed in slack, it is part a remnant of early implementation, and will be used when more logic regarding event busses arrives. Fine with me to keep it for now.
localstack/services/events/target.py
Outdated
| return client | ||
|
|
||
|
|
||
| class TargetService(ABC): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not really happy with the naming here. Could we perhaps get rid of the Service? I am aware of the overlap with Target from the API spec then, but we could maybe call it RuleTarget or EventTarget or even TargetPublisher TargetSender or something the likes, but I am not a fan of TargetService, personally.
Since this is more a naming debate, please do not let this hold back the merge.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 for TargetSender in line with EventBridge Pipes
It implements a send method, so sender seems appropriate
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for targets it indeed makes much more sense to call it sender, renamed in: 344f54c
4e2400c to
c9d6ca3
Compare
dfangl
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks good for a first iteration for me now! Please also let Joel take a final look, but no more blockers from my side!
| class EventBusService: | ||
| def __init__( | ||
| self, | ||
| name: EventBusName, | ||
| region: str, | ||
| account_id: str, | ||
| event_source_name: Optional[str] = None, | ||
| tags: Optional[TagList] = None, | ||
| policy: Optional[str] = None, | ||
| rules: Optional[RuleDict] = None, | ||
| ): | ||
| self.event_bus = EventBus( | ||
| name, | ||
| region, | ||
| account_id, | ||
| event_source_name, | ||
| tags, | ||
| policy, | ||
| rules, | ||
| ) | ||
|
|
||
| @property | ||
| def arn(self): | ||
| return self.event_bus.arn | ||
|
|
||
|
|
||
| EventBusServiceDict = dict[Arn, EventBusService] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The question was more related to this specific class, since it does not feature any additional logic other than holding the event bus state object. As discussed in slack, it is part a remnant of early implementation, and will be used when more logic regarding event busses arrives. Fine with me to keep it for now.
| self._event_bus_services_store: EventBusServiceDict = {} | ||
| self._rule_services_store: RuleServiceDict = {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As discussed, the question was less related to the necessity of the ...Service objects themselves, but whether we need to store them if there is no state in them. We can keep it for now, and see in a future iteration how they will be handled.
| def _initialize_client(self) -> BaseClient: | ||
| """Initializes internal botocore client. | ||
| If a role from a target is provided, the client will be initialized with the assumed role. | ||
| If no role is provided the client will be initialized with the account ID and region. | ||
| In both cases event bridge is requested as service principal""" | ||
| service_principal = ServicePrincipal.events | ||
| if role_arn := self.target.get("role_arn"): | ||
| # assumed role sessions expires after 6 hours in AWS, currently no expiration in LocalStack | ||
| client_factory = connect_to.with_assumed_role( | ||
| role_arn=role_arn, service_principal=service_principal, region_name=self.region | ||
| ) | ||
| else: | ||
| client_factory = connect_to(aws_access_key_id=self.account_id, region_name=self.region) | ||
| client = client_factory.get_client(self.service) | ||
| client = client.request_metadata( | ||
| service_principal=service_principal, source_arn=self.rule_arn | ||
| ) | ||
| return client |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks better, thanks! As discussed, it is necessary to set the events service principal to make sure all requests are recognized as coming from events :)
joe4dev
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM for the first iteration 👍 Please double-check whether test_create_list_describe_delete_custom_event_buses is multi-region compatible (see comment) before merging!
Thanks for addressing our comments and tracking pending comments in backlog items 🙏 .
Let's iterate quickly toward v1 parity in smaller more manageable steps 📈 🚀🚀🚀
|
|
||
| def _validate_input(self, target: Target): | ||
| super()._validate_input(target) | ||
| if not collections.get_safe(target, "$.RoleArn"): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: can't we use a simpler get here?
localstack/services/events/target.py
Outdated
| ] | ||
| ) | ||
|
|
||
| def _validate_input(self, target: Target): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This RoleArn validation looks redundant. Consider moving it to the TargetService base class in a follow-up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it is only required for specific services, this is why it is not in the base class.
| def _initialize_client(self) -> BaseClient: | ||
| """Initializes internal botocore client. | ||
| If a role from a target is provided, the client will be initialized with the assumed role. | ||
| If no role is provided the client will be initialized with the account ID and region. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In which cases does this apply? Isn't a role ARN mandatory for EventBridge?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
role arn is only required for EC2 instances, Kinesis Data Streams, Step Functions state machines and API Gateway APIs
| reason="V1 provider does not support this feature", | ||
| ) | ||
| @pytest.mark.parametrize("regions", [["us-east-1"], ["us-east-1", "us-west-1", "eu-central-1"]]) | ||
| def test_create_list_describe_delete_custom_event_buses( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as discussed, we cannot reasonably assume that all current and future tests perform perfect cleanup, and therefore we should take pre-cautions such that these tests don't flake.
| not is_v2_provider() and not is_aws_cloud(), | ||
| reason="V1 provider does not support this feature", | ||
| ) | ||
| @pytest.mark.parametrize("regions", [["us-east-1"], ["us-east-1", "us-west-1", "eu-central-1"]]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@maxhoheiser master?
https://docs.localstack.cloud/contributing/multi-account-region-testing/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
all tests use prefixes or custom event buses where list operations are involved to limit it to the current test scope.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I meant region randomization here, which surfaced some issues. Glad we caught them 👍
Your answer here seems related to another comment about list snapshots.
| response = events.list_event_buses(NamePrefix=bus_name) | ||
| snapshot.match("list-event-buses-prefix-complete-name", response) | ||
|
|
||
| response = events.list_event_buses(NamePrefix=bus_name.split("-")[0]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I'd prefer hardcoded clarity here
| snapshot.match("list-event-buses-limit-next-token", response) | ||
|
|
||
| @markers.aws.unknown | ||
| @pytest.mark.skipif(is_aws_cloud(), reason="not validated") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
super nit: there's a needs_fixing aws marker 💡
|
|
||
| @markers.aws.validated | ||
| def test_delete_rule_with_targets( | ||
| self, put_rule, sqs_create_queue, sqs_get_queue_arn, aws_client, snapshot |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good to know: the put_rule factory cleanup fails if the test case deletes the rule and pollutes the CI logs. More of a comment because we're doing quite badly everywhere with this.
| snapshot.add_transformer(snapshot.transform.regex(bus_name, "<bus-name>")) | ||
|
|
||
| for region in regions: | ||
| events = aws_client_factory(region_name=region).events |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess this overwrites the randomized region and should work, but it's probably better to double-check this assumption. Just be aware that any other fixture-based client could use a randomized region. That's probably worth mentioning in a comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I manually run the CircleCi pipeline with the flag randomize-aws-credentials set to True, and it indeed failed to correctly template replace the region in the snapshot.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed template replacement of region in snapshots for setting a region different than the default region in: 82420ae
a5ce64c to
24ed318
Compare
82420ae to
acc72c8
Compare
Motivation
This PR introduces the core structure of the new localstack native implementation of event bridge.
Furthermore, it implements the CRUD functionality based on snapshots of verified AWS tests. A clean pattern for keeping data stored in the AccountRegionBundle store separate from related functionality is required. Separation of different sender functionality based on the target should be handled more efficiently than in a if else chain.
These API operations are implemented:
Changes
Limitations
No pattern matching is implemented yet, thus all events are matched to all rules and sed to all targets.