Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .autover/changes/add-s3event-annotation.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"Projects": [
{
"Name": "Amazon.Lambda.Annotations",
"Type": "Minor",
"ChangelogMessages": [
"Added [S3Event] annotation attribute for declaratively configuring S3 event-triggered Lambda functions with support for bucket reference, event types, key prefix/suffix filters, and enabled state."
]
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -274,5 +274,12 @@ public static class DiagnosticDescriptors
category: "AWSLambdaCSharpGenerator",
DiagnosticSeverity.Error,
isEnabledByDefault: true);

public static readonly DiagnosticDescriptor InvalidS3EventAttribute = new DiagnosticDescriptor(id: "AWSLambda0136",
title: "Invalid S3EventAttribute",
messageFormat: "Invalid S3EventAttribute encountered: {0}",
category: "AWSLambdaCSharpGenerator",
DiagnosticSeverity.Error,
isEnabledByDefault: true);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using Amazon.Lambda.Annotations.ALB;
using Amazon.Lambda.Annotations.APIGateway;
using Amazon.Lambda.Annotations.S3;
using Amazon.Lambda.Annotations.SQS;
using Microsoft.CodeAnalysis;

Expand Down Expand Up @@ -91,6 +92,15 @@ public static AttributeModel Build(AttributeData att, GeneratorExecutionContext
Type = TypeModelBuilder.Build(att.AttributeClass, context)
};
}
else if (att.AttributeClass.Equals(context.Compilation.GetTypeByMetadataName(TypeFullNames.S3EventAttribute), SymbolEqualityComparer.Default))
{
var data = S3EventAttributeBuilder.Build(att);
model = new AttributeModel<S3EventAttribute>
{
Data = data,
Type = TypeModelBuilder.Build(att.AttributeClass, context)
};
}
else if (att.AttributeClass.Equals(context.Compilation.GetTypeByMetadataName(TypeFullNames.HttpApiAuthorizerAttribute), SymbolEqualityComparer.Default))
{
var data = HttpApiAuthorizerAttributeBuilder.Build(att);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using Amazon.Lambda.Annotations.S3;
using Microsoft.CodeAnalysis;
using System;

namespace Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes
{
public class S3EventAttributeBuilder
{
public static S3EventAttribute Build(AttributeData att)
{
if (att.ConstructorArguments.Length != 1)
throw new NotSupportedException($"{TypeFullNames.S3EventAttribute} must have constructor with 1 argument.");

var bucket = att.ConstructorArguments[0].Value as string;
var data = new S3EventAttribute(bucket);

foreach (var pair in att.NamedArguments)
{
if (pair.Key == nameof(data.ResourceName) && pair.Value.Value is string resourceName)
data.ResourceName = resourceName;
else if (pair.Key == nameof(data.Events) && pair.Value.Value is string events)
data.Events = events;
else if (pair.Key == nameof(data.FilterPrefix) && pair.Value.Value is string filterPrefix)
data.FilterPrefix = filterPrefix;
else if (pair.Key == nameof(data.FilterSuffix) && pair.Value.Value is string filterSuffix)
data.FilterSuffix = filterSuffix;
else if (pair.Key == nameof(data.Enabled) && pair.Value.Value is bool enabled)
data.Enabled = enabled;
}

return data;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ public static HashSet<EventType> Build(IMethodSymbol lambdaMethodSymbol,
{
events.Add(EventType.SQS);
}
else if (attribute.AttributeClass.ToDisplayString() == TypeFullNames.S3EventAttribute)
{
events.Add(EventType.S3);
}
else if (attribute.AttributeClass.ToDisplayString() == TypeFullNames.HttpApiAuthorizerAttribute
|| attribute.AttributeClass.ToDisplayString() == TypeFullNames.RestApiAuthorizerAttribute)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ internal class SyntaxReceiver : ISyntaxContextReceiver
{ "HttpApiAttribute", "HttpApi" },
{ "RestApiAttribute", "RestApi" },
{ "SQSEventAttribute", "SQSEvent" },
{ "ALBApiAttribute", "ALBApi" }
{ "ALBApiAttribute", "ALBApi" },
{ "S3EventAttribute", "S3Event" }
};

public List<MethodDeclarationSyntax> LambdaMethods { get; } = new List<MethodDeclarationSyntax>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ public static class TypeFullNames
public const string ALBFromHeaderAttribute = "Amazon.Lambda.Annotations.ALB.FromHeaderAttribute";
public const string ALBFromBodyAttribute = "Amazon.Lambda.Annotations.ALB.FromBodyAttribute";

public const string S3Event = "Amazon.Lambda.S3Events.S3Event";
public const string S3EventAttribute = "Amazon.Lambda.Annotations.S3.S3EventAttribute";

public const string LambdaSerializerAttribute = "Amazon.Lambda.Core.LambdaSerializerAttribute";
public const string DefaultLambdaSerializer = "Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer";

Expand Down Expand Up @@ -80,7 +83,8 @@ public static class TypeFullNames
RestApiAttribute,
HttpApiAttribute,
SQSEventAttribute,
ALBApiAttribute
ALBApiAttribute,
S3EventAttribute
};
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Amazon.Lambda.Annotations.ALB;
using Amazon.Lambda.Annotations.S3;
using Amazon.Lambda.Annotations.SourceGenerator.Diagnostics;
using Amazon.Lambda.Annotations.SourceGenerator.Extensions;
using Amazon.Lambda.Annotations.SourceGenerator.Models;
Expand Down Expand Up @@ -61,6 +62,7 @@ internal static bool ValidateFunction(GeneratorExecutionContext context, IMethod
ValidateApiGatewayEvents(lambdaFunctionModel, methodLocation, diagnostics);
ValidateSqsEvents(lambdaFunctionModel, methodLocation, diagnostics);
ValidateAlbEvents(lambdaFunctionModel, methodLocation, diagnostics);
ValidateS3Events(lambdaFunctionModel, methodLocation, diagnostics);

return ReportDiagnostics(diagnosticReporter, diagnostics);
}
Expand Down Expand Up @@ -98,6 +100,16 @@ internal static bool ValidateDependencies(GeneratorExecutionContext context, IMe
}
}

// Check for references to "Amazon.Lambda.S3Events" if the Lambda method is annotated with S3Event attribute.
if (lambdaMethodSymbol.HasAttribute(context, TypeFullNames.S3EventAttribute))
{
if (context.Compilation.ReferencedAssemblyNames.FirstOrDefault(x => x.Name == "Amazon.Lambda.S3Events") == null)
{
diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.MissingDependencies, methodLocation, "Amazon.Lambda.S3Events"));
return false;
}
}

