using Amazon.IdentityManagement;
using Amazon.IdentityManagement.Model;
using Amazon.Lambda.Model;
using Amazon.S3;
using Amazon.S3.Model;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Amazon.Lambda.RuntimeSupport.IntegrationTests
{
public class BaseCustomRuntimeTest
{
protected static readonly RegionEndpoint TestRegion = RegionEndpoint.USWest2;
protected static readonly string LAMBDA_ASSUME_ROLE_POLICY =
@"
{
""Version"": ""2012-10-17"",
""Statement"": [
{
""Sid"": """",
""Effect"": ""Allow"",
""Principal"": {
""Service"": ""lambda.amazonaws.com""
},
""Action"": ""sts:AssumeRole""
}
]
}
".Trim();
protected string Handler { get; }
protected string FunctionName { get; }
protected string DeploymentZipKey { get; }
protected string DeploymentPackageZipRelativePath { get; }
protected string TestBucketRoot { get; } = "customruntimetest-";
protected string ExecutionRoleName { get; }
protected string ExecutionRoleArn { get; set; }
private const string TestsProjectDirectoryName = "Amazon.Lambda.RuntimeSupport.Tests";
protected BaseCustomRuntimeTest(string functionName, string deploymentZipKey, string deploymentPackageZipRelativePath, string handler)
{
FunctionName = functionName;
ExecutionRoleName = FunctionName;
Handler = handler;
DeploymentZipKey = deploymentZipKey;
DeploymentPackageZipRelativePath = deploymentPackageZipRelativePath;
}
///
/// Clean up all test resources.
/// Also cleans up any resources that might be left from previous failed/interrupted tests.
///
///
///
///
protected async Task CleanUpTestResources(AmazonS3Client s3Client, AmazonLambdaClient lambdaClient,
AmazonIdentityManagementServiceClient iamClient, bool roleAlreadyExisted)
{
await DeleteFunctionIfExistsAsync(lambdaClient);
var listBucketsResponse = await s3Client.ListBucketsAsync();
foreach (var bucket in listBucketsResponse.Buckets)
{
if (bucket.BucketName.StartsWith(TestBucketRoot))
{
await DeleteDeploymentZipAndBucketAsync(s3Client, bucket.BucketName);
}
}
if (!roleAlreadyExisted)
{
try
{
var deleteRoleRequest = new DeleteRoleRequest
{
RoleName = ExecutionRoleName
};
await iamClient.DeleteRoleAsync(deleteRoleRequest);
}
catch (Exception)
{
// no problem - it's best effort
}
}
}
protected async Task PrepareTestResources(IAmazonS3 s3Client, IAmazonLambda lambdaClient,
AmazonIdentityManagementServiceClient iamClient)
{
var roleAlreadyExisted = await ValidateAndSetIamRoleArn(iamClient);
var testBucketName = TestBucketRoot + Guid.NewGuid().ToString();
await CreateBucketWithDeploymentZipAsync(s3Client, testBucketName);
await CreateFunctionAsync(lambdaClient, testBucketName);
return roleAlreadyExisted;
}
///
/// Create the role if it's not there already.
/// Return true if it already existed.
///
///
private async Task ValidateAndSetIamRoleArn(IAmazonIdentityManagementService iamClient)
{
var getRoleRequest = new GetRoleRequest
{
RoleName = ExecutionRoleName
};
try
{
ExecutionRoleArn = (await iamClient.GetRoleAsync(getRoleRequest)).Role.Arn;
return true;
}
catch (NoSuchEntityException)
{
// create the role
var createRoleRequest = new CreateRoleRequest
{
RoleName = ExecutionRoleName,
Description = "Test role for CustomRuntimeTests.",
AssumeRolePolicyDocument = LAMBDA_ASSUME_ROLE_POLICY
};
ExecutionRoleArn = (await iamClient.CreateRoleAsync(createRoleRequest)).Role.Arn;
// Wait for role to propagate.
await Task.Delay(10000);
await iamClient.AttachRolePolicyAsync(new AttachRolePolicyRequest
{
PolicyArn = "arn:aws:iam::aws:policy/AWSLambdaExecute",
RoleName = ExecutionRoleName,
});
return false;
}
}
private async Task CreateBucketWithDeploymentZipAsync(IAmazonS3 s3Client, string bucketName)
{
// create bucket if it doesn't exist
var listBucketsResponse = await s3Client.ListBucketsAsync();
if (listBucketsResponse.Buckets.Find((bucket) => bucket.BucketName == bucketName) == null)
{
var putBucketRequest = new PutBucketRequest
{
BucketName = bucketName
};
await s3Client.PutBucketAsync(putBucketRequest);
await Task.Delay(10000);
}
// write or overwrite deployment package
var putObjectRequest = new PutObjectRequest
{
BucketName = bucketName,
Key = DeploymentZipKey,
FilePath = GetDeploymentZipPath()
};
await s3Client.PutObjectAsync(putObjectRequest);
// Wait for bucket to propagate.
await Task.Delay(5000);
}
private async Task DeleteDeploymentZipAndBucketAsync(IAmazonS3 s3Client, string bucketName)
{
try
{
await Amazon.S3.Util.AmazonS3Util.DeleteS3BucketWithObjectsAsync(s3Client, bucketName);
}
catch (AmazonS3Exception e)
{
// If it's just telling us the bucket's not there then continue, otherwise throw.
if (!e.Message.Contains("The specified bucket does not exist"))
{
throw;
}
}
}
protected async Task InvokeFunctionAsync(IAmazonLambda lambdaClient, string payload)
{
var request = new InvokeRequest
{
FunctionName = FunctionName,
Payload = payload,
LogType = LogType.Tail
};
return await lambdaClient.InvokeAsync(request);
}
protected async Task UpdateHandlerAsync(IAmazonLambda lambdaClient, string handler, Dictionary environmentVariables = null)
{
if(environmentVariables == null)
{
environmentVariables = new Dictionary();
}
environmentVariables["TEST_HANDLER"] = handler;
var updateFunctionConfigurationRequest = new UpdateFunctionConfigurationRequest
{
FunctionName = FunctionName,
Environment = new Model.Environment
{
IsVariablesSet = true,
Variables = environmentVariables
}
};
await lambdaClient.UpdateFunctionConfigurationAsync(updateFunctionConfigurationRequest);
// Wait for eventual consistency of function change.
var getConfigurationRequest = new GetFunctionConfigurationRequest { FunctionName = FunctionName };
GetFunctionConfigurationResponse getConfigurationResponse = null;
do
{
await Task.Delay(1000);
getConfigurationResponse = await lambdaClient.GetFunctionConfigurationAsync(getConfigurationRequest);
} while (getConfigurationResponse.State == State.Pending);
await Task.Delay(1000);
}
protected async Task CreateFunctionAsync(IAmazonLambda lambdaClient, string bucketName)
{
await DeleteFunctionIfExistsAsync(lambdaClient);
var createRequest = new CreateFunctionRequest
{
FunctionName = FunctionName,
Code = new FunctionCode
{
S3Bucket = bucketName,
S3Key = DeploymentZipKey
},
Handler = this.Handler,
MemorySize = 512,
Timeout = 30,
Runtime = Runtime.Dotnet6,
Role = ExecutionRoleArn
};
var startTime = DateTime.Now;
var created = false;
while (DateTime.Now < startTime.AddSeconds(30))
{
try
{
await lambdaClient.CreateFunctionAsync(createRequest);
created = true;
break;
}
catch (InvalidParameterValueException ipve)
{
// Wait for the role to be fully propagated through AWS
if (ipve.Message == "The role defined for the function cannot be assumed by Lambda.")
{
await Task.Delay(2000);
}
else
{
throw;
}
}
}
await Task.Delay(5000);
if (!created)
{
throw new Exception($"Timed out trying to create Lambda function {FunctionName}");
}
}
protected async Task DeleteFunctionIfExistsAsync(IAmazonLambda lambdaClient)
{
var request = new DeleteFunctionRequest
{
FunctionName = FunctionName
};
try
{
var response = await lambdaClient.DeleteFunctionAsync(request);
}
catch (ResourceNotFoundException)
{
// no problem
}
}
///
/// Get the path of the deployment package for testing the custom runtime.
/// This assumes that the 'dotnet lambda package -c Release' command was run as part of the pre-build of this csproj.
///
///
private string GetDeploymentZipPath()
{
var testsProjectDirectory = FindUp(System.Environment.CurrentDirectory, TestsProjectDirectoryName, true);
if (string.IsNullOrEmpty(testsProjectDirectory))
{
throw new NoDeploymentPackageFoundException();
}
var deploymentZipFile = Path.Combine(testsProjectDirectory, DeploymentPackageZipRelativePath.Replace('\\', Path.DirectorySeparatorChar));
if (!File.Exists(deploymentZipFile))
{
throw new NoDeploymentPackageFoundException();
}
return deploymentZipFile;
}
private static string FindUp(string path, string fileOrDirectoryName, bool combine)
{
var fullPath = Path.Combine(path, fileOrDirectoryName);
if (File.Exists(fullPath) || Directory.Exists(fullPath))
{
return combine ? fullPath : path;
}
else
{
var upDirectory = Path.GetDirectoryName(path);
if (string.IsNullOrEmpty(upDirectory))
{
return null;
}
else
{
return FindUp(upDirectory, fileOrDirectoryName, combine);
}
}
}
protected class NoDeploymentPackageFoundException : Exception
{
}
}
}