From e1ddea8a0257bd069c2c1e564327d8543e4eaed5 Mon Sep 17 00:00:00 2001 From: archinksagar <68829863+archinksagar@users.noreply.github.com> Date: Sat, 22 Feb 2025 05:48:38 -0500 Subject: [PATCH] Logs: Add new methods (#8608) --- IMPLEMENTATION_COVERAGE.md | 39 +-- docs/docs/services/logs.rst | 30 +-- moto/logs/exceptions.py | 12 + moto/logs/models.py | 409 +++++++++++++++++++++++++++++ moto/logs/responses.py | 126 +++++++++ tests/test_logs/test_logs.py | 491 +++++++++++++++++++++++++++++++++++ 6 files changed, 1075 insertions(+), 32 deletions(-) diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index 68d5289fc598..35182915684c 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -995,7 +995,7 @@ ## cloudformation
-39% implemented +37% implemented - [ ] activate_organizations_access - [ ] activate_type @@ -1006,6 +1006,7 @@ - [ ] create_generated_template - [X] create_stack - [X] create_stack_instances +- [ ] create_stack_refactor - [X] create_stack_set - [ ] deactivate_organizations_access - [ ] deactivate_type @@ -1025,6 +1026,7 @@ - [ ] describe_stack_drift_detection_status - [X] describe_stack_events - [X] describe_stack_instance +- [ ] describe_stack_refactor - [X] describe_stack_resource - [ ] describe_stack_resource_drifts - [X] describe_stack_resources @@ -1038,6 +1040,7 @@ - [ ] detect_stack_set_drift - [ ] estimate_template_cost - [X] execute_change_set +- [ ] execute_stack_refactor - [ ] get_generated_template - [X] get_stack_policy - [X] get_template @@ -1053,6 +1056,8 @@ - [ ] list_resource_scans - [ ] list_stack_instance_resource_drifts - [X] list_stack_instances +- [ ] list_stack_refactor_actions +- [ ] list_stack_refactors - [X] list_stack_resources - [ ] list_stack_set_auto_deployment_targets - [X] list_stack_set_operation_results @@ -5309,21 +5314,21 @@ ## logs
-40% implemented +57% implemented - [ ] associate_kms_key - [X] cancel_export_task -- [ ] create_delivery +- [X] create_delivery - [X] create_export_task - [ ] create_log_anomaly_detector - [X] create_log_group - [X] create_log_stream - [ ] delete_account_policy - [ ] delete_data_protection_policy -- [ ] delete_delivery -- [ ] delete_delivery_destination -- [ ] delete_delivery_destination_policy -- [ ] delete_delivery_source +- [X] delete_delivery +- [X] delete_delivery_destination +- [X] delete_delivery_destination_policy +- [X] delete_delivery_source - [X] delete_destination - [ ] delete_index_policy - [ ] delete_integration @@ -5338,9 +5343,9 @@ - [ ] delete_transformer - [ ] describe_account_policies - [ ] describe_configuration_templates -- [ ] describe_deliveries -- [ ] describe_delivery_destinations -- [ ] describe_delivery_sources +- [X] describe_deliveries +- [X] describe_delivery_destinations +- [X] describe_delivery_sources - [X] describe_destinations - [X] describe_export_tasks - [ ] describe_field_indexes @@ -5355,10 +5360,10 @@ - [ ] disassociate_kms_key - [X] filter_log_events - [ ] get_data_protection_policy -- [ ] get_delivery -- [ ] get_delivery_destination -- [ ] get_delivery_destination_policy -- [ ] get_delivery_source +- [X] get_delivery +- [X] get_delivery_destination +- [X] get_delivery_destination_policy +- [X] get_delivery_source - [ ] get_integration - [ ] get_log_anomaly_detector - [X] get_log_events @@ -5374,9 +5379,9 @@ - [X] list_tags_log_group - [ ] put_account_policy - [ ] put_data_protection_policy -- [ ] put_delivery_destination -- [ ] put_delivery_destination_policy -- [ ] put_delivery_source +- [X] put_delivery_destination +- [X] put_delivery_destination_policy +- [X] put_delivery_source - [X] put_destination - [X] put_destination_policy - [ ] put_index_policy diff --git a/docs/docs/services/logs.rst b/docs/docs/services/logs.rst index b26cbcdaae07..114ad3d64037 100644 --- a/docs/docs/services/logs.rst +++ b/docs/docs/services/logs.rst @@ -16,17 +16,17 @@ logs - [ ] associate_kms_key - [X] cancel_export_task -- [ ] create_delivery +- [X] create_delivery - [X] create_export_task - [ ] create_log_anomaly_detector - [X] create_log_group - [X] create_log_stream - [ ] delete_account_policy - [ ] delete_data_protection_policy -- [ ] delete_delivery -- [ ] delete_delivery_destination -- [ ] delete_delivery_destination_policy -- [ ] delete_delivery_source +- [X] delete_delivery +- [X] delete_delivery_destination +- [X] delete_delivery_destination_policy +- [X] delete_delivery_source - [X] delete_destination - [ ] delete_index_policy - [ ] delete_integration @@ -45,9 +45,9 @@ logs - [ ] delete_transformer - [ ] describe_account_policies - [ ] describe_configuration_templates -- [ ] describe_deliveries -- [ ] describe_delivery_destinations -- [ ] describe_delivery_sources +- [X] describe_deliveries +- [X] describe_delivery_destinations +- [X] describe_delivery_sources - [X] describe_destinations - [X] describe_export_tasks @@ -83,10 +83,10 @@ logs - [ ] get_data_protection_policy -- [ ] get_delivery -- [ ] get_delivery_destination -- [ ] get_delivery_destination_policy -- [ ] get_delivery_source +- [X] get_delivery +- [X] get_delivery_destination +- [X] get_delivery_destination_policy +- [X] get_delivery_source - [ ] get_integration - [ ] get_log_anomaly_detector - [X] get_log_events @@ -106,9 +106,9 @@ logs - [X] list_tags_log_group - [ ] put_account_policy - [ ] put_data_protection_policy -- [ ] put_delivery_destination -- [ ] put_delivery_destination_policy -- [ ] put_delivery_source +- [X] put_delivery_destination +- [X] put_delivery_destination_policy +- [X] put_delivery_source - [X] put_destination - [X] put_destination_policy - [ ] put_index_policy diff --git a/moto/logs/exceptions.py b/moto/logs/exceptions.py index 4aea395c504b..c34d74d0475a 100644 --- a/moto/logs/exceptions.py +++ b/moto/logs/exceptions.py @@ -44,3 +44,15 @@ class LimitExceededException(LogsClientError): def __init__(self) -> None: self.code = 400 super().__init__("LimitExceededException", "Resource limit exceeded.") + + +class ValidationException(LogsClientError): + def __init__(self, msg: str) -> None: + self.code = 400 + super().__init__("ValidationException", msg) + + +class ConflictException(LogsClientError): + def __init__(self, msg: str) -> None: + self.code = 400 + super().__init__("ConflictException", msg) diff --git a/moto/logs/models.py b/moto/logs/models.py index 018b2fc55e5d..3fc541c121bc 100644 --- a/moto/logs/models.py +++ b/moto/logs/models.py @@ -7,10 +7,12 @@ from moto.core.common_models import BaseModel, CloudFormationModel from moto.core.utils import unix_time_millis, utcnow from moto.logs.exceptions import ( + ConflictException, InvalidParameterException, LimitExceededException, ResourceAlreadyExistsException, ResourceNotFoundException, + ValidationException, ) from moto.logs.logs_query import execute_query from moto.logs.metric_filters import MetricFilters @@ -760,6 +762,162 @@ def to_json(self) -> Dict[str, Any]: } +class DeliveryDestination(BaseModel): + def __init__( + self, + account_id: str, + region: str, + name: str, + output_format: Optional[str], + delivery_destination_configuration: Dict[str, str], + tags: Optional[Dict[str, str]], + policy: Optional[str] = None, + ): + self.name = name + self.output_format = output_format + self.arn = f"arn:aws:logs:{region}:{account_id}:delivery-destination:{name}" + destination_type = delivery_destination_configuration[ + "destinationResourceArn" + ].split(":")[2] + if destination_type == "s3": + self.delivery_destination_type = "S3" + elif destination_type == "logs": + self.delivery_destination_type = "CWL" + elif destination_type == "firehose": + self.delivery_destination_type = "FH" + self.delivery_destination_configuration = delivery_destination_configuration + self.tags = tags + self.policy = policy + + def to_dict(self) -> Dict[str, Any]: + dct = { + "name": self.name, + "arn": self.arn, + "deliveryDestinationType": self.delivery_destination_type, + "outputFormat": self.output_format, + "deliveryDestinationConfiguration": self.delivery_destination_configuration, + "tags": self.tags, + } + dct_items = {k: v for k, v in dct.items() if v} + return dct_items + + +class DeliverySource(BaseModel): + def __init__( + self, + account_id: str, + region: str, + name: str, + resource_arn: str, + log_type: str, + tags: Optional[Dict[str, str]], + ): + res_arns = [] + res_arns.append(resource_arn) + self.name = name + self.arn = f"arn:aws:logs:{region}:{account_id}:delivery-source:{name}" + self.resource_arns = res_arns + self.service = resource_arn.split(":")[2] + self.log_type = log_type + self.tags = tags + + def to_dict(self) -> Dict[str, Any]: + dct = { + "name": self.name, + "arn": self.arn, + "resourceArns": self.resource_arns, + "service": self.service, + "logType": self.log_type, + "tags": self.tags, + } + dct_items = {k: v for k, v in dct.items() if v} + return dct_items + + +class Delivery(BaseModel): + def __init__( + self, + account_id: str, + region: str, + delivery_source_name: str, + delivery_destination_arn: str, + destination_type: str, + record_fields: Optional[List[str]], + field_delimiter: Optional[str], + s3_delivery_configuration: Optional[Dict[str, Any]], + tags: Optional[Dict[str, str]], + ): + self.id = mock_random.get_random_string(length=16) + self.arn = f"arn:aws:logs:{region}:{account_id}:delivery:{self.id}" + self.delivery_source_name = delivery_source_name + self.delivery_destination_arn = delivery_destination_arn + self.destination_type = destination_type + # Default record fields + default_record_fields = [ + "date", + "time", + "x-edge-location", + "sc-bytes", + "c-ip", + "cs-method", + "cs(Host)", + "cs-uri-stem", + "sc-status", + "cs(Referer)", + "cs(User-Agent)", + "cs-uri-query", + "cs(Cookie)", + "x-edge-result-type", + "x-edge-request-id", + "x-host-header", + "cs-protocol", + "cs-bytes", + "time-taken", + "x-forwarded-for", + "ssl-protocol", + "ssl-cipher", + "x-edge-response-result-type", + "cs-protocol-version", + "fle-status", + "fle-encrypted-fields", + "c-port", + "time-to-first-byte", + "x-edge-detailed-result-type", + "sc-content-type", + "sc-content-len", + "sc-range-start", + "sc-range-end", + ] + self.record_fields = record_fields or default_record_fields + self.field_delimiter = field_delimiter + default_s3_configuration = {} + if destination_type == "S3": + # Default s3 configuration + default_s3_configuration = { + "suffixPath": "AWSLogs/{account-id}/CloudFront/", + "enableHiveCompatiblePath": False, + } + self.s3_delivery_configuration = ( + s3_delivery_configuration or default_s3_configuration + ) + self.tags = tags + + def to_dict(self) -> Dict[str, Any]: + dct = { + "id": self.id, + "arn": self.arn, + "deliverySourceName": self.delivery_source_name, + "deliveryDestinationArn": self.delivery_destination_arn, + "deliveryDestinationType": self.destination_type, + "recordFields": self.record_fields, + "fieldDelimiter": self.field_delimiter, + "s3DeliveryConfiguration": self.s3_delivery_configuration, + "tags": self.tags, + } + dct_items = {k: v for k, v in dct.items() if v} + return dct_items + + class LogsBackend(BaseBackend): def __init__(self, region_name: str, account_id: str): super().__init__(region_name, account_id) @@ -770,6 +928,9 @@ def __init__(self, region_name: str, account_id: str): self.destinations: Dict[str, Destination] = dict() self.tagger = TaggingService() self.export_tasks: Dict[str, ExportTask] = dict() + self.delivery_destinations: Dict[str, DeliveryDestination] = dict() + self.delivery_sources: Dict[str, DeliverySource] = dict() + self.deliveries: Dict[str, Delivery] = dict() def create_log_group( self, log_group_name: str, tags: Dict[str, str], **kwargs: Any @@ -1346,5 +1507,253 @@ def _find_log_group(self, log_group_id: str, log_group_name: str) -> LogGroup: raise ResourceNotFoundException() return log_group + def put_delivery_destination( + self, + name: str, + output_format: Optional[str], + delivery_destination_configuration: Dict[str, str], + tags: Optional[Dict[str, str]], + ) -> DeliveryDestination: + if output_format and output_format not in [ + "w3c", + "raw", + "json", + "plain", + "parquet", + ]: + msg = f"1 validation error detected: Value '{output_format}' at 'outputFormat' failed to satisfy constraint: Member must satisfy enum value set: [w3c, raw, json, plain, parquet]" + raise ValidationException(msg) + if name in self.delivery_destinations: + if tags: + raise ConflictException( + msg="Tags can only be provided when a resource is being created, not updated." + ) + if ( + output_format + and self.delivery_destinations[name].output_format != output_format + ): + msg = "Update to existing Delivery Destination with new Output Format is not allowed. Please create a new Delivery Destination instead." + raise ValidationException(msg) + delivery_destination = DeliveryDestination( + account_id=self.account_id, + region=self.region_name, + name=name, + output_format=output_format, + delivery_destination_configuration=delivery_destination_configuration, + tags=tags, + policy=None, + ) + self.delivery_destinations[name] = delivery_destination + if tags: + self.tag_resource(delivery_destination.arn, tags) + return delivery_destination + + def get_delivery_destination(self, name: str) -> DeliveryDestination: + if name not in self.delivery_destinations: + raise ResourceNotFoundException( + msg="Requested Delivery Destination does not exist in this account." + ) + delivery_destination = self.delivery_destinations[name] + return delivery_destination + + def describe_delivery_destinations(self) -> List[DeliveryDestination]: + # Pagination not yet implemented + delivery_destinations = list(self.delivery_destinations.values()) + return delivery_destinations + + def put_delivery_destination_policy( + self, delivery_destination_name: str, delivery_destination_policy: str + ) -> Dict[str, str]: + if delivery_destination_name not in self.delivery_destinations: + raise ResourceNotFoundException( + msg="Requested Delivery Destination does not exist in this account." + ) + dd = self.delivery_destinations[delivery_destination_name] + dd.policy = delivery_destination_policy + return {"deliveryDestinationPolicy": delivery_destination_policy} + + def get_delivery_destination_policy( + self, delivery_destination_name: str + ) -> Dict[str, Any]: + if delivery_destination_name not in self.delivery_destinations: + raise ResourceNotFoundException( + msg="Requested Delivery Destination does not exist in this account." + ) + policy = self.delivery_destinations[delivery_destination_name].policy + return {"deliveryDestinationPolicy": policy} + + def put_delivery_source( + self, name: str, resource_arn: str, log_type: str, tags: Dict[str, str] + ) -> DeliverySource: + log_types = { + "cloudfront": "ACCESS_LOGS", + "bedrock": "APPLICATION_LOGS", + "codewhisperer": "EVENT_LOGS", + "mediapackage": ["EGRESS_ACCESS_LOGS", "INGRESS_ACCESS_LOGS"], + "mediatailor": [ + "AD_DECISION_SERVER_LOGS", + "MANIFEST_SERVICE_LOGS", + "TRANSCODE_LOGS", + ], + "sso": "ERROR_LOGS", + "qdeveloper": "EVENT_LOGS", + "ses": "APPLICATION_LOG", + "workmail": [ + "ACCESS_CONTROL_LOGS", + "AUTHENTICATION_LOGS", + "WORKMAIL_AVAILABILITY_PROVIDER_LOGS", + "WORKMAIL_MAILBOX_ACCESS_LOGS", + "WORKMAIL_PERSONAL_ACCESS_TOKEN_LOGS", + ], + } + resource_type = resource_arn.split(":")[2] + + if resource_type not in log_types: + raise ResourceNotFoundException(msg="Cannot access provided service.") + + if log_type not in log_types[resource_type]: + raise ValidationException( + msg=" This service is not allowed for this LogSource." + ) + + if name in self.delivery_sources: + if tags: + raise ConflictException( + msg="Tags can only be provided when a resource is being created, not updated." + ) + if resource_arn not in self.delivery_sources[name].resource_arns: + raise ConflictException( + msg="Update to existing Delivery Source with new ResourceId is not allowed. Please create a new Delivery Source instead." + ) + delivery_source = DeliverySource( + account_id=self.account_id, + region=self.region_name, + name=name, + resource_arn=resource_arn, + log_type=log_type, + tags=tags, + ) + self.delivery_sources[name] = delivery_source + if tags: + self.tag_resource(delivery_source.arn, tags) + return delivery_source + + def describe_delivery_sources(self) -> List[DeliverySource]: + # Pagination not yet implemented + delivery_sources = list(self.delivery_sources.values()) + return delivery_sources + + def get_delivery_source(self, name: str) -> DeliverySource: + if name not in self.delivery_sources: + raise ResourceNotFoundException( + msg="Requested Delivery Source does not exist in this account.." + ) + delivery_source = self.delivery_sources[name] + return delivery_source + + def create_delivery( + self, + delivery_source_name: str, + delivery_destination_arn: str, + record_fields: Optional[List[str]], + field_delimiter: Optional[str], + s3_delivery_configuration: Optional[Dict[str, Any]], + tags: Optional[Dict[str, str]], + ) -> Delivery: + if delivery_source_name not in self.delivery_sources: + raise ResourceNotFoundException( + msg="Requested Delivery Source does not exist in this account." + ) + if delivery_destination_arn not in [ + dd.arn for dd in self.delivery_destinations.values() + ]: + raise ResourceNotFoundException( + msg="Requested Delivery Destination does not exist in this account." + ) + + for delivery in self.deliveries.values(): + if ( + delivery.delivery_source_name == delivery_source_name + and delivery.delivery_destination_arn == delivery_destination_arn + ): + raise ConflictException(msg="The specified Delivery already exists") + if delivery.delivery_source_name == delivery_source_name: + for dd in self.delivery_destinations.values(): + if ( + dd.arn == delivery_destination_arn + and delivery.destination_type == dd.delivery_destination_type + ): + raise ConflictException( + msg="Delivery already exists for this Delivery Source with the same Delivery Destination Type." + ) + + for dd in list(self.delivery_destinations.values()): + if dd.arn == delivery_destination_arn: + destination_type = dd.delivery_destination_type + + delivery = Delivery( + account_id=self.account_id, + region=self.region_name, + delivery_source_name=delivery_source_name, + delivery_destination_arn=delivery_destination_arn, + destination_type=destination_type, + record_fields=record_fields, + field_delimiter=field_delimiter, + s3_delivery_configuration=s3_delivery_configuration, + tags=tags, + ) + self.deliveries[delivery.id] = delivery + if tags: + self.tag_resource(delivery.arn, tags) + return delivery + + def describe_deliveries(self) -> List[Delivery]: + # Pagination not yet implemented + deliveries = list(self.deliveries.values()) + return deliveries + + def get_delivery(self, id: str) -> Delivery: + if id not in self.deliveries: + raise ResourceNotFoundException( + msg="Requested Delivery does not exist in this account." + ) + delivery = self.deliveries[id] + return delivery + + def delete_delivery(self, id: str) -> None: + if id not in self.deliveries: + raise ResourceNotFoundException( + msg="Requested Delivery does not exist in this account." + ) + self.deliveries.pop(id) + return + + def delete_delivery_destination(self, name: str) -> None: + if name not in self.delivery_destinations: + raise ResourceNotFoundException( + msg="Requested Delivery Destination does not exist in this account." + ) + self.delivery_destinations.pop(name) + return + + def delete_delivery_destination_policy( + self, delivery_destination_name: str + ) -> None: + delivery_destination = self.delivery_destinations.get(delivery_destination_name) + if not delivery_destination: + raise ResourceNotFoundException( + msg="Requested Delivery Destination does not exist in this account." + ) + delivery_destination.policy = None + return + + def delete_delivery_source(self, name: str) -> None: + if name not in self.delivery_sources: + raise ResourceNotFoundException( + msg="Requested Delivery Source does not exist in this account." + ) + self.delivery_sources.pop(name) + return + logs_backends = BackendDict(LogsBackend, "logs") diff --git a/moto/logs/responses.py b/moto/logs/responses.py index b6522f4d76c9..e374ebd6252f 100644 --- a/moto/logs/responses.py +++ b/moto/logs/responses.py @@ -474,3 +474,129 @@ def untag_resource(self) -> str: tag_keys = self._get_param("tagKeys") self.logs_backend.untag_resource(resource_arn, tag_keys) return "{}" + + def put_delivery_destination(self) -> str: + name = self._get_param("name") + output_format = self._get_param("outputFormat") + delivery_destination_configuration = self._get_param( + "deliveryDestinationConfiguration" + ) + tags = self._get_param("tags") + delivery_destination = self.logs_backend.put_delivery_destination( + name=name, + output_format=output_format, + delivery_destination_configuration=delivery_destination_configuration, + tags=tags, + ) + return json.dumps(dict(deliveryDestination=delivery_destination.to_dict())) + + def get_delivery_destination(self) -> str: + name = self._get_param("name") + delivery_destination = self.logs_backend.get_delivery_destination( + name=name, + ) + return json.dumps(dict(deliveryDestination=delivery_destination.to_dict())) + + def describe_delivery_destinations(self) -> str: + delivery_destinations = self.logs_backend.describe_delivery_destinations() + return json.dumps( + dict(deliveryDestinations=[dd.to_dict() for dd in delivery_destinations]) + ) + + def put_delivery_destination_policy(self) -> str: + delivery_destination_name = self._get_param("deliveryDestinationName") + delivery_destination_policy = self._get_param("deliveryDestinationPolicy") + policy = self.logs_backend.put_delivery_destination_policy( + delivery_destination_name=delivery_destination_name, + delivery_destination_policy=delivery_destination_policy, + ) + return json.dumps(dict(policy=policy)) + + def get_delivery_destination_policy(self) -> str: + delivery_destination_name = self._get_param("deliveryDestinationName") + policy = self.logs_backend.get_delivery_destination_policy( + delivery_destination_name=delivery_destination_name, + ) + return json.dumps(dict(policy=policy)) + + def put_delivery_source(self) -> str: + name = self._get_param("name") + resource_arn = self._get_param("resourceArn") + log_type = self._get_param("logType") + tags = self._get_param("tags") + delivery_source = self.logs_backend.put_delivery_source( + name=name, + resource_arn=resource_arn, + log_type=log_type, + tags=tags, + ) + return json.dumps(dict(deliverySource=delivery_source.to_dict())) + + def describe_delivery_sources(self) -> str: + delivery_sources = self.logs_backend.describe_delivery_sources() + return json.dumps( + dict(deliverySources=[ds.to_dict() for ds in delivery_sources]) + ) + + def get_delivery_source(self) -> str: + name = self._get_param("name") + delivery_source = self.logs_backend.get_delivery_source( + name=name, + ) + return json.dumps(dict(deliverySource=delivery_source.to_dict())) + + def create_delivery(self) -> str: + delivery_source_name = self._get_param("deliverySourceName") + delivery_destination_arn = self._get_param("deliveryDestinationArn") + record_fields = self._get_param("recordFields") + field_delimiter = self._get_param("fieldDelimiter") + s3_delivery_configuration = self._get_param("s3DeliveryConfiguration") + tags = self._get_param("tags") + delivery = self.logs_backend.create_delivery( + delivery_source_name=delivery_source_name, + delivery_destination_arn=delivery_destination_arn, + record_fields=record_fields, + field_delimiter=field_delimiter, + s3_delivery_configuration=s3_delivery_configuration, + tags=tags, + ) + return json.dumps(dict(delivery=delivery.to_dict())) + + def describe_deliveries(self) -> str: + deliveries = self.logs_backend.describe_deliveries() + return json.dumps(dict(deliveries=[d.to_dict() for d in deliveries])) + + def get_delivery(self) -> str: + id = self._get_param("id") + delivery = self.logs_backend.get_delivery( + id=id, + ) + return json.dumps(dict(delivery=delivery.to_dict())) + + def delete_delivery(self) -> str: + id = self._get_param("id") + self.logs_backend.delete_delivery( + id=id, + ) + return "" + + def delete_delivery_destination(self) -> str: + name = self._get_param("name") + self.logs_backend.delete_delivery_destination( + name=name, + ) + return "" + + def delete_delivery_destination_policy(self) -> str: + delivery_destination_name = self._get_param("deliveryDestinationName") + self.logs_backend.delete_delivery_destination_policy( + delivery_destination_name=delivery_destination_name, + ) + return "" + + def delete_delivery_source(self) -> str: + name = self._get_param("name") + self.logs_backend.delete_delivery_source( + name=name, + ) + return "" diff --git a/tests/test_logs/test_logs.py b/tests/test_logs/test_logs.py index cec52981c563..d686fe6eafa6 100644 --- a/tests/test_logs/test_logs.py +++ b/tests/test_logs/test_logs.py @@ -48,6 +48,25 @@ } ) +delivery_destination_policy = json.dumps( + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AllowLogDeliveryActions", + "Effect": "Allow", + "Principal": {"AWS": "arn:aws:iam::123456789012:root"}, + "Action": "logs:CreateDelivery", + "Resource": [ + f"arn:aws:logs:{TEST_REGION}:123456789012:delivery-source:*", + f"arn:aws:logs:{TEST_REGION}:123456789012:delivery:*", + f"arn:aws:logs:{TEST_REGION}:123456789012:delivery-destination:*", + ], + } + ], + } +) + @pytest.fixture(name="log_group_name") def create_log_group(): @@ -1165,3 +1184,475 @@ def test_describe_log_streams_no_prefix(): err = ex.value.response["Error"] assert err["Code"] == "InvalidParameterException" assert err["Message"] == "Cannot order by LastEventTime with a logStreamNamePrefix." + + +@mock_aws +def test_put_delivery_destination(): + client = boto3.client("logs", "us-east-1") + resp = client.put_delivery_destination( + name="test-delivery-destination", + outputFormat="json", + deliveryDestinationConfiguration={ + "destinationResourceArn": "arn:aws:s3:::test-s3-bucket" + }, + tags={"key1": "value1"}, + ) + delivery_destination = resp["deliveryDestination"] + assert delivery_destination["name"] == "test-delivery-destination" + assert delivery_destination["outputFormat"] == "json" + assert delivery_destination["deliveryDestinationConfiguration"] == { + "destinationResourceArn": "arn:aws:s3:::test-s3-bucket" + } + assert delivery_destination["tags"] == {"key1": "value1"} + + # Invalid OutputFormat + with pytest.raises(ClientError) as ex: + client.put_delivery_destination( + name="test-dd", + outputFormat="foobar", + deliveryDestinationConfiguration={ + "destinationResourceArn": "arn:aws:s3:::test-s3-bucket" + }, + ) + err = ex.value.response["Error"] + assert err["Code"] == "ValidationException" + + # Cannot update OutoutFormat + with pytest.raises(ClientError) as ex: + client.put_delivery_destination( + name="test-delivery-destination", + outputFormat="plain", + deliveryDestinationConfiguration={ + "destinationResourceArn": "arn:aws:s3:::test-s3-bucket" + }, + ) + err = ex.value.response["Error"] + assert err["Code"] == "ValidationException" + + +@mock_aws +def test_put_delivery_destination_update(): + client = boto3.client("logs", "us-east-1") + client.put_delivery_destination( + name="test-delivery-destination", + deliveryDestinationConfiguration={ + "destinationResourceArn": "arn:aws:s3:::test-s3-bucket" + }, + ) + # Update destination resource + resp = client.put_delivery_destination( + name="test-delivery-destination", + deliveryDestinationConfiguration={ + "destinationResourceArn": "arn:aws:s3:::test-s3-bucket-2" + }, + ) + delivery_destination = resp["deliveryDestination"] + assert delivery_destination["deliveryDestinationConfiguration"] == { + "destinationResourceArn": "arn:aws:s3:::test-s3-bucket-2" + } + + +@mock_aws +def test_get_delivery_destination(): + client = boto3.client("logs", "us-east-1") + for i in range(1, 3): + client.put_delivery_destination( + name=f"test-delivery-destination-{i}", + deliveryDestinationConfiguration={ + "destinationResourceArn": "arn:aws:s3:::test-s3-bucket" + }, + ) + resp = client.get_delivery_destination(name="test-delivery-destination-1") + assert "deliveryDestination" in resp + assert resp["deliveryDestination"]["name"] == "test-delivery-destination-1" + + # Invalid name for delivery destination + with pytest.raises(ClientError) as ex: + client.get_delivery_destination( + name="foobar", + ) + err = ex.value.response["Error"] + assert err["Code"] == "ResourceNotFoundException" + + +@mock_aws +def test_describe_delivery_destinations(): + client = boto3.client("logs", "us-east-1") + for i in range(1, 3): + client.put_delivery_destination( + name=f"test-delivery-destination-{i}", + deliveryDestinationConfiguration={ + "destinationResourceArn": "arn:aws:s3:::test-s3-bucket" + }, + ) + resp = client.describe_delivery_destinations() + assert len(resp["deliveryDestinations"]) == 2 + + +@mock_aws +def test_put_delivery_destination_policy(): + client = boto3.client("logs", "us-east-1") + client.put_delivery_destination( + name="test-delivery-destination", + deliveryDestinationConfiguration={ + "destinationResourceArn": "arn:aws:s3:::test-s3-bucket" + }, + ) + resp = client.put_delivery_destination_policy( + deliveryDestinationName="test-delivery-destination", + deliveryDestinationPolicy=delivery_destination_policy, + ) + assert "policy" in resp + + # Invalid name for destination policy + with pytest.raises(ClientError) as ex: + client.put_delivery_destination_policy( + deliveryDestinationName="foobar", + deliveryDestinationPolicy=delivery_destination_policy, + ) + err = ex.value.response["Error"] + assert err["Code"] == "ResourceNotFoundException" + + +@mock_aws +def test_get_delivery_destination_policy(): + client = boto3.client("logs", "us-east-1") + client.put_delivery_destination( + name="test-delivery-destination", + deliveryDestinationConfiguration={ + "destinationResourceArn": "arn:aws:s3:::test-s3-bucket" + }, + ) + client.put_delivery_destination_policy( + deliveryDestinationName="test-delivery-destination", + deliveryDestinationPolicy=delivery_destination_policy, + ) + resp = client.get_delivery_destination_policy( + deliveryDestinationName="test-delivery-destination" + ) + assert "deliveryDestinationPolicy" in resp["policy"] + + # Invalide name for destination policy + with pytest.raises(ClientError) as ex: + client.get_delivery_destination_policy( + deliveryDestinationName="foobar", + ) + err = ex.value.response["Error"] + assert err["Code"] == "ResourceNotFoundException" + + +@mock_aws +def test_put_delivery_source(): + client = boto3.client("logs", "us-east-1") + resp = client.put_delivery_source( + name="test-delivery-source", + resourceArn="arn:aws:cloudfront::123456789012:distribution/E1Q5F5862X9VJ5", + logType="ACCESS_LOGS", + tags={"key1": "value1"}, + ) + assert "deliverySource" in resp + assert "name" in resp["deliverySource"] + assert "arn" in resp["deliverySource"] + assert "resourceArns" in resp["deliverySource"] + assert "service" in resp["deliverySource"] + assert "logType" in resp["deliverySource"] + assert "tags" in resp["deliverySource"] + + # Invalid resource source. + with pytest.raises(ClientError) as ex: + client.put_delivery_source( + name="test-ds", + resourceArn="arn:aws:s3:::test-s3-bucket", # S3 cannot be a source + logType="ACCESS_LOGS", + ) + err = ex.value.response["Error"] + assert err["Code"] == "ResourceNotFoundException" + + # Invalid Log type + with pytest.raises(ClientError) as ex: + client.put_delivery_source( + name="test-ds", + resourceArn="arn:aws:cloudfront::123456789012:distribution/E1Q5F5862X9VJ5", + logType="EVENT_LOGS", + ) + err = ex.value.response["Error"] + assert err["Code"] == "ValidationException" + + # Cannot update resource source with a differen resourceArn + with pytest.raises(ClientError) as ex: + client.put_delivery_source( + name="test-delivery-source", + resourceArn="arn:aws:cloudfront::123456789012:distribution/E19DL18TOXN9JU", + logType="ACCESS_LOGS", + ) + err = ex.value.response["Error"] + assert err["Code"] == "ConflictException" + + +@mock_aws +def test_describe_delivery_sources(): + client = boto3.client("logs", "us-east-1") + for i in range(1, 3): + client.put_delivery_source( + name=f"test-delivery-source-{i}", + resourceArn="arn:aws:cloudfront::123456789012:distribution/E19DL18TOXN9JU", + logType="ACCESS_LOGS", + ) + resp = client.describe_delivery_sources() + assert len(resp["deliverySources"]) == 2 + + +@mock_aws +def test_get_delivery_source(): + client = boto3.client("logs", "us-east-1") + for i in range(1, 3): + client.put_delivery_source( + name=f"test-delivery-source-{i}", + resourceArn="arn:aws:cloudfront::123456789012:distribution/E19DL18TOXN9JU", + logType="ACCESS_LOGS", + ) + resp = client.get_delivery_source(name="test-delivery-source-1") + assert "deliverySource" in resp + assert resp["deliverySource"]["name"] == "test-delivery-source-1" + + # Invalid name for delivery source + with pytest.raises(ClientError) as ex: + client.get_delivery_source( + name="foobar", + ) + err = ex.value.response["Error"] + assert err["Code"] == "ResourceNotFoundException" + + +@mock_aws +def test_create_delivery(): + client = boto3.client("logs", "us-east-1") + client.put_delivery_source( + name="test-delivery-source", + resourceArn="arn:aws:cloudfront::123456789012:distribution/E19DL18TOXN9JU", + logType="ACCESS_LOGS", + ) + client.put_delivery_destination( + name="test-delivery-destination", + deliveryDestinationConfiguration={ + "destinationResourceArn": "arn:aws:s3:::test-s3-bucket" + }, + ) + resp = client.create_delivery( + deliverySourceName="test-delivery-source", + deliveryDestinationArn="arn:aws:logs:us-east-1:123456789012:delivery-destination:test-delivery-destination", + recordFields=[ + "date", + ], + fieldDelimiter=",", + s3DeliveryConfiguration={ + "suffixPath": "AWSLogs/123456789012/CloudFront/", + "enableHiveCompatiblePath": True, + }, + tags={"key1": "value1"}, + ) + assert "delivery" in resp + assert "id" in resp["delivery"] + assert "arn" in resp["delivery"] + assert "deliverySourceName" in resp["delivery"] + assert "deliveryDestinationArn" in resp["delivery"] + assert "deliveryDestinationType" in resp["delivery"] + assert "recordFields" in resp["delivery"] + assert "fieldDelimiter" in resp["delivery"] + assert "s3DeliveryConfiguration" in resp["delivery"] + assert "tags" in resp["delivery"] + + # Invalid delivery source + with pytest.raises(ClientError) as ex: + client.create_delivery( + deliverySourceName="foobar", + deliveryDestinationArn="arn:aws:logs:us-east-1:123456789012:delivery-destination:test-delivery-destination", + ) + err = ex.value.response["Error"] + + # Invalid Delivery destination + with pytest.raises(ClientError) as ex: + client.create_delivery( + deliverySourceName="test-delivery-source", + deliveryDestinationArn="arn:aws:logs:us-east-1:123456789012:delivery-destination:foobar", + ) + err = ex.value.response["Error"] + assert err["Code"] == "ResourceNotFoundException" + + # Delivery already exists + with pytest.raises(ClientError) as ex: + client.create_delivery( + deliverySourceName="test-delivery-source", + deliveryDestinationArn="arn:aws:logs:us-east-1:123456789012:delivery-destination:test-delivery-destination", + ) + err = ex.value.response["Error"] + assert err["Code"] == "ConflictException" + + +@mock_aws +def test_describe_deliveries(): + client = boto3.client("logs", "us-east-1") + client.put_delivery_source( + name="test-delivery-source", + resourceArn="arn:aws:cloudfront::123456789012:distribution/E19DL18TOXN9JU", + logType="ACCESS_LOGS", + ) + client.put_delivery_destination( + name="test-delivery-destination-1", + deliveryDestinationConfiguration={ + "destinationResourceArn": "arn:aws:s3:::test-s3-bucket" + }, + ) + client.put_delivery_destination( + name="test-delivery-destination-2", + deliveryDestinationConfiguration={ + "destinationResourceArn": "arn:aws:firehose:us-east-1:123456789012:deliverystream/test-delivery-stream" + }, + ) + for i in range(1, 3): + client.create_delivery( + deliverySourceName="test-delivery-source", + deliveryDestinationArn=f"arn:aws:logs:us-east-1:123456789012:delivery-destination:test-delivery-destination-{i}", + ) + resp = client.describe_deliveries() + assert len(resp["deliveries"]) == 2 + + +@mock_aws +def test_get_delivery(): + client = boto3.client("logs", "us-east-1") + client.put_delivery_source( + name="test-delivery-source", + resourceArn="arn:aws:cloudfront::123456789012:distribution/E19DL18TOXN9JU", + logType="ACCESS_LOGS", + ) + client.put_delivery_destination( + name="test-delivery-destination", + deliveryDestinationConfiguration={ + "destinationResourceArn": "arn:aws:s3:::test-s3-bucket" + }, + ) + delivery = client.create_delivery( + deliverySourceName="test-delivery-source", + deliveryDestinationArn="arn:aws:logs:us-east-1:123456789012:delivery-destination:test-delivery-destination", + ) + delivery_id = delivery["delivery"]["id"] + resp = client.get_delivery(id=delivery_id) + assert "delivery" in resp + assert resp["delivery"]["id"] == delivery_id + + # Invalid delivery id + with pytest.raises(ClientError) as ex: + client.get_delivery(id="foobar") + err = ex.value.response["Error"] + assert err["Code"] == "ResourceNotFoundException" + + +@mock_aws +def test_delete_delivery(): + client = boto3.client("logs", "us-east-1") + client.put_delivery_source( + name="test-delivery-source", + resourceArn="arn:aws:cloudfront::123456789012:distribution/E19DL18TOXN9JU", + logType="ACCESS_LOGS", + ) + client.put_delivery_destination( + name="test-delivery-destination", + deliveryDestinationConfiguration={ + "destinationResourceArn": "arn:aws:s3:::test-s3-bucket" + }, + ) + delivery = client.create_delivery( + deliverySourceName="test-delivery-source", + deliveryDestinationArn="arn:aws:logs:us-east-1:123456789012:delivery-destination:test-delivery-destination", + ) + delivery_id = delivery["delivery"]["id"] + resp = client.describe_deliveries() + assert len(resp["deliveries"]) == 1 + client.delete_delivery(id=delivery_id) + resp = client.describe_deliveries() + assert len(resp["deliveries"]) == 0 + + # invalid delivery id + with pytest.raises(ClientError) as ex: + client.delete_delivery(id="foobar") + err = ex.value.response["Error"] + assert err["Code"] == "ResourceNotFoundException" + + +@mock_aws +def test_delete_delivery_destination(): + client = boto3.client("logs", "us-east-1") + resp = client.put_delivery_destination( + name="test-delivery-destination", + deliveryDestinationConfiguration={ + "destinationResourceArn": "arn:aws:s3:::test-s3-bucket" + }, + ) + delivery_destination = resp["deliveryDestination"] + resp = client.describe_delivery_destinations() + assert len(resp["deliveryDestinations"]) == 1 + resp = client.delete_delivery_destination(name=delivery_destination["name"]) + resp = client.describe_delivery_destinations() + assert len(resp["deliveryDestinations"]) == 0 + + # Invalid name for delivery destination + with pytest.raises(ClientError) as ex: + client.delete_delivery_destination(name="foobar") + err = ex.value.response["Error"] + assert err["Code"] == "ResourceNotFoundException" + + +@mock_aws +def test_delete_delivery_destination_policy(): + client = boto3.client("logs", "us-east-1") + client.put_delivery_destination( + name="test-delivery-destination", + deliveryDestinationConfiguration={ + "destinationResourceArn": "arn:aws:s3:::test-s3-bucket" + }, + ) + client.put_delivery_destination_policy( + deliveryDestinationName="test-delivery-destination", + deliveryDestinationPolicy=delivery_destination_policy, + ) + resp = client.get_delivery_destination_policy( + deliveryDestinationName="test-delivery-destination" + ) + policy = resp["policy"] + assert "deliveryDestinationPolicy" in policy + client.delete_delivery_destination_policy( + deliveryDestinationName="test-delivery-destination" + ) + resp = client.get_delivery_destination_policy( + deliveryDestinationName="test-delivery-destination" + ) + assert resp["policy"] == {} + + # Invalid name for delivery destination policy + with pytest.raises(ClientError) as ex: + client.delete_delivery_destination_policy(deliveryDestinationName="test") + err = ex.value.response["Error"] + assert err["Code"] == "ResourceNotFoundException" + + +@mock_aws +def test_delete_delivery_source(): + client = boto3.client("logs", "us-east-1") + resp = client.put_delivery_source( + name="test-delivery-source", + resourceArn="arn:aws:cloudfront::123456789012:distribution/E1Q5F5862X9VJ5", + logType="ACCESS_LOGS", + ) + delivery_source = resp["deliverySource"] + resp = client.describe_delivery_sources() + assert len(resp["deliverySources"]) == 1 + client.delete_delivery_source(name=delivery_source["name"]) + resp = client.describe_delivery_sources() + assert len(resp["deliverySources"]) == 0 + + # Invalid name for delivery source + with pytest.raises(ClientError) as ex: + client.delete_delivery_source(name="foobar") + err = ex.value.response["Error"] + assert err["Code"] == "ResourceNotFoundException"