Skip to content

Commit ffbb42b

Browse files
committed
For the Lambda integration automatically install Amazon.Lambda.TestTool which is the Lambda service emulator
1 parent fc8dfbb commit ffbb42b

20 files changed

+575
-57
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"Projects": [
3+
{
4+
"Name": "Aspire.Hosting.AWS",
5+
"Type": "Patch",
6+
"ChangelogMessages": [
7+
"Automatically install .NET Tool Amazon.Lambda.TestTool when running Lambda functions"
8+
]
9+
}
10+
]
11+
}

Directory.Packages.props

+1
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,6 @@
3939
<PackageVersion Include="xunit" Version="2.9.2" />
4040
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
4141
<PackageVersion Include="JsonSchema.Net" Version="7.2.3" />
42+
<PackageVersion Include="Moq" Version="4.20.72" />
4243
</ItemGroup>
4344
</Project>

src/Aspire.Hosting.AWS/Lambda/APIGatewayApiResource.cs

-17
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
3+
using Aspire.Hosting.ApplicationModel;
4+
using k8s.KubeConfigModels;
5+
6+
namespace Aspire.Hosting.AWS.Lambda;
7+
8+
9+
/// <summary>
10+
/// Resource representing the Amazon API Gateway emulator.
11+
/// </summary>
12+
/// <param name="name">Aspire resource name</param>
13+
public class APIGatewayEmulatorResource(string name, APIGatewayType apiGatewayType) : ExecutableResource(name,
14+
"dotnet",
15+
Environment.CurrentDirectory
16+
)
17+
{
18+
internal void AddCommandLineArguments(IList<object> arguments)
19+
{
20+
arguments.Add("lambda-test-tool");
21+
arguments.Add("--no-launch-window");
22+
23+
arguments.Add("--api-gateway-emulator-mode");
24+
arguments.Add(apiGatewayType.ToString());
25+
}
26+
}

src/Aspire.Hosting.AWS/Lambda/APIGatewayExtensions.cs

