Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -242,5 +242,12 @@ public static class DiagnosticDescriptors
category: "AWSLambdaCSharpGenerator",
DiagnosticSeverity.Error,
isEnabledByDefault: true);

public static readonly DiagnosticDescriptor InvalidS3EventAttribute = new DiagnosticDescriptor(id: "AWSLambda0133",
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,5 +1,6 @@
using System;
using Amazon.Lambda.Annotations.APIGateway;
using Amazon.Lambda.Annotations.S3;
using Amazon.Lambda.Annotations.SQS;
using Microsoft.CodeAnalysis;

Expand Down Expand Up @@ -90,6 +91,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 @@ -21,7 +21,8 @@ internal class SyntaxReceiver : ISyntaxContextReceiver
{ "RestApiAuthorizerAttribute", "RestApiAuthorizer" },
{ "HttpApiAttribute", "HttpApi" },
{ "RestApiAttribute", "RestApi" },
{ "SQSEventAttribute", "SQSEvent" }
{ "SQSEventAttribute", "SQSEvent" },
{ "S3EventAttribute", "S3Event" }
};

public List<MethodDeclarationSyntax> LambdaMethods { get; } = new List<MethodDeclarationSyntax>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ public static class TypeFullNames
public const string SQSBatchResponse = "Amazon.Lambda.SQSEvents.SQSBatchResponse";
public const string SQSEventAttribute = "Amazon.Lambda.Annotations.SQS.SQSEventAttribute";

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 All @@ -67,7 +70,8 @@ public static class TypeFullNames
{
RestApiAttribute,
HttpApiAttribute,
SQSEventAttribute
SQSEventAttribute,
S3EventAttribute
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Amazon.Lambda.Annotations.SourceGenerator.Extensions;
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.Collections.Generic;
Expand Down Expand Up @@ -59,6 +60,7 @@ internal static bool ValidateFunction(GeneratorExecutionContext context, IMethod
// Validate Events
ValidateApiGatewayEvents(lambdaFunctionModel, methodLocation, diagnostics);
ValidateSqsEvents(lambdaFunctionModel, methodLocation, diagnostics);
ValidateS3Events(lambdaFunctionModel, methodLocation, diagnostics);

return ReportDiagnostics(diagnosticReporter, diagnostics);
}
Expand Down Expand Up @@ -86,6 +88,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 @@ -268,6 +280,43 @@ private static void ValidateSqsEvents(LambdaFunctionModel lambdaFunctionModel, L
}
}

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

// Validate S3EventAttributes
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)));
}

// 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 @@ -3,6 +3,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 @@ -221,6 +222,10 @@ private void ProcessLambdaFunctionEventAttributes(ILambdaFunctionSerializable la
eventName = ProcessSqsAttribute(lambdaFunction, sqsAttributeModel.Data, currentSyncedEventProperties);
currentSyncedEvents.Add(eventName);
break;
case AttributeModel<S3EventAttribute> s3AttributeModel:
eventName = ProcessS3Attribute(lambdaFunction, s3AttributeModel.Data, currentSyncedEventProperties);
currentSyncedEvents.Add(eventName);
break;
}
}

Expand Down Expand Up @@ -596,6 +601,66 @@ 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
var bucketName = att.Bucket.StartsWith("@") ? att.Bucket.Substring(1) : att.Bucket;
_templateWriter.RemoveToken($"{eventPath}.Properties.Bucket");
SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, $"Bucket.{REF}", bucketName);

// Events - list of S3 event types
if (att.IsEventsSet)
{
var events = att.Events.Split(';').Select(x => x.Trim()).ToList();
SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Events", events, TokenType.List);
}

// Filter - S3 key filter rules
if (att.IsFilterPrefixSet || att.IsFilterSuffixSet)
{
if (att.IsFilterPrefixSet)
{
var prefixRules = new List<Dictionary<string, string>> { new Dictionary<string, string> { { "Name", "prefix" }, { "Value", att.FilterPrefix } } };
SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Filter.S3Key.Rules", prefixRules, TokenType.List);
}
if (att.IsFilterSuffixSet)
{
var suffixRule = new Dictionary<string, string> { { "Name", "suffix" }, { "Value", att.FilterSuffix } };
if (att.IsFilterPrefixSet)
{
// Append suffix rule to existing rules list
var rules = new List<Dictionary<string, string>>
{
new Dictionary<string, string> { { "Name", "prefix" }, { "Value", att.FilterPrefix } },
suffixRule
};
SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Filter.S3Key.Rules", rules, TokenType.List);
}
else
{
var suffixRules = new List<Dictionary<string, string>> { suffixRule };
SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Filter.S3Key.Rules", suffixRules, TokenType.List);
}
}
}

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

