All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.limemojito.test.lambda.LambdaSupport Maven / Gradle / Ivy

Go to download

Test utilities for various development work. Json, reflection, getter/setter testing, DTO, Canonical form, etc. AWS support for Dynamo DB, SQS, SNS, S3. Prometheus metrics reader and asserter. Synthetic S3 Event generation.

There is a newer version: 14.3.1
Show newest version
/*
 * Copyright 2011-2024 Lime Mojito Pty Ltd
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 *
 */

package com.limemojito.test.lambda;

import com.fasterxml.jackson.core.type.TypeReference;
import com.limemojito.test.jackson.JacksonSupport;
import com.limemojito.test.s3.S3Support;
import jakarta.annotation.PreDestroy;
import jakarta.validation.constraints.Min;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.awaitility.Awaitility;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.function.adapter.aws.FunctionInvoker;
import org.springframework.stereotype.Service;
import software.amazon.awssdk.core.SdkBytes;
import software.amazon.awssdk.services.lambda.LambdaClient;
import software.amazon.awssdk.services.lambda.model.*;
import software.amazon.awssdk.utils.IoUtils;
import software.amazon.awssdk.utils.StringUtils;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URI;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.awaitility.Durations.FIVE_HUNDRED_MILLISECONDS;
import static software.amazon.awssdk.services.lambda.model.Runtime.JAVA17;
import static software.amazon.awssdk.services.lambda.model.Runtime.NODEJS20_X;

