Skip to content

Add port environment variables for Aspire integration #1942

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
Jan 27, 2025
Merged
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
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<Description>A tool to help debug and test your .NET AWS Lambda functions locally.</Description>
Expand All @@ -16,7 +16,7 @@
<Version>0.0.1-beta.1</Version>
</PropertyGroup>

<ItemGroup>
<ItemGroup Condition="$(Configuration) == 'Release'">
<None Include="$(OutputPath)\publish\wwwroot\" Pack="true" PackagePath="tools\net8.0\any\wwwroot" Visible="false" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Amazon.Lambda.TestTool.Models;
using Amazon.Lambda.TestTool.Processes;
using Amazon.Lambda.TestTool.Services;
using Amazon.Lambda.TestTool.Services.IO;
using Spectre.Console.Cli;

namespace Amazon.Lambda.TestTool.Commands;
Expand All @@ -15,15 +16,20 @@ namespace Amazon.Lambda.TestTool.Commands;
/// The default command of the application which is responsible for launching the Lambda Runtime API and the API Gateway Emulator.
/// </summary>
public sealed class RunCommand(
IToolInteractiveService toolInteractiveService) : CancellableAsyncCommand<RunCommandSettings>
IToolInteractiveService toolInteractiveService, IEnvironmentManager environmentManager) : CancellableAsyncCommand<RunCommandSettings>
{
public const string LAMBDA_RUNTIME_API_PORT = "LAMBDA_RUNTIME_API_PORT";
public const string API_GATEWAY_EMULATOR_PORT = "API_GATEWAY_EMULATOR_PORT";

/// <summary>
/// The method responsible for executing the <see cref="RunCommand"/>.
/// </summary>
public override async Task<int> ExecuteAsync(CommandContext context, RunCommandSettings settings, CancellationTokenSource cancellationTokenSource)
{
try
{
EvaluateEnvironmentVariables(settings);

var tasks = new List<Task>();

var testToolProcess = TestToolProcess.Startup(settings, cancellationTokenSource.Token);
Expand Down Expand Up @@ -80,4 +86,36 @@ public override async Task<int> ExecuteAsync(CommandContext context, RunCommandS
await cancellationTokenSource.CancelAsync();
}
}

private void EvaluateEnvironmentVariables(RunCommandSettings settings)
{
var environmentVariables = environmentManager.GetEnvironmentVariables();
if (environmentVariables == null)
return;

if (environmentVariables.Contains(LAMBDA_RUNTIME_API_PORT))
{
var envValue = environmentVariables[LAMBDA_RUNTIME_API_PORT]?.ToString();
if (int.TryParse(envValue, out var port))
{
settings.Port = port;
}
else
{
throw new ArgumentException($"Value for {LAMBDA_RUNTIME_API_PORT} environment variable was not a valid port number");
}
}
if (environmentVariables.Contains(API_GATEWAY_EMULATOR_PORT))
{
var envValue = environmentVariables[API_GATEWAY_EMULATOR_PORT]?.ToString();
if (int.TryParse(envValue, out var port))
{
settings.ApiGatewayEmulatorPort = port;
}
else
{
throw new ArgumentException($"Value for {API_GATEWAY_EMULATOR_PORT} environment variable was not a valid port number");
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -168,9 +168,11 @@ public static async Task<APIGatewayProxyRequest> ToApiGatewayRequest(

if (emulatorMode == ApiGatewayEmulatorMode.Rest) // rest uses encoded value for the path params
{
#pragma warning disable SYSLIB0013 // Type or member is obsolete
var encodedPathParameters = pathParameters.ToDictionary(
kvp => kvp.Key,
kvp => Uri.EscapeUriString(kvp.Value)); // intentionally using EscapeURiString over EscapeDataString since EscapeURiString correctly handles reserved characters :/?#[]@!$&'()*+,;= in this case
kvp => Uri.EscapeUriString(kvp.Value)); // intentionally using EscapeUriString over EscapeDataString since EscapeUriString correctly handles reserved characters :/?#[]@!$&'()*+,;= in this case
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there no better way for us to have the same behavior without using an obsolete method? What if they drop it in .NET10?

Copy link
Member Author

Choose a reason for hiding this comment

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

@gcbeattyAWS Can you answer this question since this was your change?

Copy link

Choose a reason for hiding this comment

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

so i tried looking for an alternative function that was not deprecated and produced the same output that we needed - but i could not find any. i think i tried 5 or 6 different ways and spent a couple hours on it. If we want to ensure the function never gets dropped I think we could just re-implement/copy their source code for it no?

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not worried about the method being dropped. Methods being dropped from the BCL is a big deal that is rarely done.

#pragma warning restore SYSLIB0013 // Type or member is obsolete
pathParameters = encodedPathParameters;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public static APIGatewayProxyResponse ToApiGatewayProxyResponse(this InvokeRespo
string responseJson = reader.ReadToEnd();
try
{
return JsonSerializer.Deserialize<APIGatewayProxyResponse>(responseJson);
return JsonSerializer.Deserialize<APIGatewayProxyResponse>(responseJson)!;
}
catch
{
Expand Down Expand Up @@ -132,7 +132,7 @@ private static APIGatewayHttpApiV2ProxyResponse ToHttpApiV2Response(string respo
// It has a statusCode property, so try to deserialize as full response
try
{
return JsonSerializer.Deserialize<APIGatewayHttpApiV2ProxyResponse>(response);
return JsonSerializer.Deserialize<APIGatewayHttpApiV2ProxyResponse>(response)!;
}
catch
{
Expand All @@ -155,7 +155,7 @@ private static APIGatewayHttpApiV2ProxyResponse ToHttpApiV2Response(string respo
// return "test", it actually comes as "\"test\"" to response. So we need to get the raw string which is what api gateway does.
if (jsonElement.ValueKind == JsonValueKind.String)
{
response = jsonElement.GetString();
response = jsonElement.GetString()!;
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public static void AddCustomServices(this IServiceCollection serviceCollection,
{
serviceCollection.TryAdd(new ServiceDescriptor(typeof(IToolInteractiveService), typeof(ConsoleInteractiveService), lifetime));
serviceCollection.TryAdd(new ServiceDescriptor(typeof(IDirectoryManager), typeof(DirectoryManager), lifetime));
serviceCollection.TryAdd(new ServiceDescriptor(typeof(IEnvironmentManager), typeof(EnvironmentManager), lifetime));
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public class ApiGatewayRouteConfigService : IApiGatewayRouteConfigService
{
private readonly ILogger<ApiGatewayRouteConfigService> _logger;
private readonly IEnvironmentManager _environmentManager;
private List<ApiGatewayRouteConfig> _routeConfigs = new();
private readonly List<ApiGatewayRouteConfig> _routeConfigs = new();

/// <summary>
/// Constructs an instance of <see cref="ApiGatewayRouteConfigService"/>.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ public static (IDictionary<string, string>, IDictionary<string, IList<string>>)
{
var key = lowerCaseKeyName ? header.Key.ToLower() : header.Key;
singleValueHeaders[key] = header.Value.Last() ?? "";
multiValueHeaders[key] = [.. header.Value];
multiValueHeaders[key] = [.. header.Value!];
Copy link
Member Author

Choose a reason for hiding this comment

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

I think the null warnings happening in this file were a false positive. They warnings only show up when doing dotnet pack not in VS. And the Value is a struct so it can't be null.

}

return (singleValueHeaders, multiValueHeaders);
Expand All @@ -133,7 +133,7 @@ public static (IDictionary<string, string>, IDictionary<string, IList<string>>)
foreach (var param in query)
{
singleValueParams[param.Key] = param.Value.Last() ?? "";
multiValueParams[param.Key] = [.. param.Value];
multiValueParams[param.Key] = [.. param.Value!];
}

return (singleValueParams, multiValueParams);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

using System.Collections;
using Amazon.Lambda.TestTool.Services.IO;

namespace Amazon.Lambda.TestTool.Utilities;

public class LocalEnvironmentManager(IDictionary environmentManager) : IEnvironmentManager
{
public IDictionary GetEnvironmentVariables() => environmentManager;
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public static Dictionary<string, string> ExtractPathParameters(string routeTempl

foreach (var param in template.Parameters)
{
if (routeValues.TryGetValue(param.Name, out var value))
if (routeValues.TryGetValue(param.Name!, out var value))
{
var stringValue = value?.ToString() ?? string.Empty;

Expand All @@ -51,7 +51,7 @@ public static Dictionary<string, string> ExtractPathParameters(string routeTempl
}

// Restore original parameter name
var originalParamName = RestoreOriginalParamName(param.Name);
var originalParamName = RestoreOriginalParamName(param.Name!);
result[originalParamName] = stringValue;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Amazon.Lambda.TestTool.Commands.Settings;
using Amazon.Lambda.TestTool.Models;
using Amazon.Lambda.TestTool.Services;
using Amazon.Lambda.TestTool.Services.IO;
using Moq;
using Spectre.Console.Cli;
using Xunit;
Expand All @@ -17,6 +18,7 @@ namespace Amazon.Lambda.TestTool.IntegrationTests;

public class ApiGatewayEmulatorProcessTests : IAsyncDisposable
{
private readonly Mock<IEnvironmentManager> _mockEnvironmentManager = new Mock<IEnvironmentManager>();
private readonly Mock<IToolInteractiveService> _mockInteractiveService = new Mock<IToolInteractiveService>();
private readonly Mock<IRemainingArguments> _mockRemainingArgs = new Mock<IRemainingArguments>();
private readonly ITestOutputHelper _testOutputHelper;
Expand Down Expand Up @@ -245,7 +247,7 @@ private void StartTestToolProcess(ApiGatewayEmulatorMode apiGatewayMode, TestCon
}}");
cancellationTokenSource.CancelAfter(5000);
var settings = new RunCommandSettings { Port = lambdaPort, NoLaunchWindow = true, ApiGatewayEmulatorMode = apiGatewayMode,ApiGatewayEmulatorPort = apiGatewayPort};
var command = new RunCommand(_mockInteractiveService.Object);
var command = new RunCommand(_mockInteractiveService.Object, _mockEnvironmentManager.Object);
var context = new CommandContext(new List<string>(), _mockRemainingArgs.Object, "run", null);

// Act
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

using Amazon.Lambda.TestTool.Commands.Settings;
Expand All @@ -9,11 +9,14 @@
using Moq;
using Amazon.Lambda.TestTool.UnitTests.Helpers;
using Xunit;
using Amazon.Lambda.TestTool.Services.IO;
using Amazon.Lambda.TestTool.Utilities;

namespace Amazon.Lambda.TestTool.UnitTests.Commands;

public class RunCommandTests
{
private readonly Mock<IEnvironmentManager> _mockEnvironmentManager = new Mock<IEnvironmentManager>();
private readonly Mock<IToolInteractiveService> _mockInteractiveService = new Mock<IToolInteractiveService>();
private readonly Mock<IRemainingArguments> _mockRemainingArgs = new Mock<IRemainingArguments>();

Expand All @@ -25,7 +28,7 @@ public async Task ExecuteAsync_LambdaRuntimeApi_SuccessfulLaunch()
var cancellationSource = new CancellationTokenSource();
cancellationSource.CancelAfter(5000);
var settings = new RunCommandSettings { Port = 9001, NoLaunchWindow = true };
var command = new RunCommand(_mockInteractiveService.Object);
var command = new RunCommand(_mockInteractiveService.Object, _mockEnvironmentManager.Object);
var context = new CommandContext(new List<string>(), _mockRemainingArgs.Object, "run", null);
var apiUrl = $"http://{settings.Host}:{settings.Port}";

Expand All @@ -48,7 +51,7 @@ public async Task ExecuteAsync_ApiGatewayEmulator_SuccessfulLaunch()
var cancellationSource = new CancellationTokenSource();
cancellationSource.CancelAfter(5000);
var settings = new RunCommandSettings { Port = 9002, ApiGatewayEmulatorMode = ApiGatewayEmulatorMode.HttpV2, NoLaunchWindow = true};
var command = new RunCommand(_mockInteractiveService.Object);
var command = new RunCommand(_mockInteractiveService.Object, _mockEnvironmentManager.Object);
var context = new CommandContext(new List<string>(), _mockRemainingArgs.Object, "run", null);
var apiUrl = $"http://{settings.Host}:{settings.ApiGatewayEmulatorPort}/__lambda_test_tool_apigateway_health__";

Expand All @@ -62,4 +65,33 @@ public async Task ExecuteAsync_ApiGatewayEmulator_SuccessfulLaunch()
Assert.Equal(CommandReturnCodes.Success, result);
Assert.True(isApiRunning);
}

[Fact]
public async Task ExecuteAsync_EnvPorts_SuccessfulLaunch()
{
var environmentManager = new LocalEnvironmentManager(new Dictionary<string, string>
{
{ RunCommand.LAMBDA_RUNTIME_API_PORT, "9432" },
{ RunCommand.API_GATEWAY_EMULATOR_PORT, "9765" }
});

// Arrange
Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development");
var cancellationSource = new CancellationTokenSource();
cancellationSource.CancelAfter(5000);
var settings = new RunCommandSettings { ApiGatewayEmulatorMode = ApiGatewayEmulatorMode.HttpV2, NoLaunchWindow = true };
var command = new RunCommand(_mockInteractiveService.Object, environmentManager);
var context = new CommandContext(new List<string>(), _mockRemainingArgs.Object, "run", null);
var apiUrl = $"http://{settings.Host}:9765/__lambda_test_tool_apigateway_health__";

// Act
var runningTask = command.ExecuteAsync(context, settings, cancellationSource);
var isApiRunning = await TestHelpers.WaitForApiToStartAsync(apiUrl);
await cancellationSource.CancelAsync();

// Assert
var result = await runningTask;
Assert.Equal(CommandReturnCodes.Success, result);
Assert.True(isApiRunning);
}
}
Loading