diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/SQSEventAttributeBuilder.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/SQSEventAttributeBuilder.cs index 674b91426..c58063c48 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/SQSEventAttributeBuilder.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/SQSEventAttributeBuilder.cs @@ -20,6 +20,10 @@ public static SQSEventAttribute Build(AttributeData att) foreach (var pair in att.NamedArguments) { + if (pair.Key == nameof(data.ResourceName) && pair.Value.Value is string resourceName) + { + data.ResourceName = resourceName; + } if (pair.Key == nameof(data.BatchSize) && pair.Value.Value is uint batchSize) { data.BatchSize = batchSize; diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs index c85f19575..c6f426a8d 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs @@ -263,8 +263,7 @@ private string ProcessHttpApiAttribute(ILambdaFunctionSerializable lambdaFunctio /// private string ProcessSqsAttribute(ILambdaFunctionSerializable lambdaFunction, SQSEventAttribute att) { - var eventName = att.Queue.StartsWith("@") ? att.Queue.Substring(1) : GetResourceNameFromArn(att.Queue); - var eventPath = $"Resources.{lambdaFunction.ResourceName}.Properties.Events.{eventName}"; + var eventPath = $"Resources.{lambdaFunction.ResourceName}.Properties.Events.{att.ResourceName}"; // Set event type - https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-property-function-eventsource.html#sam-function-eventsource-type _templateWriter.SetToken($"{eventPath}.Type", "SQS"); @@ -330,7 +329,7 @@ private string ProcessSqsAttribute(ILambdaFunctionSerializable lambdaFunction, S _templateWriter.SetToken($"{eventPropertiesPath}.ScalingConfig.MaximumConcurrency", att.MaximumConcurrency); } - return eventName; + return att.ResourceName; } /// @@ -545,16 +544,5 @@ private string ExtractCurrentDescriptionSuffix(string templateDescription) return string.Empty; } - - private string GetResourceNameFromArn(string arn) - { - const string malformedErrorMessage = "ARN is in incorrect format. ARN format is: arn:::::"; - var tokens = arn.Split(new char[] { ':' }, 6); - if (tokens.Length != 6) - { - throw new ArgumentException(malformedErrorMessage); - } - return tokens[5]; - } } } \ No newline at end of file diff --git a/Libraries/src/Amazon.Lambda.Annotations/README.md b/Libraries/src/Amazon.Lambda.Annotations/README.md index 6cb5c57c1..2a8dbfe42 100644 --- a/Libraries/src/Amazon.Lambda.Annotations/README.md +++ b/Libraries/src/Amazon.Lambda.Annotations/README.md @@ -727,6 +727,7 @@ This example shows how users can use the `SQSEvent` attribute to set up event so The `SQSEvent` attribute contains the following properties: * **Queue** (Required) - The SQS queue that will act as the event trigger for the Lambda function. This can either be the queue ARN or reference to the SQS queue resource that is already defined in the serverless template. To reference a SQS queue resource in the serverless template, prefix the resource name with "@" symbol. +* **ResourceName** (Optional) - The CloudFormation resource name for the SQS event source mapping. By default this is set to the SQS queue name if `Queue` is set to an SQS queue ARN. If `Queue` is set to an existing CloudFormation resource, than that is used as the default value without the "@" prefix. * **Enabled** (Optional) - If set to false, the event source mapping will be disabled and message polling will be paused. Default value is true. * **BatchSize** (Optional) - The maximum number of messages that will be sent for processing in a single batch. This value must be between 1 to 10000. Default value is 10. * **MaximumBatchingWindowInSeconds** (Optional) - The maximum amount of time, in seconds, to gather records before invoking the function. This value must be between 0 to 300. Default value is 0. @@ -746,10 +747,10 @@ report `ReportBatchItemFailures` ```csharp [LambdaFunction(ResourceName = "SQSMessageHandler", Policies = "AWSLambdaSQSQueueExecutionRole", PackageType = LambdaPackageType.Image)] -[SQSEvent("@TestQueue", BatchSize = 50, MaximumConcurrency = 5, MaximumBatchingWindowInSeconds = 5, Filters = "{ \"body\" : { \"RequestCode\" : [ \"BBBB\" ] } }")] +[SQSEvent("@TestQueue", ResourceName = "TestQueueEvent", BatchSize = 50, MaximumConcurrency = 5, MaximumBatchingWindowInSeconds = 5, Filters = "{ \"body\" : { \"RequestCode\" : [ \"BBBB\" ] } }")] public SQSBatchResponse HandleMessage(SQSEvent evnt, ILambdaContext lambdaContext) { - lambdaContext.Logger.Log($"Received {evnt.Records.Count} messages"); + lambdaContext.Logger.Log($"Received {evnt.Records.Count} messages"); return new SQSBatchResponse(); } ``` @@ -763,7 +764,7 @@ The following SQS event source mapping will be generated for the `SQSMessageHand "Metadata": { "Tool": "Amazon.Lambda.Annotations", "SyncedEvents": [ - "TestQueue" + "TestQueueEvent" ] }, "Properties": { @@ -780,7 +781,7 @@ The following SQS event source mapping will be generated for the `SQSMessageHand ] }, "Events": { - "TestQueue": { + "TestQueueEvent": { "Type": "SQS", "Properties": { "Queue": { diff --git a/Libraries/src/Amazon.Lambda.Annotations/SQS/SQSEventAttribute.cs b/Libraries/src/Amazon.Lambda.Annotations/SQS/SQSEventAttribute.cs index 405dbc50a..a2509ed9e 100644 --- a/Libraries/src/Amazon.Lambda.Annotations/SQS/SQSEventAttribute.cs +++ b/Libraries/src/Amazon.Lambda.Annotations/SQS/SQSEventAttribute.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; namespace Amazon.Lambda.Annotations.SQS { @@ -13,6 +15,9 @@ public class SQSEventAttribute : Attribute // .NET attributes cannot be nullable. To work around this, we have added nullable backing fields to all optional properties and added an internal IsSet method to identify which properties were explicitly set the customer. // These internal methods are used by the CloudFormationWriter while deciding which properties to write in the CF template. + // Only allow alphanumeric characters + private static readonly Regex _resourceNameRegex = new Regex("^[a-zA-Z0-9]+$"); + /// /// The SQS queue that will act as the event trigger for the Lambda function. /// This can either be the queue ARN or reference to the SQS queue resource that is already defined in the serverless template. @@ -20,6 +25,35 @@ public class SQSEventAttribute : Attribute /// public string Queue { get; set; } + /// + /// The CloudFormation resource name for the SQS event source mapping. By default this is set to the SQS queue name if the is set to an SQS queue ARN. + /// If is set to an existing CloudFormation resource, than that is used as the default value without the "@" prefix. + /// + public string ResourceName + { + get + { + if (IsResourceNameSet) + { + return resourceName; + } + if (Queue.StartsWith("@")) + { + return Queue.Substring(1); + } + + var arnTokens = Queue.Split(new char[] { ':' }, 6); + var queueName = arnTokens[5]; + var sanitizedQueueName = string.Join(string.Empty, queueName.Where(char.IsLetterOrDigit)); + return sanitizedQueueName; + } + set => resourceName = value; + } + + private string resourceName { get; set; } = null; + internal bool IsResourceNameSet => resourceName != null; + + /// /// If set to false, the event source mapping will be disabled and message polling will be paused. /// Default value is true. @@ -100,6 +134,18 @@ internal List Validate() { validationErrors.Add($"{nameof(SQSEventAttribute.MaximumBatchingWindowInSeconds)} = {MaximumBatchingWindowInSeconds}. It must be between 0 and 300"); } + if (!Queue.StartsWith("@")) + { + var arnTokens = Queue.Split(new char[] { ':' }, 6); + if (arnTokens.Length != 6) + { + validationErrors.Add($"{nameof(SQSEventAttribute.Queue)} = {Queue}. The SQS queue ARN is invalid. The ARN format is 'arn::sqs:::'"); + } + } + if (IsResourceNameSet && !_resourceNameRegex.IsMatch(ResourceName)) + { + validationErrors.Add($"{nameof(SQSEventAttribute.ResourceName)} = {ResourceName}. It must only contain alphanumeric characters and must not be an empty string"); + } return validationErrors; } diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/sqsEvents.template b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/sqsEvents.template index b58965a13..417e7c8e5 100644 --- a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/sqsEvents.template +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/sqsEvents.template @@ -9,7 +9,9 @@ "Tool": "Amazon.Lambda.Annotations", "SyncedEvents": [ "queue1", - "queue2" + "queue2", + "myqueue", + "testQueueEvent" ] }, "Properties": { @@ -50,6 +52,23 @@ "Enabled": false, "MaximumBatchingWindowInSeconds": 5 } + }, + "myqueue": { + "Type": "SQS", + "Properties": { + "Queue": "arn:aws:sqs:us-east-2:444455556666:my-queue" + } + }, + "testQueueEvent": { + "Type": "SQS", + "Properties": { + "Queue": { + "Fn::GetAtt": [ + "testQueue", + "Arn" + ] + } + } } } } diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SourceGeneratorTests.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SourceGeneratorTests.cs index 09d3c0782..412721374 100644 --- a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SourceGeneratorTests.cs +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SourceGeneratorTests.cs @@ -1249,7 +1249,19 @@ public async Task VerifyInvalidSQSEvents_ThrowsCompilationErrors() DiagnosticResult .CompilerError("AWSLambda0102") .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}SQSEventExamples{Path.DirectorySeparatorChar}InvalidSQSEvents.cs", 37, 9, 43, 10) - .WithMessage("Multiple event attributes on LambdaFunction are not supported") + .WithMessage("Multiple event attributes on LambdaFunction are not supported"), + + DiagnosticResult.CompilerError("AWSLambda0116") + .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}SQSEventExamples{Path.DirectorySeparatorChar}InvalidSQSEvents.cs", 45, 9, 50, 10) + .WithArguments("Queue = test-queue. The SQS queue ARN is invalid. The ARN format is 'arn::sqs:::'"), + + DiagnosticResult.CompilerError("AWSLambda0116") + .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}SQSEventExamples{Path.DirectorySeparatorChar}InvalidSQSEvents.cs", 52, 9, 57, 10) + .WithArguments("ResourceName = sqs-event-source. It must only contain alphanumeric characters and must not be an empty string"), + + DiagnosticResult.CompilerError("AWSLambda0116") + .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}SQSEventExamples{Path.DirectorySeparatorChar}InvalidSQSEvents.cs", 59, 9, 64, 10) + .WithArguments("ResourceName = . It must only contain alphanumeric characters and must not be an empty string"), } } }.RunAsync(); diff --git a/Libraries/test/TestServerlessApp/SQSEventExamples/InvalidSQSEvents.cs.error b/Libraries/test/TestServerlessApp/SQSEventExamples/InvalidSQSEvents.cs.error index a0b03ec5c..bd4964de3 100644 --- a/Libraries/test/TestServerlessApp/SQSEventExamples/InvalidSQSEvents.cs.error +++ b/Libraries/test/TestServerlessApp/SQSEventExamples/InvalidSQSEvents.cs.error @@ -13,21 +13,21 @@ namespace TestServerlessApp.SQSEventExamples public class InvalidSQSEvents { [LambdaFunction] - [SQSEvent("test-queue", BatchSize = 0, MaximumBatchingWindowInSeconds = 302, MaximumConcurrency = 1)] + [SQSEvent("@testQueue", BatchSize = 0, MaximumBatchingWindowInSeconds = 302, MaximumConcurrency = 1)] public void ProcessMessageWithInvalidSQSEventAttributes(SQSEvent evnt) { Console.WriteLine($"Event processed: {evnt}"); } [LambdaFunction] - [SQSEvent("test-queue")] + [SQSEvent("@testQueue")] public void ProcessMessageWithInvalidParameters(SQSEvent evnt, bool invalidParameter1, int invalidParameter2) { Console.WriteLine($"Event processed: {evnt}"); } [LambdaFunction] - [SQSEvent("test-queue")] + [SQSEvent("@testQueue")] public bool ProcessMessageWithInvalidReturnType(SQSEvent evnt) { Console.WriteLine($"Event processed: {evnt}"); @@ -36,10 +36,31 @@ namespace TestServerlessApp.SQSEventExamples [LambdaFunction] [RestApi(LambdaHttpMethod.Get, "/")] - [SQSEvent("test-queue")] + [SQSEvent("@testQueue")] public void ProcessMessageWithMultipleEventTypes(SQSEvent evnt) { Console.WriteLine($"Event processed: {evnt}"); } + + [LambdaFunction] + [SQSEvent("test-queue")] + public void ProcessMessageWithInvalidQueueArn(SQSEvent evnt) + { + Console.WriteLine($"Event processed: {evnt}"); + } + + [LambdaFunction] + [SQSEvent("@testQueue", ResourceName = "sqs-event-source")] + public void ProcessMessageWithInvalidResourceName(SQSEvent evnt) + { + Console.WriteLine($"Event processed: {evnt}"); + } + + [LambdaFunction] + [SQSEvent("@testQueue", ResourceName = "")] + public void ProcessMessageWithEmptyResourceName(SQSEvent evnt) + { + Console.WriteLine($"Event processed: {evnt}"); + } } } diff --git a/Libraries/test/TestServerlessApp/SQSEventExamples/ValidSQSEvents.cs.txt b/Libraries/test/TestServerlessApp/SQSEventExamples/ValidSQSEvents.cs.txt index 405a5a7dc..a304608a6 100644 --- a/Libraries/test/TestServerlessApp/SQSEventExamples/ValidSQSEvents.cs.txt +++ b/Libraries/test/TestServerlessApp/SQSEventExamples/ValidSQSEvents.cs.txt @@ -15,6 +15,8 @@ namespace TestServerlessApp.SQSEventExamples [LambdaFunction] [SQSEvent("arn:aws:sqs:us-east-2:444455556666:queue1", BatchSize = 50, MaximumConcurrency = 30, Filters = "My-Filter-1; My-Filter-2")] [SQSEvent("arn:aws:sqs:us-east-2:444455556666:queue2", MaximumBatchingWindowInSeconds = 5, Enabled = false)] + [SQSEvent("arn:aws:sqs:us-east-2:444455556666:my-queue")] + [SQSEvent("@testQueue", ResourceName = "testQueueEvent")] public void ProcessMessages(SQSEvent evnt) { Console.WriteLine($"Event processed: {evnt}"); diff --git a/Libraries/test/TestServerlessApp/SqsMessageProcessing.cs b/Libraries/test/TestServerlessApp/SqsMessageProcessing.cs index 96c79c493..7562be678 100644 --- a/Libraries/test/TestServerlessApp/SqsMessageProcessing.cs +++ b/Libraries/test/TestServerlessApp/SqsMessageProcessing.cs @@ -8,7 +8,7 @@ namespace TestServerlessApp public class SqsMessageProcessing { [LambdaFunction(ResourceName = "SQSMessageHandler", Policies = "AWSLambdaSQSQueueExecutionRole", PackageType = LambdaPackageType.Image)] - [SQSEvent("@TestQueue", BatchSize = 50, MaximumConcurrency = 5, MaximumBatchingWindowInSeconds = 5, Filters = "{ \"body\" : { \"RequestCode\" : [ \"BBBB\" ] } }")] + [SQSEvent("@TestQueue", ResourceName = "TestQueueEvent", BatchSize = 50, MaximumConcurrency = 5, MaximumBatchingWindowInSeconds = 5, Filters = "{ \"body\" : { \"RequestCode\" : [ \"BBBB\" ] } }")] public SQSBatchResponse HandleMessage(SQSEvent evnt, ILambdaContext lambdaContext) { lambdaContext.Logger.Log($"Received {evnt.Records.Count} messages"); diff --git a/Libraries/test/TestServerlessApp/serverless.template b/Libraries/test/TestServerlessApp/serverless.template index 4140bcbb8..d3821daac 100644 --- a/Libraries/test/TestServerlessApp/serverless.template +++ b/Libraries/test/TestServerlessApp/serverless.template @@ -776,7 +776,7 @@ "Metadata": { "Tool": "Amazon.Lambda.Annotations", "SyncedEvents": [ - "TestQueue" + "TestQueueEvent" ] }, "Properties": { @@ -793,7 +793,7 @@ ] }, "Events": { - "TestQueue": { + "TestQueueEvent": { "Type": "SQS", "Properties": { "Queue": {