Skip to content

Automatically install Amazon.Lambda.TestTool #28

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Feb 7, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
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/b2019317-5414-4315-b539-7735b04f8e63.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"Projects": [
{
"Name": "Aspire.Hosting.AWS",
"Type": "Patch",
"ChangelogMessages": [
"Automatically install .NET Tool Amazon.Lambda.TestTool when running Lambda functions"
]
}
]
}
1 change: 1 addition & 0 deletions Directory.Packages.props
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated to this change, but why does the entire solution has 1 Directory.Packages.props? So anytime we add a new package, we are adding it for the entire solution?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The repo was setup so that it had centralized package versioning. But in the individual project files you still specify the package you want to use. But in the project you don't set a version. For example in the project file of Aspire.Hosting.AWS we add the packages but don't set the version.

  <ItemGroup>
    <PackageReference Include="Aspire.Hosting" />
    <PackageReference Include="Amazon.CDK.Lib" />
    <PackageReference Include="AWSSDK.Core" />
    <PackageReference Include="AWSSDK.CloudFormation" />
  </ItemGroup>

This way you only have to update the version of a package in one place for all projects in a repository.

Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,6 @@
<PackageVersion Include="xunit" Version="2.9.2" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageVersion Include="JsonSchema.Net" Version="7.2.3" />
<PackageVersion Include="Moq" Version="4.20.72" />
</ItemGroup>
</Project>
17 changes: 0 additions & 17 deletions src/Aspire.Hosting.AWS/Lambda/APIGatewayApiResource.cs

This file was deleted.

26 changes: 26 additions & 0 deletions src/Aspire.Hosting.AWS/Lambda/APIGatewayEmulatorResource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.

using Aspire.Hosting.ApplicationModel;
using k8s.KubeConfigModels;

namespace Aspire.Hosting.AWS.Lambda;