return att.ResourceName;
}

/// <summary>
/// Writes all properties associated with <see cref="LambdaFunctionRoleAttribute"/> to the serverless template.
/// </summary>
Expand Down
113 changes: 113 additions & 0 deletions Libraries/src/Amazon.Lambda.Annotations/S3/S3EventAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;

namespace Amazon.Lambda.Annotations.S3
{
/// <summary>
/// This attribute defines the S3 event source configuration for a Lambda function.
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class S3EventAttribute : Attribute
{
private static readonly Regex _resourceNameRegex = new Regex("^[a-zA-Z0-9]+$");

/// <summary>
/// The S3 bucket that will act as the event trigger for the Lambda function.
/// This must be a reference to an S3 bucket resource defined in the serverless template, prefixed with "@".
/// </summary>
public string Bucket { get; set; }

/// <summary>
/// The CloudFormation resource name for the S3 event. By default this is derived from the Bucket reference without the "@" prefix.
/// </summary>
public string ResourceName
{
get
{
if (IsResourceNameSet)
return resourceName;
if (Bucket.StartsWith("@"))
return Bucket.Substring(1);
return Bucket;
}
set => resourceName = value;
}
private string resourceName = null;
internal bool IsResourceNameSet => resourceName != null;

/// <summary>
/// Semicolon-separated list of S3 event types. Default is 's3:ObjectCreated:*'.
/// </summary>
public string Events { get; set; } = "s3:ObjectCreated:*";
internal bool IsEventsSet => Events != null;

/// <summary>
/// S3 key prefix filter for the event notification.
/// </summary>
public string FilterPrefix
{
get => filterPrefix;
set => filterPrefix = value;
}
private string filterPrefix = null;
internal bool IsFilterPrefixSet => filterPrefix != null;

/// <summary>
/// S3 key suffix filter for the event notification.
/// </summary>
public string FilterSuffix
{
get => filterSuffix;
set => filterSuffix = value;
}
private string filterSuffix = null;
internal bool IsFilterSuffixSet => filterSuffix != null;

/// <summary>
/// If set to false, the event source will be disabled. Default value is true.
/// </summary>
public bool Enabled
{
get => enabled.GetValueOrDefault(true);
set => enabled = value;
}
private bool? enabled;
internal bool IsEnabledSet => enabled.HasValue;

/// <summary>
/// Creates an instance of the <see cref="S3EventAttribute"/> class.
/// </summary>
/// <param name="bucket"><see cref="Bucket"/> property</param>
public S3EventAttribute(string bucket)
{
Bucket = bucket;
}

internal List<string> Validate()
{
var validationErrors = new List<string>();

if (string.IsNullOrEmpty(Bucket))
{
validationErrors.Add($"{nameof(S3EventAttribute.Bucket)} is required and must not be empty");
}
else if (!Bucket.StartsWith("@"))
{
validationErrors.Add($"{nameof(S3EventAttribute.Bucket)} = {Bucket}. S3 event sources require a reference to an S3 bucket resource in the serverless template. Prefix the resource name with '@'");
}

if (IsResourceNameSet && !_resourceNameRegex.IsMatch(ResourceName))
{
validationErrors.Add($"{nameof(S3EventAttribute.ResourceName)} = {ResourceName}. It must only contain alphanumeric characters and must not be an empty string");
}

if (string.IsNullOrEmpty(Events))
{
validationErrors.Add($"{nameof(S3EventAttribute.Events)} must not be empty");
}

return validationErrors;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@
<ProjectReference Include="..\..\src\Amazon.Lambda.Annotations\Amazon.Lambda.Annotations.csproj" />
<ProjectReference Include="..\..\src\Amazon.Lambda.APIGatewayEvents\Amazon.Lambda.APIGatewayEvents.csproj" />
<ProjectReference Include="..\..\src\Amazon.Lambda.SQSEvents\Amazon.Lambda.SQSEvents.csproj" />
<ProjectReference Include="..\..\src\Amazon.Lambda.S3Events\Amazon.Lambda.S3Events.csproj" />
<!--
We need to force using the .NET Standard 2.0 version because the source generator test framework will complain
about using newer versions of System.Runtime then it can handle. This is not an issue in a end user scenario.
Expand Down
Loading
Loading