return true;
}

Expand Down Expand Up @@ -362,6 +374,52 @@ private static void ValidateAlbEvents(LambdaFunctionModel lambdaFunctionModel, L
}
}

private static void ValidateS3Events(LambdaFunctionModel lambdaFunctionModel, Location methodLocation, List<Diagnostic> diagnostics)
{
if (!lambdaFunctionModel.LambdaMethod.Events.Contains(EventType.S3))
return;

// Validate S3EventAttributes
var seenResourceNames = new HashSet<string>();
foreach (var att in lambdaFunctionModel.Attributes)
{
if (att.Type.FullName != TypeFullNames.S3EventAttribute)
continue;

var s3EventAttribute = ((AttributeModel<S3EventAttribute>)att).Data;
var validationErrors = s3EventAttribute.Validate();
validationErrors.ForEach(errorMessage => diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.InvalidS3EventAttribute, methodLocation, errorMessage)));

// Check for duplicate resource names (only when ResourceName is safe to evaluate)
var derivedResourceName = s3EventAttribute.ResourceName;
if (!string.IsNullOrEmpty(derivedResourceName) && !seenResourceNames.Add(derivedResourceName))
{
diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.InvalidS3EventAttribute, methodLocation,
$"Duplicate S3 event resource name '{derivedResourceName}'. Each [S3Event] attribute on the same method must have a unique ResourceName."));
}
}