+4-7
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,12 @@ public static class APIGatewayExtensions
2424
/// <param name="name">Aspire resource name</param>
2525
/// <param name="apiGatewayType">The type of API Gateway API. For example Rest, HttpV1 or HttpV2</param>
2626
/// <returns></returns>
27-
public static IResourceBuilder<APIGatewayApiResource> AddAWSAPIGatewayEmulator(this IDistributedApplicationBuilder builder, string name, APIGatewayType apiGatewayType)
27+
public static IResourceBuilder<APIGatewayEmulatorResource> AddAWSAPIGatewayEmulator(this IDistributedApplicationBuilder builder, string name, APIGatewayType apiGatewayType)
2828
{
29-
var apiGatewayEmulator = builder.AddResource(new APIGatewayApiResource(name)).ExcludeFromManifest();
30-
29+
var apiGatewayEmulator = builder.AddResource(new APIGatewayEmulatorResource(name, apiGatewayType)).ExcludeFromManifest();
3130
apiGatewayEmulator.WithArgs(context =>
3231
{
33-
context.Args.Add("--api-gateway-emulator-mode");
34-
context.Args.Add(apiGatewayType.ToString());
35-
context.Args.Add("--no-launch-window");
32+
apiGatewayEmulator.Resource.AddCommandLineArguments(context.Args);
3633
});
3734

3835
var annotation = new EndpointAnnotation(
@@ -62,7 +59,7 @@ public static IResourceBuilder<APIGatewayApiResource> AddAWSAPIGatewayEmulator(t
6259
/// <param name="httpMethod">The HTTP method the Lambda function should be called for.</param>
6360
/// <param name="path">The resource path the Lambda function should be called for.</param>
6461
/// <returns></returns>
65-
public static IResourceBuilder<APIGatewayApiResource> WithReference(this IResourceBuilder<APIGatewayApiResource> builder, IResourceBuilder<LambdaProjectResource> lambda, Method httpMethod, string path)
62+
public static IResourceBuilder<APIGatewayEmulatorResource> WithReference(this IResourceBuilder<APIGatewayEmulatorResource> builder, IResourceBuilder<LambdaProjectResource> lambda, Method httpMethod, string path)
6663
{
6764
LambdaEmulatorAnnotation? lambdaEmulatorAnnotation = null;
6865
if (builder.ApplicationBuilder.Resources.FirstOrDefault(x => x.TryGetLastAnnotation<LambdaEmulatorAnnotation>(out lambdaEmulatorAnnotation)) == null ||

src/Aspire.Hosting.AWS/Lambda/LambdaEmulatorAnnotation.cs

+18
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,22 @@ internal class LambdaEmulatorAnnotation(EndpointReference endpoint) : IResourceA
1414
/// The HTTP endpoint for the Lambda runtime emulator.
1515
/// </summary>
1616
public EndpointReference Endpoint { get; init; } = endpoint;
17+
18+
/// <summary>
19+
/// If set to true Amazon.Lambda.TestTool will updated/installed during AppHost startup. Amazon.Lambda.TestTool is
20+
/// a .NET Tool that will be installed globally.
21+
/// </summary>
22+
public bool DisableAutoInstall { get; set; }
23+
24+
/// <summary>
25+
/// Override the minimum version of Amazon.Lambda.TestTool that will be installed. If a newer vesion is already installed
26+
/// it will be used unless AllowDowngrade is set to true.
27+
/// </summary>
28+
public string? OverrideMinimumInstallVersion { get; set; }
29+
30+
/// <summary>
31+
/// If set to true and a newer version of the Amazon.Lambda.TestTool is installed then expected the installed version will be downgraded
32+
/// to match the expected version.
33+
/// </summary>
34+
public bool AllowDowngrade { get; set; }
1735
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
3+
namespace Aspire.Hosting.AWS.Lambda;
4+
5+
/// <summary>
6+
/// Options that can be added the Lambda emulator resource.
7+
/// </summary>
8+
public class LambdaEmulatorOptions
9+
{
10+
/// <summary>
11+
/// If set to true, Amazon.Lambda.TestTool will updated/installed during AppHost startup. Amazon.Lambda.TestTool is
12+
/// a .NET Tool that will be installed globally.
13+
/// </summary>
14+
public bool DisableAutoInstall { get; set; }
15+
16+
/// <summary>
17+
/// Override the minimum version of Amazon.Lambda.TestTool that will be installed. If a newer version is already installed
18+
/// it will be used unless AllowDowngrade is set to true.
19+
/// </summary>
20+
public string? OverrideMinimumInstallVersion { get; set; }
21+
22+
/// <summary>
23+
/// If set to true, and a newer version of Amazon.Lambda.TestTool is already installed then the requested version, the installed version
24+
/// will be downgraded to the request version.
25+
/// </summary>
26+
public bool AllowDowngrade { get; set; }
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
3+
using Aspire.Hosting.ApplicationModel;
4+
5+
namespace Aspire.Hosting.AWS.Lambda;
6+
7+
8+
/// <summary>
9+
/// Resource representing the Lambda Runtime API service emulator.
10+
/// </summary>
11+
/// <param name="name">Aspire resource name</param>
12+
public class LambdaEmulatorResource(string name) : ExecutableResource(name,
13+
"dotnet",
14+
Environment.CurrentDirectory
15+
)
16+
{
17+
internal void AddCommandLineArguments(IList<object> arguments)
18+
{
19+
arguments.Add("lambda-test-tool");
20+
arguments.Add("--no-launch-window");
21+
}
22+
}

src/Aspire.Hosting.AWS/Lambda/LambdaExtensions.cs

+53-23
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22

3-
using Amazon.Runtime.Internal.Endpoints.StandardLibrary;
43
using Aspire.Hosting.ApplicationModel;
54
using Aspire.Hosting.AWS;
65
using Aspire.Hosting.AWS.Lambda;
6+
using Aspire.Hosting.AWS.Utils.Internal;
77
using Aspire.Hosting.Lifecycle;
8-
using Microsoft.Extensions.DependencyInjection;
9-
using System.Collections.Immutable;
8+
using Microsoft.AspNetCore.Http;
9+
using Microsoft.Extensions.DependencyInjection.Extensions;
1010
using System.Diagnostics;
1111
using System.Net.Sockets;
1212
using System.Runtime.Versioning;
@@ -97,32 +97,62 @@ public static class LambdaExtensions
9797
return resource;
9898
}
9999

100-
private static ExecutableResource AddOrGetLambdaServiceEmulatorResource(IDistributedApplicationBuilder builder)
101-
{
102-
if (builder.Resources.FirstOrDefault(x => x.TryGetAnnotationsOfType<LambdaEmulatorAnnotation>(out _)) is not ExecutableResource serviceEmulator)
100+
/// <summary>
101+
/// Add the Lambda service emulator resource. The AddAWSLambdaFunction method will automatically add the Lambda service emulator if it hasn't
102+
/// already been added. This method only needs to be called if the emulator needs to be customized with the LambdaEmulatorOptions. If
103+
/// this method is called it must be called only once and before any AddAWSLambdaFunction calls.
104+
/// </summary>
105+
/// <param name="builder"></param>
106+
/// <param name="options">The options to configure the emulator with.</param>
107+
/// <returns></returns>
108+
/// <exception cref="InvalidOperationException">Thrown if the Lambda service emulator has already been added.</exception>
109+
public static IResourceBuilder<LambdaEmulatorResource> AddAWSLambdaServiceEmulator(this IDistributedApplicationBuilder builder, LambdaEmulatorOptions? options = null)
110+
{
111+
if (builder.Resources.FirstOrDefault(x => x.TryGetAnnotationsOfType<LambdaEmulatorAnnotation>(out _)) is ExecutableResource serviceEmulator)
103112
{
104-
var serviceEmulatorBuilder = builder.AddExecutable($"Lambda-ServiceEmulator",
105-
"dotnet-lambda-test-tool",
106-
Environment.CurrentDirectory,
107-
"--no-launch-window")
108-
.ExcludeFromManifest();
113+
throw new InvalidOperationException("A Lambda service emulator has already been added. The AddAWSLambdaFunction will add the emulator " +
114+
"if it hasn't already been added. This method must be called before AddAWSLambdaFunction if the Lambda service emulator needs to be customized.");
115+
}
109116

110-
var annotation = new EndpointAnnotation(
111-
protocol: ProtocolType.Tcp,
112-
uriScheme: "http");
117+
builder.Services.TryAddSingleton<IProcessCommandService, ProcessCommandService>();
113118

114-
serviceEmulatorBuilder.WithAnnotation(annotation);
115-
var endpointReference = new EndpointReference(serviceEmulatorBuilder.Resource, annotation);
119+
var lambdaEmulator = builder.AddResource(new LambdaEmulatorResource("LambdaServiceEmulator")).ExcludeFromManifest();
120+
lambdaEmulator.WithArgs(context =>
121+
{
122+
lambdaEmulator.Resource.AddCommandLineArguments(context.Args);
123+
});
116124

117-
serviceEmulatorBuilder.WithAnnotation(new LambdaEmulatorAnnotation(endpointReference));
125+
var annotation = new EndpointAnnotation(
126+
protocol: ProtocolType.Tcp,
127+
uriScheme: "http");
118128

119-
serviceEmulatorBuilder.WithAnnotation(new EnvironmentCallbackAnnotation(context =>
120-
{
121-
context.EnvironmentVariables[Constants.IsAspireHostedEnvVariable] = "true";
122-
context.EnvironmentVariables["LAMBDA_RUNTIME_API_PORT"] = endpointReference.Property(EndpointProperty.TargetPort);
123-
}));
129+
lambdaEmulator.WithAnnotation(annotation);
130+
var endpointReference = new EndpointReference(lambdaEmulator.Resource, annotation);
124131

125-
serviceEmulator = serviceEmulatorBuilder.Resource;
132+
lambdaEmulator.WithAnnotation(new LambdaEmulatorAnnotation(endpointReference)
133+
{
134+
DisableAutoInstall = options?.DisableAutoInstall ?? false,
135+
OverrideMinimumInstallVersion = options?.OverrideMinimumInstallVersion,
136+
AllowDowngrade = options?.AllowDowngrade ?? false,
137+
});
138+
139+
lambdaEmulator.WithAnnotation(new EnvironmentCallbackAnnotation(context =>
140+
{
141+
context.EnvironmentVariables[Constants.IsAspireHostedEnvVariable] = "true";
142+
context.EnvironmentVariables["LAMBDA_RUNTIME_API_PORT"] = endpointReference.Property(EndpointProperty.TargetPort);
143+
}));
144+
145+
serviceEmulator = lambdaEmulator.Resource;
146+
builder.Services.TryAddLifecycleHook<LambdaLifecycleHook>();
147+
148+
return lambdaEmulator;
149+
}
150+
151+
private static ExecutableResource AddOrGetLambdaServiceEmulatorResource(IDistributedApplicationBuilder builder)
152+
{
153+
if (builder.Resources.FirstOrDefault(x => x.TryGetAnnotationsOfType<LambdaEmulatorAnnotation>(out _)) is not ExecutableResource serviceEmulator)
154+
{
155+
serviceEmulator = builder.AddAWSLambdaServiceEmulator().Resource;
126156
}
127157

128158
return serviceEmulator;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
3+
using Aspire.Hosting.ApplicationModel;
4+
using Aspire.Hosting.AWS.Utils.Internal;
5+
using Aspire.Hosting.Lifecycle;
6+
using Microsoft.Extensions.Logging;
7+
using System.Text.Json;
8+
using System.Text.Json.Nodes;
9+
using System.Threading;
10+
using static Google.Protobuf.Reflection.GeneratedCodeInfo.Types;
11+
12+
namespace Aspire.Hosting.AWS.Lambda;
13+
14+
/// <summary>
15+
/// Lambda lifecycle hook takes care of getting Amazon.Lambda.TestTool is installed if there was
16+
/// a Lambda service emulator added to the resources.
17+
/// </summary>
18+
/// <param name="logger"></param>
19+
internal class LambdaLifecycleHook(ILogger<LambdaEmulatorResource> logger, IProcessCommandService processCommandService) : IDistributedApplicationLifecycleHook
20+
{
21+
internal const string DefaultLambdaTestToolVersion = "0.0.2-preview";
22+
23+
public async Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default)
24+
{
25+
LambdaEmulatorAnnotation? emulatorAnnotation = null;
26+
if (appModel.Resources.FirstOrDefault(x => x.TryGetLastAnnotation<LambdaEmulatorAnnotation>(out emulatorAnnotation)) != null && emulatorAnnotation != null)
27+
{
28+
await ApplyLambdaEmulatorAnnotationAsync(emulatorAnnotation, cancellationToken);
29+
}
30+
else
31+
{
32+
logger.LogDebug("Skipping installing Amazon.Lambda.TestTool since no Lambda emulator resource was found");
33+
}
34+
}
35+
36+
internal async Task ApplyLambdaEmulatorAnnotationAsync(LambdaEmulatorAnnotation emulatorAnnotation, CancellationToken cancellationToken = default)
37+
{
38+
if (emulatorAnnotation.DisableAutoInstall)
39+
{
40+
return;
41+
}
42+
43+
var expectedVersion = emulatorAnnotation.OverrideMinimumInstallVersion ?? DefaultLambdaTestToolVersion;
44+
var installedVersion = await GetCurrentInstalledVersionAsync(cancellationToken);
45+
46+
if (ShouldInstall(installedVersion, expectedVersion, emulatorAnnotation.AllowDowngrade))
47+
{
48+
logger.LogDebug("Installing .NET Tool Amazon.Lambda.TestTool ({version})", installedVersion);
49+
50+
var commandLineArgument = $"tool install -g Amazon.Lambda.TestTool --version {expectedVersion}";
51+
if (emulatorAnnotation.AllowDowngrade)
52+
{
53+
commandLineArgument += " --allow-downgrade";
54+
}
55+
56+
var result = await processCommandService.RunProcessAndCaptureOuputAsync(logger, "dotnet", commandLineArgument, cancellationToken);
57+
if (result.ExitCode == 0)
58+
{
59+
if (!string.IsNullOrEmpty(installedVersion))
60+
{
61+
logger.LogInformation("Successfully Updated .NET Tool Amazon.Lambda.TestTool from version {installedVersion} to {newVersion}", installedVersion, expectedVersion);
62+
}
63+
else
64+
{
65+
logger.LogInformation("Successfully installed .NET Tool Amazon.Lambda.TestTool ({version})", expectedVersion);
66+
}
67+
}
68+
else
69+
{
70+
if (!string.IsNullOrEmpty(installedVersion))
71+
{
72+
logger.LogWarning("Failed to update Amazon.Lambda.TestTool from {installedVersion} to {expectedVersion}:\n{output}", installedVersion, expectedVersion, result.Output);
73+
}
74+
else
75+
{
76+
logger.LogError("Fail to install Amazon.Lambda.TestTool ({version}) required for running Lambda functions locally:\n{output}", expectedVersion, result.Output);
77+
}
78+
}
79+
}
80+
else
81+
{
82+
logger.LogInformation("Amazon.Lambda.TestTool version {version} already installed", installedVersion);
83+
}
84+
}
85+
86+
internal static bool ShouldInstall(string currentInstalledVersion, string expectedVersionStr, bool allowDowngrading)
87+
{
88+
if (string.IsNullOrEmpty(currentInstalledVersion))
89+
{
90+
return true;
91+
}
92+
93+
var installedVersion = Version.Parse(currentInstalledVersion.Replace("-preview", string.Empty));
94+
var expectedVersion = Version.Parse(expectedVersionStr.Replace("-preview", string.Empty));
95+
96+
return (installedVersion < expectedVersion) || (allowDowngrading && installedVersion != expectedVersion);
97+
}
98+
99+
private async Task<string> GetCurrentInstalledVersionAsync(CancellationToken cancellationToken)
100+
{
101+
var results = await processCommandService.RunProcessAndCaptureOuputAsync(logger, "dotnet", "lambda-test-tool --tool-info", cancellationToken);
102+
if (results.ExitCode != 0)
103+
{
104+
return string.Empty;
105+
}
106+
107+
try
108+
{
109+
var versionDoc = JsonNode.Parse(results.Output);
110+
if (versionDoc == null)
111+
{
112+
logger.LogWarning("Error parsing version information from Amazon.Lambda.TestTool: {versionInfo}", results.Output);
113+
return string.Empty;
114+
115+
}
116+
var version = versionDoc["version"]?.ToString();
117+
logger.LogDebug("Installed version of Amazon.Lambda.TestTool is {version}", version);
118+
return version ?? string.Empty;
119+
}
120+
catch (JsonException ex)
121+
{
122+
logger.LogWarning(ex, "Error parsing version information from Amazon.Lambda.TestTool: {versionInfo}", results.Output);
123+
return string.Empty;
124+
}
125+
}
126+
}

0 commit comments

Comments
 (0)