-
Notifications
You must be signed in to change notification settings - Fork 478
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
Invoke Lambda via Http API inside Lambda Test Tool #1349
base: dev
Are you sure you want to change the base?
Changes from 3 commits
65e3947
4055ce3
e4ebe1a
9e2ab7e
6add92d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,239 @@ | ||
using System; | ||
using System.IO; | ||
using System.Linq; | ||
using System.Net; | ||
using System.Text; | ||
using System.Text.Json; | ||
using System.Threading.Tasks; | ||
using Amazon.Lambda.Core; | ||
using Amazon.Lambda.TestTool.Runtime; | ||
using Amazon.Lambda.TestTool.Runtime.LambdaMocks; | ||
using Microsoft.AspNetCore.Http; | ||
using Microsoft.AspNetCore.Mvc; | ||
|
||
namespace Amazon.Lambda.TestTool.BlazorTester.Controllers | ||
{ | ||
[Route("[controller]")] | ||
public class InvokeApiController : ControllerBase | ||
{ | ||
private readonly LocalLambdaOptions _lambdaOptions; | ||
private LambdaConfigInfo _lambdaConfig; | ||
|
||
public InvokeApiController(LocalLambdaOptions lambdaOptions) | ||
{ | ||
_lambdaOptions = lambdaOptions; | ||
} | ||
|
||
[HttpPost("execute")] | ||
public async Task<IActionResult> ExecuteFunction() | ||
{ | ||
if (!TryGetConfigFile(out var lambdaConfig)) | ||
{ | ||
return StatusCode((int)HttpStatusCode.InternalServerError, | ||
new InternalException("ServiceException", "Error while loading function configuration")); | ||
} | ||
|
||
if (lambdaConfig.FunctionInfos.Count == 0) | ||
{ | ||
return NotFound(new InternalException("ResourceNotFoundException", "Default function not found")); | ||
} | ||
|
||
return Ok(await ExecuteFunctionInternal(lambdaConfig, lambdaConfig.FunctionInfos[0])); | ||
} | ||
|
||
[HttpPost("execute/{functionName}")] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we just have the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See my comments |
||
[HttpPost("2015-03-31/functions/{functionName}/invocations")] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The goal I assume is not just be able to make REST calls but to actually use an AWS SDK pointing at this instance. That is a cool feature but we will need to add some tests for this so we know we don't branch that emulation. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the ability to call through a simple |
||
public async Task<object> ExecuteFunction(string functionName) | ||
{ | ||
if (!TryGetConfigFile(out var lambdaConfig)) | ||
{ | ||
return StatusCode((int)HttpStatusCode.InternalServerError, | ||
new InternalException("ServiceException", "Error while loading function configuration")); | ||
} | ||
|
||
var functionInfo = lambdaConfig.FunctionInfos.FirstOrDefault(f => f.Name == functionName); | ||
if (functionInfo == null) | ||
{ | ||
return NotFound(new InternalException("ResourceNotFoundException", | ||
$"Function not found: {functionName}")); | ||
} | ||
|
||
return Ok(await ExecuteFunctionInternal(lambdaConfig, functionInfo)); | ||
} | ||
|
||
private bool TryGetConfigFile(out LambdaConfigInfo configInfo) | ||
{ | ||
configInfo = null; | ||
|
||
if (_lambdaConfig != null) | ||
{ | ||
configInfo = _lambdaConfig; | ||
return true; | ||
} | ||
|
||
if (_lambdaOptions.LambdaConfigFiles.Count == 0) | ||
{ | ||
Console.Error.WriteLine("LambdaConfigFiles list is empty"); | ||
return false; | ||
} | ||
|
||
var configPath = _lambdaOptions.LambdaConfigFiles[0]; | ||
try | ||
{ | ||
configInfo = LambdaDefaultsConfigFileParser.LoadFromFile(configPath); | ||
_lambdaConfig = configInfo; | ||
} | ||
catch (Exception e) | ||
{ | ||
Console.Error.WriteLine("Error loading lambda config from '{0}'", configPath); | ||
Console.Error.WriteLine(e.ToString()); | ||
} | ||
|
||
return true; | ||
} | ||
|
||
private async Task<object> ExecuteFunctionInternal(LambdaConfigInfo lambdaConfig, | ||
LambdaFunctionInfo functionInfo) | ||
{ | ||
var requestReader = new LambdaRequestReader(Request); | ||
var function = _lambdaOptions.LoadLambdaFuntion(lambdaConfig, functionInfo.Handler); | ||
|
||
var request = new ExecutionRequest | ||
{ | ||
Function = function, | ||
AWSProfile = lambdaConfig.AWSProfile, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need some way on either a per invoke or per session users can configure the profile and region. This feature won't work for users that don't want profiles and regions in their config file. I think it is fine using the config as default the. Also we might want to use the SDK's credential and region fallback chain if neither the config or a specific profile and region are not specified. Also can we add a log message which profile and region were used? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In LambdaExecutor these config parameters are passed to env variables if they are set. If you don't fill it in the config file, you can pass these parameters directly via environment variables when you start debugging. What we actively use, for example, through At what level do you want to see |
||
AWSRegion = lambdaConfig.AWSRegion, | ||
Payload = await requestReader.ReadPayload(), | ||
ClientContext = requestReader.ReadClientContext() | ||
}; | ||
|
||
var response = await _lambdaOptions.LambdaRuntime.ExecuteLambdaFunctionAsync(request); | ||
var responseWriter = new LambdaResponseWriter(Response); | ||
|
||
if (requestReader.ReadLogType() == "Tail") | ||
{ | ||
responseWriter.WriteLogs(response.Logs); | ||
} | ||
|
||
if (!response.IsSuccess) | ||
{ | ||
responseWriter.WriteError(); | ||
return new LambdaException(response.Error); | ||
} | ||
|
||
return response.Response; | ||
} | ||
|
||
private class LambdaRequestReader | ||
{ | ||
private const string LogTypeHeader = "X-Amz-Log-Type"; | ||
private const string ClientContextHeader = "X-Amz-Client-Context"; | ||
|
||
private readonly HttpRequest _request; | ||
|
||
public LambdaRequestReader(HttpRequest request) | ||
{ | ||
_request = request; | ||
} | ||
|
||
public async Task<string> ReadPayload() | ||
{ | ||
using var reader = new StreamReader(_request.Body); | ||
return await reader.ReadToEndAsync(); | ||
} | ||
|
||
public string ReadLogType() | ||
{ | ||
return _request.Headers.TryGetValue(LogTypeHeader, out var value) | ||
? value.ToString() | ||
: string.Empty; | ||
} | ||
|
||
public IClientContext ReadClientContext() | ||
{ | ||
if (!_request.Headers.TryGetValue(ClientContextHeader, out var contextString)) | ||
{ | ||
return null; | ||
} | ||
|
||
var clientContext = JsonSerializer.Deserialize<LocalClientContext>( | ||
Convert.FromBase64String(contextString), | ||
new JsonSerializerOptions | ||
{ | ||
PropertyNameCaseInsensitive = true | ||
}); | ||
|
||
return clientContext; | ||
} | ||
} | ||
|
||
private class LambdaResponseWriter | ||
{ | ||
private const string FunctionErrorHeader = "X-Amz-Function-Error"; | ||
private const string LogResultHeader = "X-Amz-Log-Result"; | ||
|
||
private readonly HttpResponse _response; | ||
|
||
public LambdaResponseWriter(HttpResponse response) | ||
{ | ||
_response = response; | ||
} | ||
|
||
public void WriteError() | ||
{ | ||
_response.Headers[FunctionErrorHeader] = "Unhandled"; | ||
} | ||
|
||
public void WriteLogs(string logs) | ||
{ | ||
_response.Headers[LogResultHeader] = Convert.ToBase64String(Encoding.UTF8.GetBytes(logs)); | ||
} | ||
} | ||
|
||
private class InternalException | ||
{ | ||
public string ErrorCode { get; } | ||
|
||
public string ErrorMessage { get; } | ||
|
||
public InternalException(string errorCode, string errorMessage) | ||
{ | ||
ErrorCode = errorCode; | ||
ErrorMessage = errorMessage; | ||
} | ||
} | ||
|
||
private class LambdaException | ||
{ | ||
public string ErrorType { get; } | ||
|
||
public string ErrorMessage { get; } | ||
|
||
public string[] StackTrace { get; } | ||
|
||
public LambdaException(string error) | ||
{ | ||
var errorLines = error.Split('\n', StringSplitOptions.RemoveEmptyEntries); | ||
if (errorLines.Length == 0) | ||
{ | ||
StackTrace = Array.Empty<string>(); | ||
return; | ||
} | ||
|
||
StackTrace = errorLines.Skip(1).Select(s => s.Trim()).ToArray(); | ||
|
||
var errorMessage = errorLines[0]; | ||
var errorTypeDelimiterPos = errorMessage.IndexOf(':'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you re-write this as:
to improve readability? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agree with you, fixed it. |
||
if (errorTypeDelimiterPos > 0) | ||
{ | ||
ErrorType = errorMessage.Substring(0, errorTypeDelimiterPos).Trim(); | ||
ErrorMessage = errorMessage.Substring(errorTypeDelimiterPos + 1).Trim(); | ||
} | ||
else | ||
{ | ||
ErrorMessage = errorMessage; | ||
} | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
using System.Collections.Generic; | ||
using Amazon.Lambda.Core; | ||
|
||
namespace Amazon.Lambda.TestTool.Runtime.LambdaMocks | ||
{ | ||
public class LocalClientContext: IClientContext | ||
{ | ||
public IDictionary<string, string> Environment { get; set; } | ||
|
||
public IClientApplication Client { get; set; } | ||
|
||
public IDictionary<string, string> Custom { get; set; } | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the purpose of this execute? Makes me nervous that running the first function info we find. I would rather always be specific.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was convenient due to the fact that it was not necessary to know the name of the function on the client side. We store each function in separate C# projects (one configuration file per project). So you only need to know
port
on which TestTool listens.Maybe you are right, it's no clear. I need to look at our client side code and think about it