Repository: localstack/localstack

Issue:
bug: events.PutEvents fails with custom bus name using boto3
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Current Behavior

The LocalStack Docker image >=2.0.0 throws a 500 `InternalError` when calling the `events.PutEvents` operation via boto3 **with a custom event bus name** (i.e. the event bus name is not `default`).

(docker-compose logs)
```
ERROR --- [   asgi_gw_0] l.aws.handlers.logging     : exception during call chain: 'customBusName'
 INFO --- [   asgi_gw_0] localstack.request.aws     : AWS events.PutEvents => 500 (InternalError)
```

(boto3 error traceback)
```
Traceback (most recent call last):
  File "eb_publish.py", line 6, in <module>
    response = eb.put_events(
  File "/Users/sangeeta.jadoonanan/.pyenv/versions/hydra/lib/python3.8/site-packages/botocore/client.py", line 530, in _api_call
    return self._make_api_call(operation_name, kwargs)
  File "/Users/sangeeta.jadoonanan/.pyenv/versions/hydra/lib/python3.8/site-packages/botocore/client.py", line 960, in _make_api_call
    raise error_class(parsed_response, operation_name)
botocore.exceptions.ClientError: An error occurred (InternalError) when calling the PutEvents operation (reached max retries: 4): exception while calling events.PutEvents: 'customBusName'
```

### Expected Behavior

I expected the `events.PutEvents` operation to get called successfully and result in a 200 status code with a custom event bus name, like I've tested with **LocalStack 1.4**.

(docker-compose logs running LocalStack 1.4)
```
INFO --- [   asgi_gw_0] localstack.request.aws     : AWS events.PutEvents => 200
```

### How are you starting LocalStack?

With a docker-compose file

### Steps To Reproduce

#### How are you starting localstack (e.g., `bin/localstack` command, arguments, or `docker-compose.yml`)

Here's my `docker-compose.yml`:
```yaml
version: "3.4"

services:
  eventbridge:
    image: localstack/localstack
    container_name: hydra_eventbridge
    ports:
      - 4566:4566
    environment:
      - EAGER_SERVICE_LOADING=1
      - SERVICES=events
```

To start LocalStack, I run `docker-compose up`.

The [image versions `2.0.0`](https://hub.docker.com/layers/localstack/localstack/2.0.0/images/sha256-2d0861a7fd281bb4f8a8404d8249ab4aed278c5ac8bdc55f8c246399e4bffcb8?context=explore) and higher fail when passing a custom bus name. The highest working [image version is `1.4`](https://hub.docker.com/layers/localstack/localstack/1.4/images/sha256-4a966e42eff4bbeec44afe13c2d24a67954742b191caae7c15e56186cc0b9ed8?context=explore).

You can try toggling the versions in the `docker-compose.yml` to verify:
```
image: localstack/localstack:1.4
```

and

```
image: localstack/localstack:2.0.0
```


#### Client commands (e.g., AWS SDK code snippet, or sequence of "awslocal" commands)

Here's my Python boto3 snippet to publish an event with a custom bus name:
```python
import boto3

eb = boto3.client(service_name="events", endpoint_url="http://localhost:4566")

eb.put_events(
    Entries=[{
        "Source": "test-source",
        "Detail": "test-detail",
        "DetailType": "test-detail-type",
        "EventBusName": "customBusName"
    }]
)
```


### Environment

```markdown
- OS: macOS Ventura 13.2
- LocalStack: 2.0.0
- Python: 3.8.13
- boto3==1.26.127
- botocore==1.29.127
```


### Anything else?

**TL;DR:** The `1.4` localstack Docker image works for me, and anything from `2.0.0` and higher fails.

I noticed this issue with the latest `localstack` Docker image and worked backwards until I found that the breaking image version was `2.0.0`.

Also worth noting that all versions work when I use the default bus name of `default`.


Discussion:

Resolving patch:
diff --git a/localstack/services/events/provider.py b/localstack/services/events/provider.py
--- a/localstack/services/events/provider.py
+++ b/localstack/services/events/provider.py
@@ -511,11 +511,13 @@ def events_handler_put_events(self):
     for event_envelope in events:
         event = event_envelope["event"]
         event_bus_name = event.get("EventBusName") or DEFAULT_EVENT_BUS_NAME
-        event_rules = self.events_backend.event_buses[event_bus_name].rules
+        event_bus = self.events_backend.event_buses.get(event_bus_name)
+        if not event_bus:
+            continue
 
         matching_rules = [
             r
-            for r in event_rules.values()
+            for r in event_bus.rules.values()
             if r.event_bus_name == event_bus_name and not r.scheduled_expression
         ]
         if not matching_rules:


Test patch:
diff --git a/tests/integration/test_events.py b/tests/integration/test_events.py
--- a/tests/integration/test_events.py
+++ b/tests/integration/test_events.py
@@ -1737,3 +1737,84 @@ def check_invocation():
             )
 
         retry(check_invocation, sleep=5, retries=15)
