diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index 24be57b19..04b52bce7 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -917,13 +917,13 @@ - [ ] unpeer_vpc - [ ] update_domain_entry -## xray - 0% implemented -- [ ] batch_get_traces -- [ ] get_service_graph -- [ ] get_trace_graph -- [ ] get_trace_summaries -- [ ] put_telemetry_records -- [ ] put_trace_segments +## xray - 100% implemented +- [X] batch_get_traces +- [X] get_service_graph +- [X] get_trace_graph +- [X] get_trace_summaries +- [X] put_telemetry_records +- [X] put_trace_segments ## ec2 - 41% implemented - [ ] accept_reserved_instances_exchange_quote @@ -1700,16 +1700,16 @@ - [ ] stop_db_instance ## acm - 100% implemented -- [x] add_tags_to_certificate -- [x] delete_certificate -- [x] describe_certificate -- [x] get_certificate -- [x] import_certificate -- [x] list_certificates -- [x] list_tags_for_certificate -- [x] remove_tags_from_certificate -- [x] request_certificate -- [x] resend_validation_email +- [X] add_tags_to_certificate +- [X] delete_certificate +- [X] describe_certificate +- [X] get_certificate +- [X] import_certificate +- [X] list_certificates +- [X] list_tags_for_certificate +- [X] remove_tags_from_certificate +- [X] request_certificate +- [X] resend_validation_email ## elasticache - 0% implemented - [ ] add_tags_to_resource @@ -3089,20 +3089,20 @@ - [ ] start_configuration_recorder - [ ] stop_configuration_recorder -## events - 73% implemented +## events - 100% implemented - [X] delete_rule -- [ ] describe_event_bus +- [X] describe_event_bus - [X] describe_rule - [X] disable_rule - [X] enable_rule - [X] list_rule_names_by_target - [X] list_rules - [X] list_targets_by_rule -- [ ] put_events -- [ ] put_permission +- [X] put_events +- [X] put_permission - [X] put_rule - [X] put_targets -- [ ] remove_permission +- [X] remove_permission - [X] remove_targets - [X] test_event_pattern diff --git a/README.md b/README.md index 7ced7b895..a58bf2199 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,8 @@ It gets even better! Moto isn't just for Python code and it isn't just for S3. L |------------------------------------------------------------------------------| | Cloudwatch | @mock_cloudwatch | basic endpoints done | |------------------------------------------------------------------------------| +| CloudwatchEvents | @mock_events | all endpoints done | +|------------------------------------------------------------------------------| | Data Pipeline | @mock_datapipeline| basic endpoints done | |------------------------------------------------------------------------------| | DynamoDB | @mock_dynamodb | core endpoints done | @@ -127,7 +129,7 @@ It gets even better! Moto isn't just for Python code and it isn't just for S3. L |------------------------------------------------------------------------------| | SWF | @mock_swf | basic endpoints done | |------------------------------------------------------------------------------| -| X-Ray | @mock_xray | core endpoints done | +| X-Ray | @mock_xray | all endpoints done | |------------------------------------------------------------------------------| ``` diff --git a/moto/core/exceptions.py b/moto/core/exceptions.py index 5474707d6..40202f7bd 100644 --- a/moto/core/exceptions.py +++ b/moto/core/exceptions.py @@ -34,6 +34,8 @@ ERROR_JSON_RESPONSE = u"""{ class RESTError(HTTPException): + code = 400 + templates = { 'single_error': SINGLE_ERROR_RESPONSE, 'error': ERROR_RESPONSE, @@ -54,7 +56,6 @@ class DryRunClientError(RESTError): class JsonRESTError(RESTError): - def __init__(self, error_type, message, template='error_json', **kwargs): super(JsonRESTError, self).__init__( error_type, message, template, **kwargs) diff --git a/moto/events/models.py b/moto/events/models.py index faec7b434..5c1d507ca 100644 --- a/moto/events/models.py +++ b/moto/events/models.py @@ -1,6 +1,7 @@ import os import re +from moto.core.exceptions import JsonRESTError from moto.core import BaseBackend, BaseModel @@ -50,6 +51,8 @@ class Rule(BaseModel): class EventsBackend(BaseBackend): + ACCOUNT_ID = re.compile(r'^(\d{1,12}|\*)$') + STATEMENT_ID = re.compile(r'^[a-zA-Z0-9-_]{1,64}$') def __init__(self): self.rules = {} @@ -58,6 +61,8 @@ class EventsBackend(BaseBackend): self.rules_order = [] self.next_tokens = {} + self.permissions = {} + def _get_rule_by_index(self, i): return self.rules.get(self.rules_order[i]) @@ -181,6 +186,17 @@ class EventsBackend(BaseBackend): return False + def put_events(self, events): + num_events = len(events) + + if num_events < 1: + raise JsonRESTError('ValidationError', 'Need at least 1 event') + elif num_events > 10: + raise JsonRESTError('ValidationError', 'Can only submit 10 events at once') + + # We dont really need to store the events yet + return [] + def remove_targets(self, name, ids): rule = self.rules.get(name) @@ -193,5 +209,40 @@ class EventsBackend(BaseBackend): def test_event_pattern(self): raise NotImplementedError() + def put_permission(self, action, principal, statement_id): + if action is None or action != 'PutEvents': + raise JsonRESTError('InvalidParameterValue', 'Action must be PutEvents') + + if principal is None or self.ACCOUNT_ID.match(principal) is None: + raise JsonRESTError('InvalidParameterValue', 'Principal must match ^(\d{1,12}|\*)$') + + if statement_id is None or self.STATEMENT_ID.match(statement_id) is None: + raise JsonRESTError('InvalidParameterValue', 'StatementId must match ^[a-zA-Z0-9-_]{1,64}$') + + self.permissions[statement_id] = {'action': action, 'principal': principal} + + def remove_permission(self, statement_id): + try: + del self.permissions[statement_id] + except KeyError: + raise JsonRESTError('ResourceNotFoundException', 'StatementId not found') + + def describe_event_bus(self): + arn = "arn:aws:events:us-east-1:000000000000:event-bus/default" + statements = [] + for statement_id, data in self.permissions.items(): + statements.append({ + 'Sid': statement_id, + 'Effect': 'Allow', + 'Principal': {'AWS': 'arn:aws:iam::{0}:root'.format(data['principal'])}, + 'Action': 'events:{0}'.format(data['action']), + 'Resource': arn + }) + return { + 'Policy': {'Version': '2012-10-17', 'Statement': statements}, + 'Name': 'default', + 'Arn': arn + } + events_backend = EventsBackend() diff --git a/moto/events/responses.py b/moto/events/responses.py index 8f433844a..f9cb9b5b5 100644 --- a/moto/events/responses.py +++ b/moto/events/responses.py @@ -18,9 +18,17 @@ class EventsHandler(BaseResponse): 'RoleArn': rule.role_arn } - def load_body(self): - decoded_body = self.body - return json.loads(decoded_body or '{}') + @property + def request_params(self): + if not hasattr(self, '_json_body'): + try: + self._json_body = json.loads(self.body) + except ValueError: + self._json_body = {} + return self._json_body + + def _get_param(self, param, if_none=None): + return self.request_params.get(param, if_none) def error(self, type_, message='', status=400): headers = self.response_headers @@ -28,8 +36,7 @@ class EventsHandler(BaseResponse): return json.dumps({'__type': type_, 'message': message}), headers, def delete_rule(self): - body = self.load_body() - name = body.get('Name') + name = self._get_param('Name') if not name: return self.error('ValidationException', 'Parameter Name is required.') @@ -38,8 +45,7 @@ class EventsHandler(BaseResponse): return '', self.response_headers def describe_rule(self): - body = self.load_body() - name = body.get('Name') + name = self._get_param('Name') if not name: return self.error('ValidationException', 'Parameter Name is required.') @@ -53,8 +59,7 @@ class EventsHandler(BaseResponse): return json.dumps(rule_dict), self.response_headers def disable_rule(self): - body = self.load_body() - name = body.get('Name') + name = self._get_param('Name') if not name: return self.error('ValidationException', 'Parameter Name is required.') @@ -65,8 +70,7 @@ class EventsHandler(BaseResponse): return '', self.response_headers def enable_rule(self): - body = self.load_body() - name = body.get('Name') + name = self._get_param('Name') if not name: return self.error('ValidationException', 'Parameter Name is required.') @@ -80,10 +84,9 @@ class EventsHandler(BaseResponse): pass def list_rule_names_by_target(self): - body = self.load_body() - target_arn = body.get('TargetArn') - next_token = body.get('NextToken') - limit = body.get('Limit') + target_arn = self._get_param('TargetArn') + next_token = self._get_param('NextToken') + limit = self._get_param('Limit') if not target_arn: return self.error('ValidationException', 'Parameter TargetArn is required.') @@ -94,10 +97,9 @@ class EventsHandler(BaseResponse): return json.dumps(rule_names), self.response_headers def list_rules(self): - body = self.load_body() - prefix = body.get('NamePrefix') - next_token = body.get('NextToken') - limit = body.get('Limit') + prefix = self._get_param('NamePrefix') + next_token = self._get_param('NextToken') + limit = self._get_param('Limit') rules = events_backend.list_rules(prefix, next_token, limit) rules_obj = {'Rules': []} @@ -111,10 +113,9 @@ class EventsHandler(BaseResponse): return json.dumps(rules_obj), self.response_headers def list_targets_by_rule(self): - body = self.load_body() - rule_name = body.get('Rule') - next_token = body.get('NextToken') - limit = body.get('Limit') + rule_name = self._get_param('Rule') + next_token = self._get_param('NextToken') + limit = self._get_param('Limit') if not rule_name: return self.error('ValidationException', 'Parameter Rule is required.') @@ -128,13 +129,25 @@ class EventsHandler(BaseResponse): return json.dumps(targets), self.response_headers def put_events(self): + events = self._get_param('Entries') + + failed_entries = events_backend.put_events(events) + + if failed_entries: + return json.dumps({ + 'FailedEntryCount': len(failed_entries), + 'Entries': failed_entries + }) + return '', self.response_headers def put_rule(self): - body = self.load_body() - name = body.get('Name') - event_pattern = body.get('EventPattern') - sched_exp = body.get('ScheduleExpression') + name = self._get_param('Name') + event_pattern = self._get_param('EventPattern') + sched_exp = self._get_param('ScheduleExpression') + state = self._get_param('State') + desc = self._get_param('Description') + role_arn = self._get_param('RoleArn') if not name: return self.error('ValidationException', 'Parameter Name is required.') @@ -156,17 +169,16 @@ class EventsHandler(BaseResponse): name, ScheduleExpression=sched_exp, EventPattern=event_pattern, - State=body.get('State'), - Description=body.get('Description'), - RoleArn=body.get('RoleArn') + State=state, + Description=desc, + RoleArn=role_arn ) return json.dumps({'RuleArn': rule_arn}), self.response_headers def put_targets(self): - body = self.load_body() - rule_name = body.get('Rule') - targets = body.get('Targets') + rule_name = self._get_param('Rule') + targets = self._get_param('Targets') if not rule_name: return self.error('ValidationException', 'Parameter Rule is required.') @@ -180,9 +192,8 @@ class EventsHandler(BaseResponse): return '', self.response_headers def remove_targets(self): - body = self.load_body() - rule_name = body.get('Rule') - ids = body.get('Ids') + rule_name = self._get_param('Rule') + ids = self._get_param('Ids') if not rule_name: return self.error('ValidationException', 'Parameter Rule is required.') @@ -197,3 +208,22 @@ class EventsHandler(BaseResponse): def test_event_pattern(self): pass + + def put_permission(self): + action = self._get_param('Action') + principal = self._get_param('Principal') + statement_id = self._get_param('StatementId') + + events_backend.put_permission(action, principal, statement_id) + + return '' + + def remove_permission(self): + statement_id = self._get_param('StatementId') + + events_backend.remove_permission(statement_id) + + return '' + + def describe_event_bus(self): + return json.dumps(events_backend.describe_event_bus()) diff --git a/tests/test_events/test_events.py b/tests/test_events/test_events.py index da8238f72..e839bde5b 100644 --- a/tests/test_events/test_events.py +++ b/tests/test_events/test_events.py @@ -3,6 +3,8 @@ import random import boto3 from moto.events import mock_events +from botocore.exceptions import ClientError +from nose.tools import assert_raises RULES = [ @@ -171,11 +173,36 @@ def test_remove_targets(): assert(targets_before - 1 == targets_after) -if __name__ == '__main__': - test_list_rules() - test_describe_rule() - test_enable_disable_rule() - test_list_rule_names_by_target() - test_list_rules() - test_list_targets_by_rule() - test_remove_targets() +@mock_events +def test_permissions(): + client = boto3.client('events', 'eu-central-1') + + client.put_permission(Action='PutEvents', Principal='111111111111', StatementId='Account1') + client.put_permission(Action='PutEvents', Principal='222222222222', StatementId='Account2') + + resp = client.describe_event_bus() + assert len(resp['Policy']['Statement']) == 2 + + client.remove_permission(StatementId='Account2') + + resp = client.describe_event_bus() + assert len(resp['Policy']['Statement']) == 1 + assert resp['Policy']['Statement'][0]['Sid'] == 'Account1' + + +@mock_events +def test_put_events(): + client = boto3.client('events', 'eu-central-1') + + event = { + "Source": "com.mycompany.myapp", + "Detail": '{"key1": "value3", "key2": "value4"}', + "Resources": ["resource1", "resource2"], + "DetailType": "myDetailType" + } + + client.put_events(Entries=[event]) + # Boto3 would error if it didn't return 200 OK + + with assert_raises(ClientError): + client.put_events(Entries=[event]*20)