/// <summary>
/// Resource representing the Amazon API Gateway emulator.
/// </summary>
/// <param name="name">Aspire resource name</param>
public class APIGatewayEmulatorResource(string name, APIGatewayType apiGatewayType) : ExecutableResource(name,
"dotnet",
Environment.CurrentDirectory
)
{
internal void AddCommandLineArguments(IList<object> arguments)
{
arguments.Add("lambda-test-tool");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't the name of our tool dotnet-lambda-test-tool?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

apparently dotnet lambda-test-tool works and so does dotnet-lambda-test-tool.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed in sync meeting and we are on the same page now.

arguments.Add("--no-launch-window");

arguments.Add("--api-gateway-emulator-mode");
arguments.Add(apiGatewayType.ToString());
}
}
11 changes: 4 additions & 7 deletions src/Aspire.Hosting.AWS/Lambda/APIGatewayExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,12 @@ public static class APIGatewayExtensions
/// <param name="name">Aspire resource name</param>
/// <param name="apiGatewayType">The type of API Gateway API. For example Rest, HttpV1 or HttpV2</param>
/// <returns></returns>
public static IResourceBuilder<APIGatewayApiResource> AddAWSAPIGatewayEmulator(this IDistributedApplicationBuilder builder, string name, APIGatewayType apiGatewayType)
public static IResourceBuilder<APIGatewayEmulatorResource> AddAWSAPIGatewayEmulator(this IDistributedApplicationBuilder builder, string name, APIGatewayType apiGatewayType)
{
var apiGatewayEmulator = builder.AddResource(new APIGatewayApiResource(name)).ExcludeFromManifest();

var apiGatewayEmulator = builder.AddResource(new APIGatewayEmulatorResource(name, apiGatewayType)).ExcludeFromManifest();
apiGatewayEmulator.WithArgs(context =>
{
context.Args.Add("--api-gateway-emulator-mode");
context.Args.Add(apiGatewayType.ToString());
context.Args.Add("--no-launch-window");
apiGatewayEmulator.Resource.AddCommandLineArguments(context.Args);
});

var annotation = new EndpointAnnotation(
Expand Down Expand Up @@ -62,7 +59,7 @@ public static IResourceBuilder<APIGatewayApiResource> AddAWSAPIGatewayEmulator(t
/// <param name="httpMethod">The HTTP method the Lambda function should be called for.</param>
/// <param name="path">The resource path the Lambda function should be called for.</param>
/// <returns></returns>
public static IResourceBuilder<APIGatewayApiResource> WithReference(this IResourceBuilder<APIGatewayApiResource> builder, IResourceBuilder<LambdaProjectResource> lambda, Method httpMethod, string path)
public static IResourceBuilder<APIGatewayEmulatorResource> WithReference(this IResourceBuilder<APIGatewayEmulatorResource> builder, IResourceBuilder<LambdaProjectResource> lambda, Method httpMethod, string path)
{
LambdaEmulatorAnnotation? lambdaEmulatorAnnotation = null;
if (builder.ApplicationBuilder.Resources.FirstOrDefault(x => x.TryGetLastAnnotation<LambdaEmulatorAnnotation>(out lambdaEmulatorAnnotation)) == null ||
Expand Down
18 changes: 18 additions & 0 deletions src/Aspire.Hosting.AWS/Lambda/LambdaEmulatorAnnotation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,22 @@ internal class LambdaEmulatorAnnotation(EndpointReference endpoint) : IResourceA
/// The HTTP endpoint for the Lambda runtime emulator.
/// </summary>
public EndpointReference Endpoint { get; init; } = endpoint;

/// <summary>
/// If set to true Amazon.Lambda.TestTool will updated/installed during AppHost startup. Amazon.Lambda.TestTool is
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the name and the description are saying 2 different things. The name suggests that we won't auto install, but the description is saying that we will?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

/// a .NET Tool that will be installed globally.
/// </summary>
public bool DisableAutoInstall { get; set; }

/// <summary>
/// Override the minimum version of Amazon.Lambda.TestTool that will be installed. If a newer vesion is already installed
/// it will be used unless AllowDowngrade is set to true.
/// </summary>
public string? OverrideMinimumInstallVersion { get; set; }

/// <summary>
/// If set to true and a newer version of the Amazon.Lambda.TestTool is installed then expected the installed version will be downgraded
/// to match the expected version.
/// </summary>
public bool AllowDowngrade { get; set; }
}
27 changes: 27 additions & 0 deletions src/Aspire.Hosting.AWS/Lambda/LambdaEmulatorOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.

namespace Aspire.Hosting.AWS.Lambda;

/// <summary>
/// Options that can be added the Lambda emulator resource.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: add "to" between "added" and "the"

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

/// </summary>
public class LambdaEmulatorOptions
{
/// <summary>
/// If set to true, Amazon.Lambda.TestTool will updated/installed during AppHost startup. Amazon.Lambda.TestTool is
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same comment as before

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

/// a .NET Tool that will be installed globally.
/// </summary>
public bool DisableAutoInstall { get; set; }

/// <summary>
/// Override the minimum version of Amazon.Lambda.TestTool that will be installed. If a newer version is already installed
/// it will be used unless AllowDowngrade is set to true.
/// </summary>
public string? OverrideMinimumInstallVersion { get; set; }

/// <summary>
/// If set to true, and a newer version of Amazon.Lambda.TestTool is already installed then the requested version, the installed version
/// will be downgraded to the request version.
/// </summary>
public bool AllowDowngrade { get; set; }
}
22 changes: 22 additions & 0 deletions src/Aspire.Hosting.AWS/Lambda/LambdaEmulatorResource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.

using Aspire.Hosting.ApplicationModel;

namespace Aspire.Hosting.AWS.Lambda;


/// <summary>
/// Resource representing the Lambda Runtime API service emulator.
/// </summary>
/// <param name="name">Aspire resource name</param>
public class LambdaEmulatorResource(string name) : ExecutableResource(name,
"dotnet",
Environment.CurrentDirectory
)
{
internal void AddCommandLineArguments(IList<object> arguments)
{
arguments.Add("lambda-test-tool");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same question on tool name

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also didn't we change the behavior so that you need to specify the port for the emulator to run? won't this error out as is?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed in sync meeting.

arguments.Add("--no-launch-window");
}
}
76 changes: 53 additions & 23 deletions src/Aspire.Hosting.AWS/Lambda/LambdaExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.

using Amazon.Runtime.Internal.Endpoints.StandardLibrary;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.AWS;
using Aspire.Hosting.AWS.Lambda;
using Aspire.Hosting.AWS.Utils.Internal;
using Aspire.Hosting.Lifecycle;
using Microsoft.Extensions.DependencyInjection;
using System.Collections.Immutable;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection.Extensions;
using System.Diagnostics;
using System.Net.Sockets;
using System.Runtime.Versioning;
Expand Down Expand Up @@ -97,32 +97,62 @@ public static class LambdaExtensions
return resource;
}

private static ExecutableResource AddOrGetLambdaServiceEmulatorResource(IDistributedApplicationBuilder builder)
{
if (builder.Resources.FirstOrDefault(x => x.TryGetAnnotationsOfType<LambdaEmulatorAnnotation>(out _)) is not ExecutableResource serviceEmulator)
/// <summary>
/// Add the Lambda service emulator resource. The AddAWSLambdaFunction method will automatically add the Lambda service emulator if it hasn't
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you add doc references to "AddAWSLambdaFunction" and "LambdaEmulatorOptions"

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

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

var annotation = new EndpointAnnotation(
protocol: ProtocolType.Tcp,
uriScheme: "http");
builder.Services.TryAddSingleton<IProcessCommandService, ProcessCommandService>();

serviceEmulatorBuilder.WithAnnotation(annotation);
var endpointReference = new EndpointReference(serviceEmulatorBuilder.Resource, annotation);
var lambdaEmulator = builder.AddResource(new LambdaEmulatorResource("LambdaServiceEmulator")).ExcludeFromManifest();
lambdaEmulator.WithArgs(context =>
{
lambdaEmulator.Resource.AddCommandLineArguments(context.Args);
});

serviceEmulatorBuilder.WithAnnotation(new LambdaEmulatorAnnotation(endpointReference));
var annotation = new EndpointAnnotation(
protocol: ProtocolType.Tcp,
uriScheme: "http");

serviceEmulatorBuilder.WithAnnotation(new EnvironmentCallbackAnnotation(context =>
{
context.EnvironmentVariables[Constants.IsAspireHostedEnvVariable] = "true";
context.EnvironmentVariables["LAMBDA_RUNTIME_API_PORT"] = endpointReference.Property(EndpointProperty.TargetPort);
}));
lambdaEmulator.WithAnnotation(annotation);
var endpointReference = new EndpointReference(lambdaEmulator.Resource, annotation);

serviceEmulator = serviceEmulatorBuilder.Resource;
lambdaEmulator.WithAnnotation(new LambdaEmulatorAnnotation(endpointReference)
{
DisableAutoInstall = options?.DisableAutoInstall ?? false,
OverrideMinimumInstallVersion = options?.OverrideMinimumInstallVersion,
AllowDowngrade = options?.AllowDowngrade ?? false,
});

lambdaEmulator.WithAnnotation(new EnvironmentCallbackAnnotation(context =>
{
context.EnvironmentVariables[Constants.IsAspireHostedEnvVariable] = "true";
context.EnvironmentVariables["LAMBDA_RUNTIME_API_PORT"] = endpointReference.Property(EndpointProperty.TargetPort);
}));

serviceEmulator = lambdaEmulator.Resource;
builder.Services.TryAddLifecycleHook<LambdaLifecycleHook>();

return lambdaEmulator;
}

private static ExecutableResource AddOrGetLambdaServiceEmulatorResource(IDistributedApplicationBuilder builder)
{
if (builder.Resources.FirstOrDefault(x => x.TryGetAnnotationsOfType<LambdaEmulatorAnnotation>(out _)) is not ExecutableResource serviceEmulator)
{
serviceEmulator = builder.AddAWSLambdaServiceEmulator().Resource;
}

return serviceEmulator;
Expand Down
126 changes: 126 additions & 0 deletions src/Aspire.Hosting.AWS/Lambda/LambdaLifecycleHook.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.

using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.AWS.Utils.Internal;
using Aspire.Hosting.Lifecycle;
using Microsoft.Extensions.Logging;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading;
using static Google.Protobuf.Reflection.GeneratedCodeInfo.Types;

namespace Aspire.Hosting.AWS.Lambda;

/// <summary>
/// Lambda lifecycle hook takes care of getting Amazon.Lambda.TestTool is installed if there was
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: is installed -> installed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

/// a Lambda service emulator added to the resources.
/// </summary>
/// <param name="logger"></param>
internal class LambdaLifecycleHook(ILogger<LambdaEmulatorResource> logger, IProcessCommandService processCommandService) : IDistributedApplicationLifecycleHook
{
internal const string DefaultLambdaTestToolVersion = "0.0.2-preview";

public async Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default)
{
LambdaEmulatorAnnotation? emulatorAnnotation = null;
if (appModel.Resources.FirstOrDefault(x => x.TryGetLastAnnotation<LambdaEmulatorAnnotation>(out emulatorAnnotation)) != null && emulatorAnnotation != null)
{
await ApplyLambdaEmulatorAnnotationAsync(emulatorAnnotation, cancellationToken);
}
else
{
logger.LogDebug("Skipping installing Amazon.Lambda.TestTool since no Lambda emulator resource was found");
}
}

internal async Task ApplyLambdaEmulatorAnnotationAsync(LambdaEmulatorAnnotation emulatorAnnotation, CancellationToken cancellationToken = default)
{
if (emulatorAnnotation.DisableAutoInstall)
{
return;
}

var expectedVersion = emulatorAnnotation.OverrideMinimumInstallVersion ?? DefaultLambdaTestToolVersion;
var installedVersion = await GetCurrentInstalledVersionAsync(cancellationToken);

if (ShouldInstall(installedVersion, expectedVersion, emulatorAnnotation.AllowDowngrade))
{
logger.LogDebug("Installing .NET Tool Amazon.Lambda.TestTool ({version})", installedVersion);

var commandLineArgument = $"tool install -g Amazon.Lambda.TestTool --version {expectedVersion}";
if (emulatorAnnotation.AllowDowngrade)
{
commandLineArgument += " --allow-downgrade";
}

var result = await processCommandService.RunProcessAndCaptureOuputAsync(logger, "dotnet", commandLineArgument, cancellationToken);
if (result.ExitCode == 0)
{
if (!string.IsNullOrEmpty(installedVersion))
{
logger.LogInformation("Successfully Updated .NET Tool Amazon.Lambda.TestTool from version {installedVersion} to {newVersion}", installedVersion, expectedVersion);
}
else
{
logger.LogInformation("Successfully installed .NET Tool Amazon.Lambda.TestTool ({version})", expectedVersion);
}
}
else
{
if (!string.IsNullOrEmpty(installedVersion))
{
logger.LogWarning("Failed to update Amazon.Lambda.TestTool from {installedVersion} to {expectedVersion}:\n{output}", installedVersion, expectedVersion, result.Output);
}
else
{
logger.LogError("Fail to install Amazon.Lambda.TestTool ({version}) required for running Lambda functions locally:\n{output}", expectedVersion, result.Output);
}
}
}
else
{
logger.LogInformation("Amazon.Lambda.TestTool version {version} already installed", installedVersion);
}
}

internal static bool ShouldInstall(string currentInstalledVersion, string expectedVersionStr, bool allowDowngrading)
{
if (string.IsNullOrEmpty(currentInstalledVersion))
{
return true;
}

var installedVersion = Version.Parse(currentInstalledVersion.Replace("-preview", string.Empty));
var expectedVersion = Version.Parse(expectedVersionStr.Replace("-preview", string.Empty));

return (installedVersion < expectedVersion) || (allowDowngrading && installedVersion != expectedVersion);
}

private async Task<string> GetCurrentInstalledVersionAsync(CancellationToken cancellationToken)
{
var results = await processCommandService.RunProcessAndCaptureOuputAsync(logger, "dotnet", "lambda-test-tool --tool-info", cancellationToken);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might need to update based on other PR changes aws/aws-lambda-dotnet#1969

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

if (results.ExitCode != 0)
{
return string.Empty;
}

try
{
var versionDoc = JsonNode.Parse(results.Output);
if (versionDoc == null)
{
logger.LogWarning("Error parsing version information from Amazon.Lambda.TestTool: {versionInfo}", results.Output);
return string.Empty;

}
var version = versionDoc["version"]?.ToString();
logger.LogDebug("Installed version of Amazon.Lambda.TestTool is {version}", version);
return version ?? string.Empty;
}
catch (JsonException ex)
{
logger.LogWarning(ex, "Error parsing version information from Amazon.Lambda.TestTool: {versionInfo}", results.Output);
return string.Empty;
}
}
}
Loading
Loading