diff --git a/localstack-core/localstack/services/lambda_/resource_providers/aws_lambda_eventsourcemapping.py b/localstack-core/localstack/services/lambda_/resource_providers/aws_lambda_eventsourcemapping.py index c8340ca46e0d4..1f82478526dd8 100644 --- a/localstack-core/localstack/services/lambda_/resource_providers/aws_lambda_eventsourcemapping.py +++ b/localstack-core/localstack/services/lambda_/resource_providers/aws_lambda_eventsourcemapping.py @@ -1,6 +1,7 @@ # LocalStack Resource Provider Scaffolding v2 from __future__ import annotations +import copy from pathlib import Path from typing import Optional, TypedDict @@ -126,8 +127,16 @@ def create( model = request.desired_state lambda_client = request.aws_client_factory.lambda_ - response = lambda_client.create_event_source_mapping(**model) + params = copy.deepcopy(model) + if tags := params.get("Tags"): + transformed_tags = {} + for tag_definition in tags: + transformed_tags[tag_definition["Key"]] = tag_definition["Value"] + params["Tags"] = transformed_tags + + response = lambda_client.create_event_source_mapping(**params) model["Id"] = response["UUID"] + model["EventSourceMappingArn"] = response["EventSourceMappingArn"] return ProgressEvent( status=OperationStatus.SUCCESS, diff --git a/tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.py b/tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.py new file mode 100644 index 0000000000000..2376a9fde5671 --- /dev/null +++ b/tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.py @@ -0,0 +1,54 @@ +import json +import os + +import pytest + +from localstack.testing.pytest import markers +from localstack.testing.scenario.provisioning import cleanup_s3_bucket +from localstack.utils.strings import short_uid +from localstack.utils.sync import retry +from tests.aws.services.lambda_.event_source_mapping.utils import is_old_esm + + +@markers.aws.validated +@pytest.mark.skipif(condition=is_old_esm(), reason="Not implemented in v1 provider") +@markers.snapshot.skip_snapshot_verify( + paths=[ + "$..Tags.'aws:cloudformation:logical-id'", + "$..Tags.'aws:cloudformation:stack-id'", + "$..Tags.'aws:cloudformation:stack-name'", + ] +) +def test_adding_tags(deploy_cfn_template, aws_client, snapshot, cleanups): + template_path = os.path.join( + os.path.join(os.path.dirname(__file__), "../../../templates/event_source_mapping_tags.yml") + ) + assert os.path.isfile(template_path) + + output_key = f"key-{short_uid()}" + stack = deploy_cfn_template( + template_path=template_path, + parameters={"OutputKey": output_key}, + ) + # ensure the S3 bucket is empty so we can delete it + cleanups.append(lambda: cleanup_s3_bucket(aws_client.s3, stack.outputs["OutputBucketName"])) + + snapshot.add_transformer(snapshot.transform.regex(stack.stack_id, "")) + snapshot.add_transformer(snapshot.transform.regex(stack.stack_name, "")) + + event_source_mapping_arn = stack.outputs["EventSourceMappingArn"] + tags_response = aws_client.lambda_.list_tags(Resource=event_source_mapping_arn) + snapshot.match("event-source-mapping-tags", tags_response) + + # check the mapping works + queue_url = stack.outputs["QueueUrl"] + aws_client.sqs.send_message( + QueueUrl=queue_url, + MessageBody=json.dumps({"body": "something"}), + ) + + retry( + lambda: aws_client.s3.head_object(Bucket=stack.outputs["OutputBucketName"], Key=output_key), + retries=10, + sleep=5.0, + ) diff --git a/tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.snapshot.json b/tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.snapshot.json new file mode 100644 index 0000000000000..1cc37cf30ca33 --- /dev/null +++ b/tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.snapshot.json @@ -0,0 +1,19 @@ +{ + "tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.py::test_adding_tags": { + "recorded-date": "06-11-2024, 11:55:29", + "recorded-content": { + "event-source-mapping-tags": { + "Tags": { + "aws:cloudformation:logical-id": "EventSourceMapping", + "aws:cloudformation:stack-id": "", + "aws:cloudformation:stack-name": "", + "my": "tag" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + } +} diff --git a/tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.validation.json b/tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.validation.json new file mode 100644 index 0000000000000..7bbb9723d78fe --- /dev/null +++ b/tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.validation.json @@ -0,0 +1,5 @@ +{ + "tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.py::test_adding_tags": { + "last_validated_date": "2024-11-06T11:55:29+00:00" + } +} diff --git a/tests/aws/templates/event_source_mapping_tags.yml b/tests/aws/templates/event_source_mapping_tags.yml new file mode 100644 index 0000000000000..22af10ddb9f60 --- /dev/null +++ b/tests/aws/templates/event_source_mapping_tags.yml @@ -0,0 +1,120 @@ +Parameters: + OutputKey: + Type: String + +Resources: + Queue: + Type: AWS::SQS::Queue + UpdateReplacePolicy: Delete + DeletionPolicy: Delete + + FunctionRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + Service: lambda.amazonaws.com + Version: '2012-10-17' + ManagedPolicyArns: + - Fn::Join: + - '' + - - 'arn:' + - Ref: AWS::Partition + - :iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + Tags: + - Key: my + Value: tag + + FunctionRolePolicy: + Type: AWS::IAM::Policy + Properties: + PolicyDocument: + Statement: + - Action: + - sqs:ChangeMessageVisibility + - sqs:DeleteMessage + - sqs:GetQueueAttributes + - sqs:GetQueueUrl + - sqs:ReceiveMessage + Effect: Allow + Resource: + Fn::GetAtt: + - Queue + - Arn + - Action: + - s3:PutObject + Effect: Allow + Resource: + Fn::Sub: + - "${bucketArn}/${key}" + - bucketArn: !GetAtt OutputBucket.Arn + key: !Ref OutputKey + Version: '2012-10-17' + PolicyName: FunctionRolePolicy + Roles: + - Ref: FunctionRole + + OutputBucket: + Type: AWS::S3::Bucket + + Function: + Type: AWS::Lambda::Function + Properties: + Code: + ZipFile: | + import os + import boto3 + + BUCKET = os.environ["BUCKET"] + KEY = os.environ["KEY"] + + def handler(event, context): + client = boto3.client("s3") + client.put_object(Bucket=BUCKET, Key=KEY, Body=b"ok") + return "ok" + Handler: index.handler + Environment: + Variables: + BUCKET: !Ref OutputBucket + KEY: !Ref OutputKey + + Role: + Fn::GetAtt: + - FunctionRole + - Arn + Runtime: python3.11 + Tags: + - Key: my + Value: tag + DependsOn: + - FunctionRolePolicy + - FunctionRole + + EventSourceMapping: + Type: AWS::Lambda::EventSourceMapping + Properties: + EventSourceArn: + Fn::GetAtt: + - Queue + - Arn + FunctionName: + Ref: Function + Tags: + - Key: my + Value: tag + +Outputs: + QueueUrl: + Value: !Ref Queue + + EventSourceMappingArn: + Value: !GetAtt EventSourceMapping.EventSourceMappingArn + + FunctionName: + Value: !Ref Function + + OutputBucketName: + Value: !Ref OutputBucket \ No newline at end of file