// Validate method parameters - first param must be S3Event, optional second param ILambdaContext
var parameters = lambdaFunctionModel.LambdaMethod.Parameters;
if (parameters.Count == 0 ||
parameters.Count > 2 ||
(parameters.Count == 1 && parameters[0].Type.FullName != TypeFullNames.S3Event) ||
(parameters.Count == 2 && (parameters[0].Type.FullName != TypeFullNames.S3Event || parameters[1].Type.FullName != TypeFullNames.ILambdaContext)))
{
var errorMessage = $"When using the {nameof(S3EventAttribute)}, the Lambda method can accept at most 2 parameters. " +
$"The first parameter is required and must be of type {TypeFullNames.S3Event}. " +
$"The second parameter is optional and must be of type {TypeFullNames.ILambdaContext}.";
diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.InvalidLambdaMethodSignature, methodLocation, errorMessage));
}

// Validate method return type - must be void or Task
if (!lambdaFunctionModel.LambdaMethod.ReturnsVoid && !lambdaFunctionModel.LambdaMethod.ReturnsVoidTask)
{
var errorMessage = $"When using the {nameof(S3EventAttribute)}, the Lambda method can return either void or {TypeFullNames.Task}";
diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.InvalidLambdaMethodSignature, methodLocation, errorMessage));
}
}

private static bool ReportDiagnostics(DiagnosticReporter diagnosticReporter, List<Diagnostic> diagnostics)
{
var isValid = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Amazon.Lambda.Annotations.SourceGenerator.FileIO;
using Amazon.Lambda.Annotations.SourceGenerator.Models;
using Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes;
using Amazon.Lambda.Annotations.S3;
using Amazon.Lambda.Annotations.SQS;
using Microsoft.CodeAnalysis;
using System;
Expand Down Expand Up @@ -227,6 +228,10 @@ private void ProcessLambdaFunctionEventAttributes(ILambdaFunctionSerializable la
var albResourceNames = ProcessAlbApiAttribute(lambdaFunction, albAttributeModel.Data);
currentAlbResources.AddRange(albResourceNames);
break;
case AttributeModel<S3EventAttribute> s3AttributeModel:
eventName = ProcessS3Attribute(lambdaFunction, s3AttributeModel.Data, currentSyncedEventProperties);
currentSyncedEvents.Add(eventName);
break;
}
}

Expand Down Expand Up @@ -603,6 +608,54 @@ private string ProcessSqsAttribute(ILambdaFunctionSerializable lambdaFunction, S
return att.ResourceName;
}

/// <summary>
/// Writes all properties associated with <see cref="S3EventAttribute"/> to the serverless template.
/// </summary>
private string ProcessS3Attribute(ILambdaFunctionSerializable lambdaFunction, S3EventAttribute att, Dictionary<string, List<string>> syncedEventProperties)
{
var eventName = att.ResourceName;
var eventPath = $"Resources.{lambdaFunction.ResourceName}.Properties.Events.{eventName}";

_templateWriter.SetToken($"{eventPath}.Type", "S3");

// Bucket - always a Ref since S3 events require the bucket resource in the same template (validated to start with "@")
var bucketName = att.Bucket.Substring(1);
_templateWriter.RemoveToken($"{eventPath}.Properties.Bucket");
SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, $"Bucket.{REF}", bucketName);

// Events - list of S3 event types (always written since S3 SAM events require it; uses default "s3:ObjectCreated:*" if not explicitly set)
{
var events = att.Events.Split(';').Select(x => x.Trim()).Where(x => !string.IsNullOrWhiteSpace(x)).ToList();
SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Events", events, TokenType.List);
}

// Filter - S3 key filter rules
if (att.IsFilterPrefixSet || att.IsFilterSuffixSet)
{
var rules = new List<Dictionary<string, string>>();

if (att.IsFilterPrefixSet)
{
rules.Add(new Dictionary<string, string> { { "Name", "prefix" }, { "Value", att.FilterPrefix } });
}

if (att.IsFilterSuffixSet)
{
rules.Add(new Dictionary<string, string> { { "Name", "suffix" }, { "Value", att.FilterSuffix } });
}

SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Filter.S3Key.Rules", rules, TokenType.List);
}

// Enabled
if (att.IsEnabledSet)
{
SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Enabled", att.Enabled);
}

return att.ResourceName;
}

/// <summary>
/// Generates CloudFormation resources for an Application Load Balancer target.
/// Unlike API Gateway events which map to SAM event types, ALB integration requires
Expand Down
Loading
Loading