+
+    @pytest.mark.aws_validated
+    def test_put_events_nonexistent_event_bus(
+        self,
+        aws_client,
+        sqs_create_queue,
+        sqs_queue_arn,
+        events_put_rule,
+        events_allow_event_rule_to_sqs_queue,
+        snapshot,
+    ):
+        default_bus_rule_name = f"rule-{short_uid()}"
+        default_bus_target_id = f"test-target-default-b-{short_uid()}"
+        nonexistent_event_bus = f"event-bus-{short_uid()}"
+
+        snapshot.add_transformer(
+            [
+                snapshot.transform.key_value("MD5OfBody"),  # the event contains a timestamp
+                snapshot.transform.key_value("ReceiptHandle"),
+                snapshot.transform.regex(nonexistent_event_bus, "<custom-event-bus>"),
+            ]
+        )
+        # create SQS queue + add rules & targets so that we can check the default event bus received the message
+        # even if one entry was wrong
+
+        queue_url = sqs_create_queue()
+        queue_arn = sqs_queue_arn(queue_url)
+
+        rule_on_default_bus = events_put_rule(
+            Name=default_bus_rule_name,
+            EventPattern=json.dumps({"detail-type": ["CustomType"], "source": ["MySource"]}),
+            State="ENABLED",
+        )
+
+        events_allow_event_rule_to_sqs_queue(
+            event_rule_arn=rule_on_default_bus["RuleArn"],
+            sqs_queue_arn=queue_arn,
+            sqs_queue_url=queue_url,
+        )
+
+        aws_client.events.put_targets(
+            Rule=default_bus_rule_name,
+            Targets=[{"Id": default_bus_target_id, "Arn": queue_arn}],
+        )
+
+        # create two entries, one with no EventBus specified (so it will target the default one)
+        # and one with a nonexistent EventBusName, which should be ignored
+        entries = [
+            {
+                "Source": "MySource",
+                "DetailType": "CustomType",
+                "Detail": json.dumps({"message": "for the default event bus"}),
+            },
+            {
+                "EventBusName": nonexistent_event_bus,
+                "Source": "MySource",
+                "DetailType": "CustomType",
+                "Detail": json.dumps({"message": "for the custom event bus"}),
+            },
+        ]
+        response = aws_client.events.put_events(Entries=entries)
+        snapshot.match("put-events", response)
+
+        def _get_sqs_messages():
+            resp = aws_client.sqs.receive_message(
+                QueueUrl=queue_url, VisibilityTimeout=0, WaitTimeSeconds=1
+            )
+            msgs = resp.get("Messages")
+            assert len(msgs) == 1
+            aws_client.sqs.delete_message(
+                QueueUrl=queue_url, ReceiptHandle=msgs[0]["ReceiptHandle"]
+            )
+            return msgs
+
+        messages = retry(_get_sqs_messages, retries=5, sleep=0.1)
+        snapshot.match("get-events", messages)
+
+        # try to get the custom EventBus we passed the Event to
+        with pytest.raises(ClientError) as e:
+            aws_client.events.describe_event_bus(Name=nonexistent_event_bus)
+        snapshot.match("non-existent-bus", e.value.response)
diff --git a/tests/integration/test_events.snapshot.json b/tests/integration/test_events.snapshot.json
--- a/tests/integration/test_events.snapshot.json
+++ b/tests/integration/test_events.snapshot.json
@@ -102,5 +102,55 @@
         }
       }
     }
+  },
+  "tests/integration/test_events.py::TestEvents::test_put_events_nonexistent_event_bus": {
+    "recorded-date": "05-05-2023, 12:00:22",
+    "recorded-content": {
+      "put-events": {
+        "Entries": [
+          {
+            "EventId": "<uuid:1>"
+          },
+          {
+            "EventId": "<uuid:2>"
+          }
+        ],
+        "FailedEntryCount": 0,
+        "ResponseMetadata": {
+          "HTTPHeaders": {},
+          "HTTPStatusCode": 200
+        }
+      },
+      "get-events": [
+        {
+          "MessageId": "<uuid:3>",
+          "ReceiptHandle": "<receipt-handle:1>",
+          "MD5OfBody": "<m-d5-of-body:1>",
+          "Body": {
+            "version": "0",
+            "id": "<uuid:1>",
+            "detail-type": "CustomType",
+            "source": "MySource",
+            "account": "111111111111",
+            "time": "date",
+            "region": "<region>",
+            "resources": [],
+            "detail": {
+              "message": "for the default event bus"
+            }
+          }
+        }
+      ],
+      "non-existent-bus": {
+        "Error": {
+          "Code": "ResourceNotFoundException",
+          "Message": "Event bus <custom-event-bus> does not exist."
+        },
+        "ResponseMetadata": {
+          "HTTPHeaders": {},
+          "HTTPStatusCode": 400
+        }
+      }
+    }
   }
 }