/**
 * 

Be aware that creating lambdas on localstack uses containers on the underlying docker infrastructure. * Use cleanup() to clean any left over lambdas after deployments. You can do this in a test in an @AfterEach, * etc.

* * *

We setup the _JAVA_OPTIONS here to nudge localstack into debug mode. We add the standard lambda * JAVA_TOOL_OPTIONS as well if set by the user. If not set we stop compilation at interpreted only to reduce * cold start. Production will use SnapStart and different settings.

* * @see #cleanup() */ @Service @Slf4j public class LambdaSupport { private static final int TWO_KB = 2048; private static final int MB = 1024 * 1024; private static final int NO_DEBUG = -1; /** * Raw java execution timout seconds (180). This gives 3 minutes to covers non SnapStart deployments. */ public static final int JAVA_EXECUTION_TIMEOUT = 180; /** * Raw javaDebug execution timout seconds (900). This gives 15 minutes to debug in localstack before a retry. * * @see #javaDebug(String, String, Map) */ public static final int JAVA_DEBUG_EXECUTION_TIMEOUT = 900; /** * Default localstack lambda role (stubbed IAM) */ public static final String LAMBDA_ROLE = "arn:aws:iam::000000000000:role/lambda-role"; /** * Default port for debugging. */ public static final int DEFAULT_DEBUG_PORT = 5050; /** * Default java memory size. */ public static final int DEFAULT_JAVA_MEMORY_MEGABYTES = 1024; /** * Default stub (javascript) memory size. */ public static final int STUB_MEMORY = 256 * MB; /** * Default debug VM setup options */ public static final String DEBUG_OPS = "-Xshare:off -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=0.0.0.0:%d"; /** * Name of default handler from spring-cloud-function. */ public static final String SPRING_CLOUD_FUNCTION_HANDLER = FunctionInvoker.class.getName(); /** * Environment variable to set to override repository location when loading jars for lambdas using maven * co-ordinates. */ public static final String LIME_MAVEN_REPOSITORY = "LIME_MAVEN_REPOSITORY"; /** * As used in the maven base pom /jar-lambda-development/pom.xml */ private static final String LAMBDA_JAR_CLASSIFIER = "aws"; private static final String FUNCTION_DEF_KEY = "SPRING_CLOUD_FUNCTION_DEFINITION"; private final LambdaClient lambdaClient; private final S3Support s3; private final JacksonSupport json; private final int deployTimeoutSeconds; private final Set tracker; private final Set mappingTracker; /** * The Lambda function is identified by its name and Amazon Resource Name (ARN). * This class serves as a convenient way to handle and manipulate Lambda function details. */ public record Lambda(String name, String arn) { } /** * Initializes the LambdaSupport object with the provided dependencies and deploy timeout. * * @param lambdaClient The LambdaClient instance used for interacting with AWS Lambda. * @param s3 The S3Support instance used for interacting with AWS S3. * @param json The JacksonSupport instance used for JSON processing. * @param deployTimeoutSeconds The deploy timeout in seconds. Defaults to 180 if not specified. */ public LambdaSupport(LambdaClient lambdaClient, S3Support s3, JacksonSupport json, @Value("${lambda.support.deploy.timeoutSeconds:180}") int deployTimeoutSeconds) { this.lambdaClient = lambdaClient; this.s3 = s3; this.json = json; this.deployTimeoutSeconds = deployTimeoutSeconds; this.tracker = new HashSet<>(); this.mappingTracker = new HashSet<>(); log.info("Deploy timeout at {} seconds", deployTimeoutSeconds); } /** * Invoke for lambda events that use custom json parsing from AWS. * * @param lambda lambda to call * @param lambdaEvent event to convert * @param responseType event to convert to (assume Lambda Event) * @param Type to expect for response. * @return instance of the return type from the lambda. * @see com.amazonaws.services.lambda.runtime.serialization.events.LambdaEventSerializers */ public T invokeLambdaEvent(Lambda lambda, Object lambdaEvent, Class responseType) { String jsonRequest = json.toJsonLambdaEvent(lambdaEvent); String responseJson = invokeAllowFail(lambda, jsonRequest); return json.parseLambdaEvent(responseJson, responseType); } /** * Invokes a Lambda function asynchronously with the specified input and returns the response. * * @param lambda the Lambda function to invoke * @param pojo the input object to be converted to JSON * @param responseType the class representing the type of the response object * @param the type of the response object * @return the response object parsed from the JSON string */ public T invoke(Lambda lambda, Object pojo, Class responseType) { String jsonString = json.toJson(pojo); String responseJson = invokeAllowFail(lambda, jsonString); return json.parse(responseJson, responseType); } /** * Invokes the specified lambda function with the given POJO object as input * and returns the response converted to the specified response object type. * * @param lambda the lambda function to invoke * @param pojo the POJO object to pass as input to the lambda function * @param responseType the type of the response object to convert the response to * @param the type of the response object * @return the response object converted to the specified type */ public T invoke(Lambda lambda, Object pojo, TypeReference responseType) { String jsonString = json.toJson(pojo); String responseJson = invokeAllowFail(lambda, jsonString); return json.parse(responseJson, responseType); } /** * Invoke with pure json string request and response. * * @param lambda Lambda to invoke * @param jsonString the input string * @return the output string */ public String invokeAllowFail(Lambda lambda, String jsonString) { log.debug("Invoking {} with json {}", lambda.arn, jsonString); InvokeResponse response = lambdaClient.invoke(r -> r.functionName(lambda.name) .payload(SdkBytes.fromUtf8String(jsonString))); String responseJson = response.payload().asUtf8String(); log.debug("Response: {}", responseJson); String errorMessage = response.functionError(); if (errorMessage != null) { throw new RuntimeException("Lambda %s failed with %s".formatted(lambda, errorMessage)); } return responseJson; } /** * Creates a Lambda function object using the provided function name by retrieving data from the Lambda runtime. * * @param functionName the name of the Lambda function * @return a Lambda object representing the function */ public Lambda forName(String functionName) { return new Lambda(functionName, describe(functionName).configuration().functionArn()); } /** * Creates a lambda stub with the given name and response data. * The response data is serialized to JSON format using the provided JSON library. * The created lambda stub can be used in integration tests. * * @param name The name of the lambda stub. * @param responsePojo The response data object to be serialized to JSON format. * @return A lambda stub with the given name and serialized response data. */ @SneakyThrows public Lambda stub(String name, Object responsePojo) { return stub(name, json.toJson(responsePojo)); } /** * Creates a lambda stub with the given name and response data. * The response data is serialized to JSON format using the provided JSON library. * The created lambda stub can be used in integration tests. * * @param name The name of the lambda stub. * @param response The response JSON. * @return A lambda stub with the given name and response data. */ public Lambda stub(String name, String response) { return deployStub(name, response, false); } /** * Creates a lambda stub with the given name and response data as a failure event. * The response data is serialized to JSON format using the provided JSON library. * The created lambda stub can be used in integration tests. * * @param name The name of the lambda stub. * @param responsePojo The response data object to be serialized to JSON format. * @return A lambda stub with the given name and response data as a fail event. */ @SneakyThrows public Lambda stubFail(String name, Object responsePojo) { return stubFail(name, json.toJson(responsePojo)); } /** * Creates a lambda stub with the given name and response data as a failure event. * The response data is serialized to JSON format using the provided JSON library. * The created lambda stub can be used in integration tests. * * @param name The name of the lambda stub. * @param response The response JSON. * @return A lambda stub with the given name and response JSON as a fail event. */ public Lambda stubFail(String name, String response) { return deployStub(name, response, true); } /** * Deploys Java lambda with sane defaults such as 1GB Memory. To debug use the deprecated javaDebug method but * do not commit this code as it will pause the deployed lambda until a debug connection is made. * * @param moduleLocation Maven module directory to deploy (relative path to this module). * @param handler Java class name fully qualified as the Lambda Stream Handler * @param environment Map of environment variables to deploy. May be decorated with more. * @return the deployed lambda. * @see #SPRING_CLOUD_FUNCTION_HANDLER */ public Lambda java(String moduleLocation, String handler, Map environment) { return java(moduleLocation, handler, DEFAULT_JAVA_MEMORY_MEGABYTES, environment); } /** * Deploys Java lambda with sane defaults. To debug use the deprecated javaDebug method but * do not commit this code as it will pause the deployed lambda until a debug connection is made. * *

AWS Lambda vCPU is proportional to memory. Though for execution time consider that it is normally * single threaded. AWS keeps the number of CPUs vs Memory ranking internal.

* * @param moduleLocation Maven module directory to deploy (relative path to this module). * @param handler Java class name fully qualified as the Lambda Stream Handler * @param memoryMegabytes Number of MB to allocate to lambda. AWS vCPU is proportional to memory. * @param environment Map of environment variables to deploy. May be decorated with more. * @return the deployed lambda. * @see #SPRING_CLOUD_FUNCTION_HANDLER */ public Lambda java(String moduleLocation, String handler, @Min(DEFAULT_JAVA_MEMORY_MEGABYTES) int memoryMegabytes, Map environment) { return deployJavaFromSourceBase(moduleLocation, handler, memoryMegabytes, NO_DEBUG, environment); } /** * Deploys Java lambda with sane defaults. To debug use the deprecated javaDebug method but * do not commit this code as it will pause the deployed lambda until a debug connection is made. This method * finds jars in your local maven repository directory paying respect to the environment variable * LIME_MAVEN_REPOSITORY or defaulting to ~/.m2 if this is not present. *

* Add required jars to your test scope as dependencies for maven to download to your local repo before running * tests with localstack. *

* AWS Lambda vCPU is proportional to memory. Though for execution time consider that it is normally * single threaded. AWS keeps the number of CPUs vs Memory ranking internal. * * @param groupId Maven group id to find. * @param artifactId Maven artifactId to find. * @param version Maven artifact version. * @param classifier Maven artifact classifier. Use blank for default. * @param handler Java class name fully qualified as the Lambda Stream Handler * @param memoryMegabytes Number of MB to allocate to lambda. AWS vCPU is proportional to memory. * @param environment Map of environment variables to deploy. May be decorated with more. * @return the deployed lambda. * @see #SPRING_CLOUD_FUNCTION_HANDLER */ public Lambda java(String groupId, String artifactId, String version, String classifier, String handler, @Min(DEFAULT_JAVA_MEMORY_MEGABYTES) int memoryMegabytes, Map environment) { return deployJava(artifactId, findMavenJarFromLocalRepo(groupId, artifactId, version, classifier), handler, memoryMegabytes, NO_DEBUG, environment); } /** * Debug should not be called directly as we pause for server connections by default * * @param moduleLocation Maven module directory to deploy (relative path to this module). * @param handler Java class name fully qualified as the Lambda Stream Handler * @param environment Map of environment variables to deploy. May be decorated with more. * @return the deployed lambda. * @see #SPRING_CLOUD_FUNCTION_HANDLER * @see #java(String, String, Map) * @deprecated DO NOT USE IN COMMITTED CODE AS THIS STOPS THE VM UNTIL DEBUG CONNECTION. */ @Deprecated public Lambda javaDebug(String moduleLocation, String handler, Map environment) { return javaDebug(moduleLocation, handler, DEFAULT_JAVA_MEMORY_MEGABYTES, environment, DEFAULT_DEBUG_PORT); } /** * Debug should not be called directly as we pause for server connections by default. Refer to readme.md. * * @param moduleLocation Maven module directory to deploy (relative path to this module). * @param handler Java class name fully qualified as the Lambda Stream Handler * @param memoryMegabytes Number of MB to allocate to lambda. AWS vCPU is proportional to memory. * @param environment Map of environment variables to deploy. May be decorated with more. * @param debugPort port for debugging to occur on (when > 0). Must align with docker-compose.yml file. * @return the deployed lambda. * @see #SPRING_CLOUD_FUNCTION_HANDLER * @see #DEFAULT_DEBUG_PORT * @see #java(String, String, String, String, String, int, Map) * @deprecated DO NOT USE IN COMMITTED CODE AS THIS STOPS THE VM UNTIL DEBUG CONNECTION. */ @Deprecated public Lambda javaDebug(String moduleLocation, String handler, @Min(DEFAULT_JAVA_MEMORY_MEGABYTES) int memoryMegabytes, Map environment, @Min(1) int debugPort) { return deployJavaFromSourceBase(moduleLocation, handler, memoryMegabytes, debugPort, environment); } /** * Debug should not be called directly as we pause for server connections by default. Refer to readme.md. * * @param groupId Maven group id to find. * @param artifactId Maven artifactId to find. * @param version Maven artifact version. * @param classifier Maven artifact classifier. Use blank for default. * @param handler Java class name fully qualified as the Lambda Stream Handler * @param memoryMegabytes Number of MB to allocate to lambda. AWS vCPU is proportional to memory. * @param environment Map of environment variables to deploy. May be decorated with more. * @param debugPort port for debugging to occur on (when > 0). Must align with docker-compose.yml file. * @return the deployed lambda. * @see #SPRING_CLOUD_FUNCTION_HANDLER * @see #DEFAULT_DEBUG_PORT * @see #java(String, String, String, String, String, int, Map) * @deprecated DO NOT USE IN COMMITTED CODE AS THIS STOPS THE VM UNTIL DEBUG CONNECTION. */ @Deprecated @SuppressWarnings("ParameterNumber") public Lambda javaDebug(String groupId, String artifactId, String version, String classifier, String handler, @Min(DEFAULT_JAVA_MEMORY_MEGABYTES) int memoryMegabytes, Map environment, @Min(1) int debugPort) { return deployJava(artifactId, findMavenJarFromLocalRepo(groupId, artifactId, version, classifier), handler, memoryMegabytes, debugPort, environment); } /** * Cleans up the resources used by the deployed lambdas. *

* This method is annotated with the `@PreDestroy` annotation, indicating that it is executed * before the object is destroyed. *

* The cleanup process involves the following steps: *

* 1. Logs a warning message, indicating the number of deployed lambdas to be cleaned up. * 2. Iterates over the `mappingTracker` collection and invokes the `safeDeleteMapping` method * for each entry. * 3. Clears the `mappingTracker` collection. * 4. Iterates over the `tracker` collection and invokes the `safeDelete` method for each entry. * 5. Clears the `tracker` collection. *

* Please note that this method does not return any value. * * @see PreDestroy */ @PreDestroy public void cleanup() { log.warn("Cleaning up deployed lambdas (container dregs) {} lambdas", tracker.size()); mappingTracker.forEach(this::safeDeleteMapping); mappingTracker.clear(); tracker.forEach(this::safeDelete); tracker.clear(); } /** * Maps an event source to a lambda using configuration defaults. Note JSON should be loaded and consumed * using xxxLambdaEvent methods of JsonSupport. * * @param sourceArn ARN of aws service generating events. * @param destination Lambda that is the destination of events. * @see JacksonSupport#loadLambdaEvent(String, Class) */ public void addEventSourceTo(String sourceArn, Lambda destination) { CreateEventSourceMappingResponse mapping = lambdaClient.createEventSourceMapping(r -> r.functionName(destination.name()) .eventSourceArn(sourceArn) .enabled(true)); mappingTracker.add(mapping.uuid()); } /** * Waits for lambda state using the deployment timeout seconds (default 300). Overridden * on construction of LambdaSupport using spring property lambda.support.deploy.timeoutSeconds * * @param lambda lambda to wait for * @param state state to wait for */ public void waitForState(Lambda lambda, State state) { waitForState(lambda, deployTimeoutSeconds, state); } /** * Waits for lambda state using the deployment timeout seconds (default 300). Overridden * on construction of LambdaSupport using spring property lambda.support.deploy.timeoutSeconds * * @param lambda lambda to wait for * @param maxSeconds Maximum seconds to wait. * @param state state to wait for */ public void waitForState(Lambda lambda, int maxSeconds, State state) { Awaitility.waitAtMost(maxSeconds, TimeUnit.SECONDS) .pollInterval(FIVE_HUNDRED_MILLISECONDS) .alias("%s did not reach state %s".formatted(lambda.name, state)) .until(() -> checkFailed(lambda, state)); } private Lambda deployJavaFromSourceBase(String moduleLocation, String handler, int memoryMegabytes, int debugPort, Map environment) { final String artifactId = moduleLocation.substring(moduleLocation.lastIndexOf('/') + 1); return deployJava(artifactId, () -> findJarForModule(moduleLocation), handler, memoryMegabytes, debugPort, environment ); } private Lambda deployJava(String artifactId, Supplier getCodeJar, String handler, int memoryMegabytes, int debugPort, Map environment) { final byte[] awsJar = getCodeJar.get(); final String deployBucket = "lambda-deploy"; final String name = lambdaName(artifactId, environment); final String key = "%s.jar".formatted(name); final URI s3Uri = s3.toS3Uri(deployBucket, key); log.info("Uploading code to {}", s3Uri); s3.createBucket(deployBucket); s3.putData(s3Uri, "application/zip", awsJar); final Integer timeout = isInDebugMode(debugPort) ? JAVA_DEBUG_EXECUTION_TIMEOUT : JAVA_EXECUTION_TIMEOUT; log.info(""" Deploying Java AWS function {} with \ttimeout: {} seconds \thandler {} \tenv {}""", name, timeout, handler, environment); final String desc = "Deployment of %s from artifactId %s with timeout %d (s)".formatted(name, artifactId, timeout); return deploy(name, r -> r.functionName(name) .description(desc) .memorySize(memoryMegabytes * MB) .handler(handler) .runtime(JAVA17) .environment(e -> e.variables(spyEnvironment(environment, debugPort, memoryMegabytes))) .code(c -> c.s3Bucket(deployBucket).s3Key(key)) .packageType("Zip") .role(LAMBDA_ROLE) .timeout(timeout) ); } private static Supplier findMavenJarFromLocalRepo(String groupId, String artifactId, String version, String classifier) { final File artifactLocation = assertArtifactLocation(groupId, artifactId, version, classifier); return () -> loadArtifact(artifactLocation); } private static File assertArtifactLocation(String groupId, String artifactId, String version, String classifier) { final String mavenRepositoryLocation = mavenLocalRepositoryLocation(); final File artifactLocation = new File(new File(mavenRepositoryLocation, mavenArtifactFolder(groupId, artifactId, version)), mavenArtifactName(artifactId, version, classifier)); if (!artifactLocation.isFile()) { throw new IllegalArgumentException( "Can not locate %s in %s local maven repository. Check in pom.xml" .formatted(artifactLocation, mavenRepositoryLocation)); } return artifactLocation; } @SneakyThrows private static byte[] loadArtifact(File artifactLocation) { try (FileInputStream inputStream = new FileInputStream(artifactLocation)) { return IoUtils.toByteArray(inputStream); } } private static String mavenLocalRepositoryLocation() { String mavenRepositoryLocation = System.getenv(LIME_MAVEN_REPOSITORY); if (mavenRepositoryLocation == null) { mavenRepositoryLocation = "%s/.m2/repository".formatted(System.getProperty("user.home")); log.info("Using default maven repository {}. Override if required with {}", mavenRepositoryLocation, LIME_MAVEN_REPOSITORY); } else { log.info("Using maven repository {} from environment {}", mavenRepositoryLocation, LIME_MAVEN_REPOSITORY); } return mavenRepositoryLocation; } private static String mavenArtifactFolder(String groupId, String artifactId, String version) { return "%s/%s/%s".formatted(groupId.replace('.', '/'), artifactId, version); } private static String mavenArtifactName(String artifactId, String version, String classifier) { if (StringUtils.isBlank(classifier)) { return "%s-%s.jar".formatted(artifactId, version); } return "%s-%s-%s.jar".formatted(artifactId, version, classifier); } private static String lambdaName(String artifactId, Map environment) { return "%s%s".formatted( artifactId, environment.containsKey(FUNCTION_DEF_KEY) ? "-" + environment.get(FUNCTION_DEF_KEY) : ""); } private void safeDelete(String name) { try { lambdaClient.deleteFunction(r -> r.functionName(name)); } catch (Exception e) { log.warn("Error deleting function {}: {} {}", name, e.getClass().getSimpleName(), e.getMessage()); } } private void safeDeleteMapping(String uuid) { try { lambdaClient.deleteEventSourceMapping(c -> c.uuid(uuid)); } catch (Exception e) { log.warn("Error deleting mapping for {}: {}", uuid, e.getMessage()); } } @SneakyThrows private byte[] findJarForModule(String moduleLocation) { log.info("Uploading code from {}", moduleLocation); File moduleBaseDir = new File(moduleLocation); File targetDir = new File(moduleBaseDir, "target"); if (!targetDir.isDirectory()) { throw new IOException("Can not access module target directory %s".formatted(targetDir)); } final String suffix = "-%s.jar".formatted(LAMBDA_JAR_CLASSIFIER); String[] list = targetDir.list((dir, file) -> file.endsWith(suffix)); if (list == null || list.length != 1) { throw new IOException("Incorrect jars (not ending with %s) detected %s".formatted(suffix, list)); } try (FileInputStream input = new FileInputStream(new File(targetDir, list[0]))) { return IoUtils.toByteArray(input); } } private Map spyEnvironment(Map environment, int debugPort, int memoryMegabytes) { log.debug("Supplied Environment is {}", environment); String javaToolOptions = computeToolOptions(environment, memoryMegabytes); Map spyEnv = new LinkedHashMap<>(environment); final boolean inDebugMode = isInDebugMode(debugPort); if (inDebugMode) { log.warn( """ *** \tWARNING JAVA LAMBDA DEBUG ENABLED ON LOCALHOST PORT {}. \tThis can cause port clashes. \tDebug port must match JVM_DEBUG_PORT in docker-compose.yml ***""", debugPort); } spyEnv.put("_JAVA_OPTIONS", "%s %s".formatted(inDebugMode ? DEBUG_OPS.formatted(debugPort) : "", javaToolOptions)); log.debug("Decorated Environment is {}", spyEnv); return spyEnv; } private static String computeToolOptions(Map environment, int memoryMegabytes) { final String javaOptionsKey = "JAVA_TOOL_OPTIONS"; String javaToolOptions = environment.get(javaOptionsKey); if (javaToolOptions == null) { javaToolOptions = environment.getOrDefault(javaOptionsKey, "-XX:+TieredCompilation -XX:TieredStopAtLevel=1"); } final double eightyPercent = 0.8; final int maxHeap = (int) (memoryMegabytes * eightyPercent); log.debug("Defaulting Localstack Lambda Max Heap to 80% of configured memory: {} MB", maxHeap); javaToolOptions += " -Xms%dm -Xmx%dm".formatted(maxHeap, maxHeap); return javaToolOptions; } private Lambda deployStub(String name, String response, boolean failure) { log.info("Creating stub function {} with result {}", name, response); return deploy(name, stubConfiguration(name, response, failure)); } private boolean checkFailed(Lambda lambda, State state) { FunctionConfiguration configuration = describe(lambda.name).configuration(); State currentState = configuration.state(); String stateReason = configuration.stateReason() == null ? "" : configuration.stateReason(); if (currentState == State.FAILED) { log.error("Lambda {} failed with {}", lambda.name, stateReason); } else { log.debug("Lambda {} deployment in state {} {}", lambda.name, currentState, stateReason); } return currentState == state; } private Consumer stubConfiguration(String name, String response, boolean failure) { return r -> r.functionName(name) .description("Stub for %s".formatted(name)) .runtime(NODEJS20_X) .handler("index.handler") .memorySize(STUB_MEMORY) .code(c -> dynamicStubCode(c, response, failure)) .packageType(PackageType.ZIP) .role(LAMBDA_ROLE); } private GetFunctionResponse describe(String name) { return lambdaClient.getFunction(r -> r.functionName(name)); } @SneakyThrows private void dynamicStubCode(FunctionCode.Builder c, String response, boolean failure) { String nodeJsCode = """ exports.handler = async function(event) { if (%b) { throw new Error('%s') } return JSON.parse('%s') }; """.formatted(failure, response, response); // inline hack. try (ByteArrayOutputStream outputBytes = new ByteArrayOutputStream(TWO_KB); ZipOutputStream codeZip = new ZipOutputStream(outputBytes)) { codeZip.putNextEntry(new ZipEntry("index.js")); codeZip.write(nodeJsCode.getBytes(UTF_8)); codeZip.finish(); c.zipFile(SdkBytes.fromByteArray(outputBytes.toByteArray())); } } private Lambda deploy(String name, Consumer stubRequest) { try { return performDeploy(name, stubRequest); } catch (ResourceConflictException e) { log.info("Replacing existing function {}", name); safeDelete(name); return performDeploy(name, stubRequest); } } private static boolean isInDebugMode(int debugPort) { return debugPort > 0; } private Lambda performDeploy(String name, Consumer stubRequest) { CreateFunctionResponse function = lambdaClient.createFunction(stubRequest); log.debug("Pending lambda deployment from {}", function.functionArn()); tracker.add(name); Lambda deployed = new Lambda(name, function.functionArn()); waitForState(deployed, deployTimeoutSeconds, State.ACTIVE); log.info("Deployed lambda {}", deployed); return deployed; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy