From bcc27ff094030e03950ac2c8b9a77eea9f4eab6e Mon Sep 17 00:00:00 2001 From: Matto Date: Fri, 29 Dec 2023 23:40:51 +1100 Subject: [PATCH] Created infrastructure for java21 * Run CDK in java 21 with image `ghcr.io/muhamadto/spring-native-amazonlinux2-builder:21-amazonlinux2` * Replaced `com.coffeebeans.springnativeawslambda.infra.lambda.CustomRuntime2Function` with `com.coffeebeans.springnativeawslambda.infra.lambda.CustomRuntime2023FunctionJava21` and updated tests. * `docker-compose.yml` now adopting `apigateway` functions from `ghcr.io/muhamadto/spring-native-amazonlinux2-builder:21-amazonlinux2-awscliv2`. However, the lambda is not loading and need some more work. * Reinstated `native:compile` in `prepare-package` phase. This is to ensure the `spring-native-aws-lambda-function-native-zip.zip` include would include the `spring-native-aws-lambda-function` native executable. Signed-off-by: matto --- .dockerignore | 2 + .github/workflows/release.yml | 4 +- .gitignore | 10 +- README.md | 79 +- docker-compose.yml | 26 +- settings-spring.xml | 25 - spring-native-aws-lambda-function/pom.xml | 11 +- spring-native-aws-lambda-infra/pom.xml | 14 + .../infra/ApiBaseStack.java | 354 ++++---- .../infra/Application.java | 73 +- .../infra/CoffeeBeansConstruct.java | 8 + .../infra/Constants.java | 14 + .../infra/SpringNativeAwsLambdaStack.java | 19 +- .../springnativeawslambda/infra/TagUtils.java | 14 +- .../lambda/CustomRuntime2023Function.java | 99 +++ .../infra/lambda/CustomRuntime2Function.java | 806 ------------------ .../infra/LambdaTest.java | 435 +++++----- .../infra/RestApiTest.java | 13 +- .../infra/TagUtilsTest.java | 11 +- .../infra/TemplateSupport.java | 61 +- .../infra/TopicTest.java | 22 +- .../lambda/CustomRuntime2023FunctionTest.java | 268 ++++++ .../lambda/CustomRuntime2FunctionTest.java | 269 ------ 23 files changed, 975 insertions(+), 1662 deletions(-) delete mode 100644 settings-spring.xml create mode 100644 spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/CoffeeBeansConstruct.java create mode 100644 spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/Constants.java create mode 100644 spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/lambda/CustomRuntime2023Function.java delete mode 100644 spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/lambda/CustomRuntime2Function.java create mode 100644 spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/lambda/CustomRuntime2023FunctionTest.java delete mode 100644 spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/lambda/CustomRuntime2FunctionTest.java diff --git a/.dockerignore b/.dockerignore index 99dde17..4d096dd 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,6 +2,8 @@ .idea/ badges/ **/target/ +cdk.out/ +volume/ .gitignore CODE_OF_CONDUCT.md HELP.md diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4149bd9..b6f1645 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,7 +24,7 @@ jobs: release: runs-on: ubuntu-latest container: - image: ghcr.io/muhamadto/spring-native-amazonlinux2-builder:20-amazonlinux2 + image: ghcr.io/muhamadto/spring-native-amazonlinux2-builder:21-amazonlinux2 options: --user=worker:ci permissions: id-token: write @@ -56,7 +56,7 @@ jobs: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} ENV: ${{ env.ENV }} COST_CENTRE: ${{ env.COST_CENTRE }} - run: ./mvnw -ntp -Pnative clean package -DskipTests=true + run: ./mvnw -ntp clean package -U -Pnative -DskipTests - name: cdk diff uses: muhamadto/aws-cdk-github-actions@v3 with: diff --git a/.gitignore b/.gitignore index 84982e4..18d8135 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ -target/ -!spring-native-aws-lambda-function/.mvn/wrapper/maven-wrapper.jar -!**/src/main/**/target/ -!**/src/test/**/target/ +**/target/ +cdk.out/ +volume/ +**/maven-wrapper.jar ### STS ### .apt_generated @@ -30,5 +30,3 @@ build/ ### VS Code ### .vscode/ - -**/maven-wrapper.jar diff --git a/README.md b/README.md index 1715b83..11f3ee2 100644 --- a/README.md +++ b/README.md @@ -11,17 +11,17 @@ [![Bugs](https://sonarcloud.io/api/project_badges/measure?project=muhamadto_spring-native-aws-lambda&metric=bugs)](https://sonarcloud.io/summary/new_code?id=muhamadto_spring-native-aws-lambda) [![SonarCloud Coverage](https://sonarcloud.io/api/project_badges/measure?project=muhamadto_spring-native-aws-lambda&metric=coverage)](https://sonarcloud.io/component_measures?id=muhamadto_spring-native-aws-lambda&metric=new_coverage&view=list) -| Component | Version | -|---------------|----------| -| JDK | 20 | -| Spring Cloud | 2022.0.1 | -| Spring Boot | 3.1.2 | +| Component | Version | +|--------------|----------| +| JDK | 21 | +| Spring Cloud | 2023.0.0 | +| Spring Boot | 3.2.1 | ## Test ```bash $ sdk use java 22.2.r17-grl -$ ./mvnw -ntp clean verify -U --settings ./settings-spring.xml +$ ./mvnw -ntp clean verify -U ``` ## Building and Running @@ -33,12 +33,30 @@ $ ./mvnw -ntp clean verify -U --settings ./settings-spring.xml ```shell $ docker-compose up ``` +2. Make a call + ```shell + $ curl --location --request POST 'http://localhost:4566/restapis//test/_user_request_/test' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "body": "{ \"name\": \"CoffeeBeans\" }" + }' + ``` + The service responds + ```json + [ + { + "name": "CoffeeBeans", + "saved": true + } + ] + ``` #### Using `mvnw` -1. Set `spring.main.web-application-type` to `servlet` for local development -2. Run the following commands + +1. Run the following commands ```shell - $ ./mvnw -ntp clean package -U -Pnative -pl spring-native-aws-lambda-function --settings ./settings-spring.xml + $ export SPRING_PROFILES_ACTIVE=local + $ ./mvnw -ntp clean package -U -Pnative -DskipTests -pl spring-native-aws-lambda-function $ ./spring-native-aws-lambda-function/target/spring-native-aws-lambda-function ``` The service starts in less than 100 ms @@ -59,7 +77,7 @@ $ ./mvnw -ntp clean verify -U --settings ./settings-spring.xml $ curl --location --request POST 'http://localhost:8080' \ --header 'Content-Type: application/json' \ --data-raw '{ - "name": "CoffeeBeans" + "body": "{ \"name\": \"CoffeeBeans\" }" }' ``` The service responds @@ -339,43 +357,4 @@ Now that the setup is done you can deploy to AWS. "name": "CoffeeBeans" }' ``` -3. Et voila! It runs with 500 ms for cold start. - -## Maven Repository - -This project uses experimental dependencies from Spring. - -* One way to pul them is to add `repositories` and `pluginRepositories` elements to `pom.xml`. - -* An alternative add them to `settings.xml` as following - -```xml - - - - - spring-native-demo - - - spring-releases - Spring Releases - https://repo.spring.io/release - - - - - - spring-releases - Spring Releases - https://repo.spring.io/release - - - - - - spring-native-demo - - -``` +3. Et voila! It runs with 500 ms for cold start. \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index fbdd222..165976c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,11 +15,8 @@ version: "3.9" services: - spring-native-aws-lambda-function: - image: ghcr.io/muhamadto/spring-native-amazonlinux2-builder:20-amazonlinux2-awscliv2 - ports: - - "8080:8080" - - "5005:5005" + spring-native-aws-lambda-function-infra: + image: ghcr.io/muhamadto/spring-native-amazonlinux2-builder:21-amazonlinux2-awscliv2 volumes: - ./:/app - ${M2_REPO}:/home/worker/.m2 @@ -54,15 +51,24 @@ services: print_info_message "divider" "Package GraalVM function" && - ./mvnw -ntp clean package -U -Pnative -pl spring-native-aws-lambda-function --settings ./settings-spring.xml && + ./mvnw -ntp clean package -U -Pnative -DskipTests -pl spring-native-aws-lambda-function && + print_info_message "divider" "Creating LAMBDA function" && lambda_create_function lambda-FUNCTION provided.al2023 512 ./spring-native-aws-lambda-function/target/spring-native-aws-lambda-function-native-zip.zip spring-native-aws-lambda-function && lambda_wait_for_function lambda-FUNCTION && lambda_list_functions && - - print_info_message "block" "Successfully creating 'spring-native-aws-lambda-function'" && - - exit 0 + + print_info_message "divider" "Creating API Gateway" && + REST_API_ID="$(apigateway_create_restApi "somerestapiname")" && + RESOURCE_ID="$(apigateway_create_resource "$$REST_API_ID" "somePathId")" + apigateway_create_method "$$REST_API_ID" "$$RESOURCE_ID" "POST" && + apigateway_create_lambda_integration "$$REST_API_ID" "$$RESOURCE_ID" "POST" "arn:aws:apigateway:ap-southeast-2:lambda:path/2015-03-31/functions/arn:aws:lambda:ap-southeast-2:000000000000:function:lambda-FUNCTION/invocations" && + apigateway_create_deployment "$$REST_API_ID" "test" && + --rest-api-id && + + print_info_message "plain" "Endpoint available at: http://localhost:4566/restapis/$$REST_API_ID/test/_user_request_/test" && + + print_info_message "block" "Successfully creating 'spring-native-aws-lambda-function'" ' depends_on: - localstack diff --git a/settings-spring.xml b/settings-spring.xml deleted file mode 100644 index 33ceb6e..0000000 --- a/settings-spring.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - native - - diff --git a/spring-native-aws-lambda-function/pom.xml b/spring-native-aws-lambda-function/pom.xml index 8b50c8f..da68a0a 100644 --- a/spring-native-aws-lambda-function/pom.xml +++ b/spring-native-aws-lambda-function/pom.xml @@ -167,7 +167,7 @@ --verbose --no-fallback --enable-preview - --gc=G1 + -march=native --strict-image-heap --enable-url-protocols=http @@ -175,6 +175,15 @@ -H:+ReportExceptionStackTraces + + + build-native + prepare-package + + compile + + + maven-assembly-plugin diff --git a/spring-native-aws-lambda-infra/pom.xml b/spring-native-aws-lambda-infra/pom.xml index 04d6391..4cae631 100644 --- a/spring-native-aws-lambda-infra/pom.xml +++ b/spring-native-aws-lambda-infra/pom.xml @@ -118,4 +118,18 @@ + + + ${project.artifactId} + + + org.apache.maven.plugins + maven-deploy-plugin + 3.1.1 + + true + + + + \ No newline at end of file diff --git a/spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/ApiBaseStack.java b/spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/ApiBaseStack.java index bebcee0..fafc682 100644 --- a/spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/ApiBaseStack.java +++ b/spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/ApiBaseStack.java @@ -18,11 +18,7 @@ package com.coffeebeans.springnativeawslambda.infra; -import com.coffeebeans.springnativeawslambda.infra.lambda.CustomRuntime2Function; -import java.util.Map; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotEmpty; -import javax.validation.constraints.NotNull; +import com.coffeebeans.springnativeawslambda.infra.lambda.CustomRuntime2023Function; import org.apache.commons.lang3.StringUtils; import software.amazon.awscdk.Duration; import software.amazon.awscdk.Stack; @@ -35,6 +31,7 @@ import software.amazon.awscdk.services.iam.Role; import software.amazon.awscdk.services.lambda.Code; import software.amazon.awscdk.services.lambda.Function; +import software.amazon.awscdk.services.lambda.FunctionProps; import software.amazon.awscdk.services.sns.ITopic; import software.amazon.awscdk.services.sns.Topic; import software.amazon.awscdk.services.sns.subscriptions.SqsSubscription; @@ -43,173 +40,186 @@ import software.amazon.awscdk.services.sqs.Queue; import software.constructs.Construct; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.Map; + +import static software.amazon.awscdk.services.lambda.Runtime.PROVIDED_AL2023; + public class ApiBaseStack extends Stack { - private static final int LAMBDA_FUNCTION_TIMEOUT_IN_SECONDS = 3; - private static final int LAMBDA_FUNCTION_MEMORY_SIZE = 512; - private static final int LAMBDA_FUNCTION_RETRY_ATTEMPTS = 2; - private static final String FIFO_SUFFIX = ".fifo"; - private static final String DEAD_LETTER_QUEUE_SUFFIX = "-dlq"; - - public ApiBaseStack( - @NotNull final Construct scope, - @NotBlank final String id, - @NotNull final StackProps props) { - super(scope, id, props); - } - - @NotNull - protected Queue createQueue(@NotBlank final String queueId) { - final DeadLetterQueue deadLetterQueue = createDeadLetterQueue( - queueId + DEAD_LETTER_QUEUE_SUFFIX); - - return Queue.Builder.create(this, queueId) - .queueName(queueId) - .deadLetterQueue(deadLetterQueue) - .build(); - } - - @NotNull - protected Queue createFifoQueue( - @NotBlank final String queueId, - final boolean contentBasedDeduplication, - @NotNull final DeduplicationScope messageGroup) { - final String fifoQueueId = queueId + FIFO_SUFFIX; - final String fifoDeadLetterQueueId = queueId + DEAD_LETTER_QUEUE_SUFFIX; - - final DeadLetterQueue deadLetterQueue = - createFifoDeadLetterQueue(fifoDeadLetterQueueId, contentBasedDeduplication, messageGroup); - - return Queue.Builder.create(this, fifoQueueId) - .queueName(fifoQueueId) - .fifo(true) - .deadLetterQueue(deadLetterQueue) - .contentBasedDeduplication(contentBasedDeduplication) - .deduplicationScope(messageGroup) - .build(); - } - - @NotNull - protected DeadLetterQueue createDeadLetterQueue(@NotBlank final String deadLetterQueueId) { - final Queue queue = Queue.Builder.create(this, deadLetterQueueId) - .queueName(deadLetterQueueId) - .build(); - - return DeadLetterQueue.builder() - .queue(queue) - .maxReceiveCount(3) - .build(); - } - - @NotNull - protected DeadLetterQueue createFifoDeadLetterQueue( - @NotBlank final String deadLetterQueueId, - final boolean contentBasedDeduplication, - @NotNull final DeduplicationScope messageGroup) { - final String fifoDeadLetterQueueId = deadLetterQueueId + FIFO_SUFFIX; - final Queue queue = Queue.Builder.create(this, fifoDeadLetterQueueId) - .queueName(fifoDeadLetterQueueId) - .fifo(true) - .contentBasedDeduplication(contentBasedDeduplication) - .deduplicationScope(messageGroup) - .build(); - - return DeadLetterQueue.builder() - .queue(queue) - .maxReceiveCount(3) - .build(); - } - - @NotNull - protected Topic createTopic( - @NotBlank final String topicId) { - - return Topic.Builder.create(this, topicId) - .topicName(topicId) - .build(); - } - - @NotNull - protected Topic createFifoTopic( - @NotBlank final String topicId, - final boolean fifo, - final boolean contentBasedDeduplication) { - String fifoTopicId = topicId + FIFO_SUFFIX; - - return Topic.Builder.create(this, fifoTopicId) - .topicName(fifoTopicId) - .fifo(fifo) - .contentBasedDeduplication(contentBasedDeduplication) - .build(); - } - - @NotNull - protected static SqsSubscription createSqsSubscription(@NotNull final Queue queue) { - return SqsSubscription.Builder.create(queue).build(); - } - - @NotNull - protected Function createFunction( - @NotBlank final String lambdaId, - @NotBlank final String handler, - @NotNull final Code code, - @NotNull final Topic deadLetterTopic, - @NotNull Role role, - @NotEmpty final Map environment) { - return this.createFunction(null, - lambdaId, - handler, - code, - deadLetterTopic, - role, - environment); - } - - @NotNull - protected Function createFunction( - final IVpc vpc, - @NotBlank final String lambdaId, - @NotBlank final String handler, - @NotNull final Code code, - @NotNull final ITopic deadLetterTopic, - @NotNull IRole role, - @NotEmpty final Map environment) { - return CustomRuntime2Function.Builder.create(this, lambdaId) - .functionName(lambdaId) - .description("Lambda example with spring native") - .code(code) - .handler(handler) - .role(role) - .vpc(vpc) - .environment(environment) - .deadLetterTopic(deadLetterTopic) - .timeout(Duration.seconds(LAMBDA_FUNCTION_TIMEOUT_IN_SECONDS)) - .memorySize(LAMBDA_FUNCTION_MEMORY_SIZE) - .retryAttempts(LAMBDA_FUNCTION_RETRY_ATTEMPTS) - .build(); - } - - @NotNull - protected LambdaRestApi createLambdaRestApi( - @NotBlank final String stageName, - @NotBlank final String restApiId, - @NotNull final String resourceName, - @NotNull final String httpMethod, - @NotNull final Function function, - final boolean proxy) { - - // point to the lambda - final LambdaRestApi lambdaRestApi = LambdaRestApi.Builder.create(this, restApiId) - .restApiName(restApiId) - .handler(function) - .proxy(proxy) - .deployOptions(StageOptions.builder().stageName(stageName).build()) - .build(); - - // get root resource to add methods - final Resource resource = lambdaRestApi.getRoot().addResource(resourceName); - resource.addMethod(StringUtils.toRootUpperCase(httpMethod)); - - return lambdaRestApi; - } + private static final int LAMBDA_FUNCTION_TIMEOUT_IN_SECONDS = 3; + private static final int LAMBDA_FUNCTION_MEMORY_SIZE = 512; + private static final int LAMBDA_FUNCTION_RETRY_ATTEMPTS = 2; + private static final String FIFO_SUFFIX = ".fifo"; + private static final String DEAD_LETTER_QUEUE_SUFFIX = "-dlq"; + + public ApiBaseStack( + @NotNull final Construct scope, + @NotBlank final String id, + @NotNull final StackProps props) { + super(scope, id, props); + } + + @NotNull + protected Queue createQueue(@NotBlank final String queueId) { + final DeadLetterQueue deadLetterQueue = createDeadLetterQueue( + queueId + DEAD_LETTER_QUEUE_SUFFIX); + + return Queue.Builder.create(this, queueId) + .queueName(queueId) + .deadLetterQueue(deadLetterQueue) + .build(); + } + + @NotNull + protected Queue createFifoQueue( + @NotBlank final String queueId, + final boolean contentBasedDeduplication, + @NotNull final DeduplicationScope messageGroup) { + final String fifoQueueId = queueId + FIFO_SUFFIX; + final String fifoDeadLetterQueueId = queueId + DEAD_LETTER_QUEUE_SUFFIX; + + final DeadLetterQueue deadLetterQueue = + createFifoDeadLetterQueue(fifoDeadLetterQueueId, contentBasedDeduplication, messageGroup); + + return Queue.Builder.create(this, fifoQueueId) + .queueName(fifoQueueId) + .fifo(true) + .deadLetterQueue(deadLetterQueue) + .contentBasedDeduplication(contentBasedDeduplication) + .deduplicationScope(messageGroup) + .build(); + } + + @NotNull + protected DeadLetterQueue createDeadLetterQueue(@NotBlank final String deadLetterQueueId) { + final Queue queue = Queue.Builder.create(this, deadLetterQueueId) + .queueName(deadLetterQueueId) + .build(); + + return DeadLetterQueue.builder() + .queue(queue) + .maxReceiveCount(3) + .build(); + } + + @NotNull + protected DeadLetterQueue createFifoDeadLetterQueue( + @NotBlank final String deadLetterQueueId, + final boolean contentBasedDeduplication, + @NotNull final DeduplicationScope messageGroup) { + final String fifoDeadLetterQueueId = deadLetterQueueId + FIFO_SUFFIX; + final Queue queue = Queue.Builder.create(this, fifoDeadLetterQueueId) + .queueName(fifoDeadLetterQueueId) + .fifo(true) + .contentBasedDeduplication(contentBasedDeduplication) + .deduplicationScope(messageGroup) + .build(); + + return DeadLetterQueue.builder() + .queue(queue) + .maxReceiveCount(3) + .build(); + } + + @NotNull + protected Topic createTopic( + @NotBlank final String topicId) { + + return Topic.Builder.create(this, topicId) + .topicName(topicId) + .build(); + } + + @NotNull + protected Topic createFifoTopic( + @NotBlank final String topicId, + final boolean fifo, + final boolean contentBasedDeduplication) { + String fifoTopicId = topicId + FIFO_SUFFIX; + + return Topic.Builder.create(this, fifoTopicId) + .topicName(fifoTopicId) + .fifo(fifo) + .contentBasedDeduplication(contentBasedDeduplication) + .build(); + } + + @NotNull + protected static SqsSubscription createSqsSubscription(@NotNull final Queue queue) { + return SqsSubscription.Builder.create(queue).build(); + } + + @NotNull + protected Function createFunction( + @NotBlank final String lambdaId, + @NotBlank final String handler, + @NotNull final Code code, + @NotNull final Topic deadLetterTopic, + @NotNull Role role, + @NotEmpty final Map environment) { + return this.createFunction(null, + lambdaId, + handler, + code, + deadLetterTopic, + role, + environment); + } + + @NotNull + protected Function createFunction( + final IVpc vpc, + @NotBlank final String lambdaId, + @NotBlank final String handler, + @NotNull final Code code, + @NotNull final ITopic deadLetterTopic, + @NotNull IRole role, + @NotEmpty final Map environment) { + + FunctionProps functionProps = FunctionProps.builder() + .runtime(PROVIDED_AL2023) + .functionName(lambdaId) + .description("Lambda example with spring native") + .code(code) + .handler(handler) + .role(role) + .vpc(vpc) + .environment(environment) + .deadLetterTopic(deadLetterTopic) + .timeout(Duration.seconds(LAMBDA_FUNCTION_TIMEOUT_IN_SECONDS)) + .memorySize(LAMBDA_FUNCTION_MEMORY_SIZE) + .retryAttempts(LAMBDA_FUNCTION_RETRY_ATTEMPTS) + .build(); + + return new CustomRuntime2023Function(this, lambdaId, functionProps) + .getFunction(); + + } + + @NotNull + protected LambdaRestApi createLambdaRestApi( + @NotBlank final String stageName, + @NotBlank final String restApiId, + @NotNull final String resourceName, + @NotNull final String httpMethod, + @NotNull final Function function, + final boolean proxy) { + + // point to the lambda + final LambdaRestApi lambdaRestApi = LambdaRestApi.Builder.create(this, restApiId) + .restApiName(restApiId) + .handler(function) + .proxy(proxy) + .deployOptions(StageOptions.builder().stageName(stageName).build()) + .build(); + + // get root resource to add methods + final Resource resource = lambdaRestApi.getRoot().addResource(resourceName); + resource.addMethod(StringUtils.toRootUpperCase(httpMethod)); + + return lambdaRestApi; + } } \ No newline at end of file diff --git a/spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/Application.java b/spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/Application.java index fc88188..15ecaaf 100644 --- a/spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/Application.java +++ b/spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/Application.java @@ -18,54 +18,53 @@ package com.coffeebeans.springnativeawslambda.infra; +import lombok.NoArgsConstructor; +import software.amazon.awscdk.App; +import software.amazon.awscdk.Tags; + +import java.util.Map; +import java.util.Objects; + +import static com.coffeebeans.springnativeawslambda.infra.Constants.KEY_COST_CENTRE; +import static com.coffeebeans.springnativeawslambda.infra.Constants.KEY_ENV; import static com.coffeebeans.springnativeawslambda.infra.StackUtils.createStack; -import static com.coffeebeans.springnativeawslambda.infra.TagUtils.TAG_KEY_ENV; -import static com.coffeebeans.springnativeawslambda.infra.TagUtils.TAG_VALUE_COST_CENTRE; import static com.coffeebeans.springnativeawslambda.infra.TagUtils.createTags; import static com.google.common.base.Preconditions.checkNotNull; import static lombok.AccessLevel.PRIVATE; -import java.util.Map; -import java.util.Objects; -import lombok.NoArgsConstructor; -import software.amazon.awscdk.App; -import software.amazon.awscdk.Tags; - @NoArgsConstructor(access = PRIVATE) public final class Application { - private static final String DEV_STACK_NAME = "spring-native-aws-lambda-function-dev-stack"; - private static final String PRD_STACK_NAME = "spring-native-aws-lambda-function-prd-stack"; - private static final String ENVIRONMENT_NAME_DEV = "dev"; - private static final String ENVIRONMENT_NAME_PRD = "prd"; - private static final String LAMBDA_CODE_PATH = - SpringNativeAwsLambdaStack.LAMBDA_FUNCTION_ID - + "/target/spring-native-aws-lambda-function-native-zip.zip"; - private static final String QUALIFIER = "cbcore"; - private static final String FILE_ASSETS_BUCKET_NAME = "cbcore-cdk-bucket"; + private static final String DEV_STACK_NAME = "spring-native-aws-lambda-function-dev-stack"; + private static final String PRD_STACK_NAME = "spring-native-aws-lambda-function-prd-stack"; + private static final String ENVIRONMENT_NAME_DEV = "dev"; + private static final String ENVIRONMENT_NAME_PRD = "prd"; + private static final String LAMBDA_CODE_PATH = + SpringNativeAwsLambdaStack.LAMBDA_FUNCTION_ID + + "/target/spring-native-aws-lambda-function-native-zip.zip"; + private static final String QUALIFIER = "cbcore"; + private static final String FILE_ASSETS_BUCKET_NAME = "cbcore-cdk-bucket"; - public static void main(final String... args) { - final App app = new App(); + public static void main(final String... args) { + final App app = new App(); - final String env = System.getenv(TAG_KEY_ENV); - checkNotNull(env, "'env' environment variable is required"); - final Map tags = createTags(env, TAG_VALUE_COST_CENTRE); + final String env = System.getenv(KEY_ENV); + checkNotNull(env, "'env' environment variable is required"); + final Map tags = createTags(env, KEY_COST_CENTRE); - switch (env) { - case ENVIRONMENT_NAME_DEV -> - createStack(app, DEV_STACK_NAME, LAMBDA_CODE_PATH, QUALIFIER, FILE_ASSETS_BUCKET_NAME, - env); - case ENVIRONMENT_NAME_PRD -> - createStack(app, PRD_STACK_NAME, LAMBDA_CODE_PATH, QUALIFIER, FILE_ASSETS_BUCKET_NAME, - env); - default -> throw new IllegalArgumentException("Environment variable " + TAG_KEY_ENV - + " is not set to a valid value. Set it to '[dev|prd]'"); - } + switch (env) { + case ENVIRONMENT_NAME_DEV -> + createStack(app, DEV_STACK_NAME, LAMBDA_CODE_PATH, QUALIFIER, FILE_ASSETS_BUCKET_NAME, env); + case ENVIRONMENT_NAME_PRD -> + createStack(app, PRD_STACK_NAME, LAMBDA_CODE_PATH, QUALIFIER, FILE_ASSETS_BUCKET_NAME, env); + default -> throw new IllegalArgumentException("Environment variable " + KEY_ENV + + " is not set to a valid value. Set it to '[dev|prd]'"); + } - tags.entrySet().stream() - .filter(tag -> Objects.nonNull(tag.getValue())) - .forEach(tag -> Tags.of(app).add(tag.getKey(), tag.getValue())); + tags.entrySet().stream() + .filter(tag -> Objects.nonNull(tag.getValue())) + .forEach(tag -> Tags.of(app).add(tag.getKey(), tag.getValue())); - app.synth(); - } + app.synth(); + } } \ No newline at end of file diff --git a/spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/CoffeeBeansConstruct.java b/spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/CoffeeBeansConstruct.java new file mode 100644 index 0000000..141706d --- /dev/null +++ b/spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/CoffeeBeansConstruct.java @@ -0,0 +1,8 @@ +package com.coffeebeans.springnativeawslambda.infra; + +import software.constructs.IConstruct; + +public interface CoffeeBeansConstruct extends IConstruct { + + +} diff --git a/spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/Constants.java b/spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/Constants.java new file mode 100644 index 0000000..d628ffa --- /dev/null +++ b/spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/Constants.java @@ -0,0 +1,14 @@ +package com.coffeebeans.springnativeawslambda.infra; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class Constants { + public static final String KEY_ENV = "ENV"; + public static final String KEY_COST_CENTRE = "COST_CENTRE"; + public static final String KEY_APPLICATION_NAME = "applicationName"; + + public static final String VALUE_COST_CENTRE = "coffeeBeans-core"; + +} diff --git a/spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/SpringNativeAwsLambdaStack.java b/spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/SpringNativeAwsLambdaStack.java index b601893..c850914 100644 --- a/spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/SpringNativeAwsLambdaStack.java +++ b/spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/SpringNativeAwsLambdaStack.java @@ -18,14 +18,6 @@ package com.coffeebeans.springnativeawslambda.infra; -import static com.coffeebeans.springnativeawslambda.infra.TagUtils.TAG_KEY_ENV; -import static software.amazon.awscdk.services.iam.ManagedPolicy.fromAwsManagedPolicyName; -import static software.amazon.awscdk.services.lambda.Code.fromAsset; - -import java.util.List; -import java.util.Map; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotNull; import software.amazon.awscdk.StackProps; import software.amazon.awscdk.services.iam.IManagedPolicy; import software.amazon.awscdk.services.iam.Role; @@ -35,6 +27,15 @@ import software.amazon.awscdk.services.sns.Topic; import software.constructs.Construct; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.util.List; +import java.util.Map; + +import static com.coffeebeans.springnativeawslambda.infra.Constants.KEY_ENV; +import static software.amazon.awscdk.services.iam.ManagedPolicy.fromAwsManagedPolicyName; +import static software.amazon.awscdk.services.lambda.Code.fromAsset; + public class SpringNativeAwsLambdaStack extends ApiBaseStack { static final String LAMBDA_FUNCTION_ID = "spring-native-aws-lambda-function"; @@ -65,7 +66,7 @@ public SpringNativeAwsLambdaStack( final AssetCode assetCode = fromAsset(lambdaCodePath); final Map environment = - Map.of(ENVIRONMENT_VARIABLE_SPRING_PROFILES_ACTIVE, stage, TAG_KEY_ENV, stage); + Map.of(ENVIRONMENT_VARIABLE_SPRING_PROFILES_ACTIVE, stage, KEY_ENV, stage); final Function function = createFunction( LAMBDA_FUNCTION_ID, diff --git a/spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/TagUtils.java b/spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/TagUtils.java index 1ba1c97..7153229 100644 --- a/spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/TagUtils.java +++ b/spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/TagUtils.java @@ -18,20 +18,20 @@ package com.coffeebeans.springnativeawslambda.infra; -import java.util.Map; -import javax.validation.constraints.NotBlank; import lombok.AccessLevel; import lombok.NoArgsConstructor; +import javax.validation.constraints.NotBlank; +import java.util.Map; + +import static com.coffeebeans.springnativeawslambda.infra.Constants.KEY_COST_CENTRE; +import static com.coffeebeans.springnativeawslambda.infra.Constants.KEY_ENV; + @NoArgsConstructor(access = AccessLevel.PRIVATE) public final class TagUtils { - public static final String TAG_KEY_ENV = "ENV"; - public static final String TAG_KEY_COST_CENTRE = "COST_CENTRE"; - public static final String TAG_VALUE_COST_CENTRE = "coffeeBeans-core"; - public static Map createTags(@NotBlank final String env, @NotBlank final String costCentre) { - return Map.of(TAG_KEY_ENV, env, TAG_KEY_COST_CENTRE, costCentre); + return Map.of(KEY_ENV, env, KEY_COST_CENTRE, costCentre); } } diff --git a/spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/lambda/CustomRuntime2023Function.java b/spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/lambda/CustomRuntime2023Function.java new file mode 100644 index 0000000..379f7b0 --- /dev/null +++ b/spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/lambda/CustomRuntime2023Function.java @@ -0,0 +1,99 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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.coffeebeans.springnativeawslambda.infra.lambda; + +import com.coffeebeans.springnativeawslambda.infra.CoffeeBeansConstruct; +import lombok.Getter; +import org.jetbrains.annotations.NotNull; +import software.amazon.awscdk.Duration; +import software.amazon.awscdk.services.lambda.Function; +import software.amazon.awscdk.services.lambda.FunctionProps; +import software.constructs.Construct; + +import static com.google.common.base.Preconditions.checkArgument; +import static org.apache.commons.collections4.MapUtils.isNotEmpty; +import static org.apache.commons.lang3.StringUtils.isNoneBlank; +import static software.amazon.awscdk.services.lambda.Runtime.PROVIDED_AL2023; + +/** + * A lambda function with runtime provided.al2023 (custom runtime al2023). Creating function with any other + * runtime (e.g. passed in the {@link FunctionProps} or via any other means will be ignored). + */ + +@Getter +public class CustomRuntime2023Function extends Construct implements CoffeeBeansConstruct { + + private static final int FUNCTION_DEFAULT_TIMEOUT_IN_SECONDS = 10; + private static final int FUNCTION_DEFAULT_MEMORY_SIZE = 512; + private static final int FUNCTION_DEFAULT_RETRY_ATTEMPTS = 2; + + private final software.amazon.awscdk.services.lambda.Function function; + + + /** + * @param scope This parameter is required. + * @param id This parameter is required. + * @param props This parameter is required. + */ + public CustomRuntime2023Function(@NotNull final Construct scope, @NotNull final String id, + @NotNull final FunctionProps props) { + super(scope, id); + + checkArgument(isNoneBlank(props.getHandler()), "'handler' is required"); + checkArgument(isNoneBlank(props.getDescription()), "'description' is required"); + checkArgument(isNotEmpty(props.getEnvironment()), "'environment' is required"); + + final Duration timeout = props.getTimeout() == null + ? Duration.seconds(FUNCTION_DEFAULT_TIMEOUT_IN_SECONDS) + : props.getTimeout(); + + final Number memorySize = props.getMemorySize() == null + ? FUNCTION_DEFAULT_MEMORY_SIZE + : props.getMemorySize(); + + final Number retryAttempts = props.getRetryAttempts() == null + ? FUNCTION_DEFAULT_RETRY_ATTEMPTS + : props.getRetryAttempts(); + + checkArgument(props.getMemorySize().intValue() >= 128 + && props.getMemorySize().intValue() <= 3008, + "'memorySize' must be between 128 and 3008 (inclusive)"); + + checkArgument(props.getRetryAttempts().intValue() >= 0 + && props.getRetryAttempts().intValue() <= 2, + "'retryAttempts' must be between 0 and 2 (inclusive)"); + + + function = Function.Builder.create(this, id) + .runtime(PROVIDED_AL2023) + .functionName(props.getFunctionName()) + .description(props.getDescription()) + .code(props.getCode()) + .handler(props.getHandler()) + .role(props.getRole()) + .vpc(props.getVpc()) + .environment(props.getEnvironment()) + .deadLetterTopic(props.getDeadLetterTopic()) + .timeout(timeout) + .memorySize(memorySize) + .retryAttempts(retryAttempts) + .build(); + + } +} diff --git a/spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/lambda/CustomRuntime2Function.java b/spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/lambda/CustomRuntime2Function.java deleted file mode 100644 index 3982e2c..0000000 --- a/spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/lambda/CustomRuntime2Function.java +++ /dev/null @@ -1,806 +0,0 @@ -/* - * Licensed to Muhammad Hamadto - * - * 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 - * - * See the NOTICE file distributed with this work for additional information regarding copyright ownership. - * - * 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.coffeebeans.springnativeawslambda.infra.lambda; - -import static com.google.common.base.Preconditions.checkArgument; -import static org.apache.commons.collections4.MapUtils.isNotEmpty; -import static org.apache.commons.lang3.StringUtils.isNoneBlank; -import static software.amazon.awscdk.services.lambda.Runtime.PROVIDED_AL2023; - -import java.util.List; -import java.util.Map; -import org.jetbrains.annotations.NotNull; -import software.amazon.awscdk.Duration; -import software.amazon.awscdk.Size; -import software.amazon.awscdk.services.codeguruprofiler.IProfilingGroup; -import software.amazon.awscdk.services.ec2.ISecurityGroup; -import software.amazon.awscdk.services.ec2.IVpc; -import software.amazon.awscdk.services.ec2.SubnetSelection; -import software.amazon.awscdk.services.iam.IRole; -import software.amazon.awscdk.services.iam.PolicyStatement; -import software.amazon.awscdk.services.kms.IKey; -import software.amazon.awscdk.services.lambda.Architecture; -import software.amazon.awscdk.services.lambda.Code; -import software.amazon.awscdk.services.lambda.FileSystem; -import software.amazon.awscdk.services.lambda.Function; -import software.amazon.awscdk.services.lambda.FunctionProps; -import software.amazon.awscdk.services.lambda.ICodeSigningConfig; -import software.amazon.awscdk.services.lambda.IDestination; -import software.amazon.awscdk.services.lambda.IEventSource; -import software.amazon.awscdk.services.lambda.ILayerVersion; -import software.amazon.awscdk.services.lambda.LambdaInsightsVersion; -import software.amazon.awscdk.services.lambda.LogRetentionRetryOptions; -import software.amazon.awscdk.services.lambda.Runtime; -import software.amazon.awscdk.services.lambda.Tracing; -import software.amazon.awscdk.services.lambda.VersionOptions; -import software.amazon.awscdk.services.logs.RetentionDays; -import software.amazon.awscdk.services.sns.ITopic; -import software.amazon.awscdk.services.sqs.IQueue; -import software.constructs.Construct; - -/** - * A lambda function with runtime provided.al2 (custom runtime 2). Creating function with any other - * runtime, will result in unexpected behaviour. - */ - -public class CustomRuntime2Function extends Function { - - private static final int FUNCTION_DEFAULT_TIMEOUT_IN_SECONDS = 10; - private static final int FUNCTION_DEFAULT_MEMORY_SIZE = 512; - private static final int FUNCTION_DEFAULT_RETRY_ATTEMPTS = 2; - - /** - * @param scope This parameter is required. - * @param id This parameter is required. - * @param props This parameter is required. - */ - private CustomRuntime2Function(@NotNull final Construct scope, @NotNull final String id, - @NotNull final FunctionProps props) { - super(scope, id, props); - } - - /** - * The runtime configured for this lambda. - */ - @Override - public @NotNull Runtime getRuntime() { - return PROVIDED_AL2023; - } - - public static final class Builder implements - software.amazon.jsii.Builder { - - /** - * @param scope This parameter is required. - * @param id This parameter is required. - * @return a new instance of {@link CustomRuntime2Function.Builder}. - */ - public static CustomRuntime2Function.Builder create(final Construct scope, final String id) { - return new CustomRuntime2Function.Builder(scope, id); - } - - private final Construct scope; - private final String id; - private final FunctionProps.Builder props; - - private Builder(final Construct scope, final String id) { - this.scope = scope; - this.id = id; - this.props = new FunctionProps.Builder(); - - this.props.runtime(PROVIDED_AL2023); - } - - /** - * The maximum age of a request that Lambda sends to a function for processing. - *

- * Minimum: 60 seconds Maximum: 6 hours - *

- * Default: Duration.hours(6) - *

- * - * @param maxEventAge The maximum age of a request that Lambda sends to a function for - * processing. This parameter is required. - * @return {@code this} - */ - public CustomRuntime2Function.Builder maxEventAge(final Duration maxEventAge) { - this.props.maxEventAge(maxEventAge); - return this; - } - - /** - * The destination for failed invocations. - *

- * Default: - no destination - *

- * - * @param onFailure The destination for failed invocations. This parameter is required. - * @return {@code this} - */ - public CustomRuntime2Function.Builder onFailure(final IDestination onFailure) { - this.props.onFailure(onFailure); - return this; - } - - /** - * The destination for successful invocations. - *

- * Default: - no destination - *

- * - * @param onSuccess The destination for successful invocations. This parameter is required. - * @return {@code this} - */ - public CustomRuntime2Function.Builder onSuccess(final IDestination onSuccess) { - this.props.onSuccess(onSuccess); - return this; - } - - /** - * The maximum number of times to retry when the function returns an error. - *

- * Minimum: 0 Maximum: 2 - *

- * Default: 2 - *

- * - * @param retryAttempts The maximum number of times to retry when the function returns an error. - * This parameter is required. - * @return {@code this} - */ - public CustomRuntime2Function.Builder retryAttempts(final Number retryAttempts) { - this.props.retryAttempts( - retryAttempts == null ? FUNCTION_DEFAULT_RETRY_ATTEMPTS : retryAttempts); - return this; - } - - /** - * Whether to allow the Lambda to send all network traffic. - *

- * If set to false, you must individually add traffic rules to allow the Lambda to connect to - * network targets. - *

- * Default: true - *

- * - * @param allowAllOutbound Whether to allow the Lambda to send all network traffic. This - * parameter is required. - * @return {@code this} - */ - public CustomRuntime2Function.Builder allowAllOutbound(final Boolean allowAllOutbound) { - this.props.allowAllOutbound(allowAllOutbound); - return this; - } - - /** - * Lambda Functions in a public subnet can NOT access the internet. - *

- * Use this property to acknowledge this limitation and still place the function in a public - * subnet. - *

- * Default: false - *

- * - * @param allowPublicSubnet Lambda Functions in a public subnet can NOT access the internet. - * This parameter is required. - * @return {@code this} - * @see https://stackoverflow.com/questions/52992085/why-cant-an-aws-lambda-function-inside-a-public-subnet-in-a-vpc-connect-to-the/52994841#52994841 - */ - public CustomRuntime2Function.Builder allowPublicSubnet(final Boolean allowPublicSubnet) { - this.props.allowPublicSubnet(allowPublicSubnet); - return this; - } - - /** - * The system architectures compatible with this lambda function. - *

- * Default: Architecture.X86_64 - *

- * - * @param architecture The system architectures compatible with this lambda function. This - * parameter is required. - * @return {@code this} - */ - public CustomRuntime2Function.Builder architecture(final Architecture architecture) { - this.props.architecture(architecture); - return this; - } - - /** - * Code signing config associated with this function. - *

- * Default: - Not Sign the Code - *

- * - * @param codeSigningConfig Code signing config associated with this function. This parameter is - * required. - * @return {@code this} - */ - public CustomRuntime2Function.Builder codeSigningConfig( - final ICodeSigningConfig codeSigningConfig) { - this.props.codeSigningConfig(codeSigningConfig); - return this; - } - - /** - * Options for the `lambda.Version` resource automatically created by the `fn.currentVersion` - * method. - *

- * Default: - default options as described in `VersionOptions` - *

- * - * @param currentVersionOptions Options for the `lambda.Version` resource automatically created - * by the `fn.currentVersion` method. This parameter is required. - * @return {@code this} - */ - public CustomRuntime2Function.Builder currentVersionOptions( - final VersionOptions currentVersionOptions) { - this.props.currentVersionOptions(currentVersionOptions); - return this; - } - - /** - * The SQS queue to use if DLQ is enabled. - *

- * If SNS topic is desired, specify deadLetterTopic property instead. - *

- * Default: - SQS queue with 14 day retention period if `deadLetterQueueEnabled` is `true` - *

- * - * @param deadLetterQueue The SQS queue to use if DLQ is enabled. This parameter is required. - * @return {@code this} - */ - public CustomRuntime2Function.Builder deadLetterQueue(final IQueue deadLetterQueue) { - this.props.deadLetterQueue(deadLetterQueue); - return this; - } - - /** - * Enabled DLQ. - *

- * If deadLetterQueue is undefined, an SQS queue with default options will be - * defined for your Function. - *

- * Default: - false unless `deadLetterQueue` is set, which implies DLQ is enabled. - *

- * - * @param deadLetterQueueEnabled Enabled DLQ. This parameter is required. - * @return {@code this} - */ - public CustomRuntime2Function.Builder deadLetterQueueEnabled( - final Boolean deadLetterQueueEnabled) { - this.props.deadLetterQueueEnabled(deadLetterQueueEnabled); - return this; - } - - /** - * The SNS topic to use as a DLQ. - *

- * Note that if deadLetterQueueEnabled is set to true, an SQS queue - * will be created rather than an SNS topic. Using an SNS topic as a DLQ requires this property - * to be set explicitly. - *

- * Default: - no SNS topic - *

- * - * @param deadLetterTopic The SNS topic to use as a DLQ. This parameter is required. - * @return {@code this} - */ - public CustomRuntime2Function.Builder deadLetterTopic(final ITopic deadLetterTopic) { - this.props.deadLetterTopic(deadLetterTopic); - return this; - } - - /** - * A description of the function. - *

- * Default: - No description. - *

- * - * @return {@code this} - */ - public Builder description() { - return description(null); - } - - /** - * A description of the function. - *

- * Default: - No description. - *

- * - * @param description A description of the function. This parameter is required. - * @return {@code this} - */ - public CustomRuntime2Function.Builder description(final String description) { - this.props.description(description); - return this; - } - - /** - * Key-value pairs that Lambda caches and makes available for your Lambda functions. - *

- * Use environment variables to apply configuration changes, such as test and production - * environment configurations, without changing your Lambda function source code. - *

- * Default: - No environment variables. - *

- * - * @param environment Key-value pairs that Lambda caches and makes available for your Lambda - * functions. This parameter is required. - * @return {@code this} - */ - public CustomRuntime2Function.Builder environment(final Map environment) { - this.props.environment(environment); - return this; - } - - /** - * The AWS KMS key that's used to encrypt your function's environment variables. - *

- * Default: - AWS Lambda creates and uses an AWS managed customer master key (CMK). - *

- * - * @param environmentEncryption The AWS KMS key that's used to encrypt your function's - * environment variables. This parameter is required. - * @return {@code this} - */ - public CustomRuntime2Function.Builder environmentEncryption(final IKey environmentEncryption) { - this.props.environmentEncryption(environmentEncryption); - return this; - } - - /** - * The size of the function’s /tmp directory in MiB. - *

- * Default: 512 MiB - *

- * - * @param ephemeralStorageSize The size of the function’s /tmp directory in MiB. This parameter - * is required. - * @return {@code this} - */ - public CustomRuntime2Function.Builder ephemeralStorageSize(final Size ephemeralStorageSize) { - this.props.ephemeralStorageSize(ephemeralStorageSize); - return this; - } - - /** - * Event sources for this function. - *

- * You can also add event sources using addEventSource. - *

- * Default: - No event sources. - *

- * - * @param events Event sources for this function. This parameter is required. - * @return {@code this} - */ - public CustomRuntime2Function.Builder events(final List events) { - this.props.events(events); - return this; - } - - /** - * The filesystem configuration for the lambda function. - *

- * Default: - will not mount any filesystem - *

- * - * @param filesystem The filesystem configuration for the lambda function. This parameter is - * required. - * @return {@code this} - */ - public CustomRuntime2Function.Builder filesystem(final FileSystem filesystem) { - this.props.filesystem(filesystem); - return this; - } - - /** - * A name for the function. - *

- * Default: - AWS CloudFormation generates a unique physical ID and uses that ID for the - * function's name. For more information, see Name Type. - *

- * - * @param functionName A name for the function. This parameter is required. - * @return {@code this} - */ - public CustomRuntime2Function.Builder functionName(final String functionName) { - this.props.functionName(functionName); - return this; - } - - /** - * Initial policy statements to add to the created Lambda Role. - *

- * You can call addToRolePolicy to the created lambda to add statements post - * creation. - *

- * Default: - No policy statements are added to the created Lambda role. - *

- * - * @param initialPolicy Initial policy statements to add to the created Lambda Role. This - * parameter is required. - * @return {@code this} - */ - public CustomRuntime2Function.Builder initialPolicy( - final List initialPolicy) { - this.props.initialPolicy(initialPolicy); - return this; - } - - /** - * Specify the version of CloudWatch Lambda insights to use for monitoring. - *

- * Default: - No Lambda Insights - *

- * - * @param insightsVersion Specify the version of CloudWatch Lambda insights to use for - * monitoring. This parameter is required. - * @return {@code this} - * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Lambda-Insights-Getting-Started-docker.html - */ - public CustomRuntime2Function.Builder insightsVersion( - final LambdaInsightsVersion insightsVersion) { - this.props.insightsVersion(insightsVersion); - return this; - } - - /** - * A list of layers to add to the function's execution environment. - *

- * You can configure your Lambda function to pull in additional code during initialization in - * the form of layers. Layers are packages of libraries or other dependencies that can be used - * by multiple functions. - *

- * Default: - No layers. - *

- * - * @param layers A list of layers to add to the function's execution environment. This parameter - * is required. - * @return {@code this} - */ - public CustomRuntime2Function.Builder layers(final List layers) { - this.props.layers(layers); - return this; - } - - /** - * The number of days log events are kept in CloudWatch Logs. - *

- * When updating this property, unsetting it doesn't remove the log retention policy. To remove - * the retention policy, set the value to - * INFINITE. - *

- * Default: logs.RetentionDays.INFINITE - *

- * - * @param logRetention The number of days log events are kept in CloudWatch Logs. This parameter - * is required. - * @return {@code this} - */ - public CustomRuntime2Function.Builder logRetention(final RetentionDays logRetention) { - this.props.logRetention(logRetention); - return this; - } - - /** - * When log retention is specified, a custom resource attempts to create the CloudWatch log - * group. - *

- * These options control the retry policy when interacting with CloudWatch APIs. - *

- * Default: - Default AWS SDK retry options. - *

- * - * @return {@code this} - */ - public Builder logRetentionRetryOptions() { - return logRetentionRetryOptions(null); - } - - /** - * When log retention is specified, a custom resource attempts to create the CloudWatch log - * group. - *

- * These options control the retry policy when interacting with CloudWatch APIs. - *

- * Default: - Default AWS SDK retry options. - *

- * - * @param logRetentionRetryOptions When log retention is specified, a custom resource attempts - * to create the CloudWatch log group. This parameter is - * required. - * @return {@code this} - */ - public CustomRuntime2Function.Builder logRetentionRetryOptions( - final LogRetentionRetryOptions logRetentionRetryOptions) { - this.props.logRetentionRetryOptions(logRetentionRetryOptions); - return this; - } - - /** - * The IAM role for the Lambda function associated with the custom resource that sets the - * retention policy. - *

- * Default: - A new role is created. - *

- * - * @param logRetentionRole The IAM role for the Lambda function associated with the custom - * resource that sets the retention policy. This parameter is required. - * @return {@code this} - */ - public CustomRuntime2Function.Builder logRetentionRole(final IRole logRetentionRole) { - this.props.logRetentionRole(logRetentionRole); - return this; - } - - /** - * The amount of memory, in MB, that is allocated to your Lambda function. - *

- * Lambda uses this value to proportionally allocate the amount of CPU power. For more - * information, see Resource Model in the AWS Lambda Developer Guide. - *

- * Default: 128 - *

- * - * @param memorySize The amount of memory, in MB, that is allocated to your Lambda function. - * This parameter is required. - * @return {@code this} - */ - public CustomRuntime2Function.Builder memorySize(final Number memorySize) { - this.props.memorySize(memorySize == null ? FUNCTION_DEFAULT_MEMORY_SIZE : memorySize); - return this; - } - - /** - * Enable profiling. - *

- * Default: - No profiling. - *

- * - * @param profiling Enable profiling. This parameter is required. - * @return {@code this} - * @see https://docs.aws.amazon.com/codeguru/latest/profiler-ug/setting-up-lambda.html - */ - public CustomRuntime2Function.Builder profiling(final Boolean profiling) { - this.props.profiling(profiling); - return this; - } - - /** - * Profiling Group. - *

- * Default: - A new profiling group will be created if `profiling` is set. - *

- * - * @param profilingGroup Profiling Group. This parameter is required. - * @return {@code this} - * @see https://docs.aws.amazon.com/codeguru/latest/profiler-ug/setting-up-lambda.html - */ - public CustomRuntime2Function.Builder profilingGroup(final IProfilingGroup profilingGroup) { - this.props.profilingGroup(profilingGroup); - return this; - } - - /** - * The maximum of concurrent executions you want to reserve for the function. - *

- * Default: - No specific limit - account limit. - *

- * - * @param reservedConcurrentExecutions The maximum of concurrent executions you want to reserve - * for the function. This parameter is required. - * @return {@code this} - * @see https://docs.aws.amazon.com/lambda/latest/dg/concurrent-executions.html - */ - public CustomRuntime2Function.Builder reservedConcurrentExecutions( - final Number reservedConcurrentExecutions) { - this.props.reservedConcurrentExecutions(reservedConcurrentExecutions); - return this; - } - - /** - * Lambda execution role. - *

- * This is the role that will be assumed by the function upon execution. It controls the - * permissions that the function will have. The Role must be assumable by the - * 'lambda.amazonaws.com' service principal. - *

- * The default Role automatically has permissions granted for Lambda execution. If you provide a - * Role, you must add the relevant AWS managed policies yourself. - *

- * The relevant managed policies are "service-role/AWSLambdaBasicExecutionRole" and - * "service-role/AWSLambdaVPCAccessExecutionRole". - *

- * Default: - A unique role will be generated for this lambda function. Both supplied and - * generated roles can always be changed by calling `addToRolePolicy`. - *

- * - * @param role Lambda execution role. This parameter is required. - * @return {@code this} - */ - public CustomRuntime2Function.Builder role(final IRole role) { - this.props.role(role); - return this; - } - - /** - * The list of security groups to associate with the Lambda's network interfaces. - *

- * Only used if 'vpc' is supplied. - *

- * Default: - If the function is placed within a VPC and a security group is not specified, - * either by this or securityGroup prop, a dedicated security group will be created for this - * function. - *

- * - * @param securityGroups The list of security groups to associate with the Lambda's network - * interfaces. This parameter is required. - * @return {@code this} - */ - public CustomRuntime2Function.Builder securityGroups( - final List securityGroups) { - this.props.securityGroups(securityGroups); - return this; - } - - /** - * The function execution time (in seconds) after which Lambda terminates the function. - *

- * Because the execution time affects cost, set this value based on the function's expected - * execution time. - *

- * Default: Duration.seconds(3) - *

- * - * @param timeout The function execution time (in seconds) after which Lambda terminates the - * function. This parameter is required. - * @return {@code this} - */ - public CustomRuntime2Function.Builder timeout(final Duration timeout) { - this.props.timeout( - timeout == null ? Duration.seconds(FUNCTION_DEFAULT_TIMEOUT_IN_SECONDS) : timeout); - return this; - } - - /** - * Enable AWS X-Ray Tracing for Lambda Function. - *

- * Default: Tracing.Disabled - *

- * - * @param tracing Enable AWS X-Ray Tracing for Lambda Function. This parameter is required. - * @return {@code this} - */ - public CustomRuntime2Function.Builder tracing(final Tracing tracing) { - this.props.tracing(tracing); - return this; - } - - /** - * VPC network to place Lambda network interfaces. - *

- * Specify this if the Lambda function needs to access resources in a VPC. This is required when - * vpcSubnets is specified. - *

- * Default: - Function is not placed within a VPC. - *

- * - * @param vpc VPC network to place Lambda network interfaces. This parameter is required. - * @return {@code this} - */ - public CustomRuntime2Function.Builder vpc(final IVpc vpc) { - this.props.vpc(vpc); - return this; - } - - /** - * Where to place the network interfaces within the VPC. - *

- * This requires vpc to be specified in order for interfaces to actually be placed - * in the subnets. If vpc is not specify, this will raise an error. - *

- * Note: Internet access for Lambda Functions requires a NAT Gateway, so picking public subnets - * is not allowed (unless - * allowPublicSubnet is set to true). - *

- * Default: - the Vpc default strategy if not specified - *

- * - * @param vpcSubnets Where to place the network interfaces within the VPC. This parameter is - * required. - * @return {@code this} - */ - public CustomRuntime2Function.Builder vpcSubnets(final SubnetSelection vpcSubnets) { - this.props.vpcSubnets(vpcSubnets); - return this; - } - - /** - * The source code of your Lambda function. - *

- * You can point to a file in an Amazon Simple Storage Service (Amazon S3) bucket or specify - * your source code as inline text. - *

- * - * @param code The source code of your Lambda function. This parameter is required. - * @return {@code this} - */ - public CustomRuntime2Function.Builder code(final Code code) { - this.props.code(code); - return this; - } - - /** - * The name of the method within your code that Lambda calls to execute your function. - *

- * The format includes the file name. It can also include namespaces and other qualifiers, - * depending on the runtime. For more information, see - * foundation-progmodel. - *

- * Use Handler.FROM_IMAGE when defining a function from a Docker image. - *

- * NOTE: If you specify your source code as inline text by specifying the ZipFile property - * within the Code property, specify index.function_name as the handler. - *

- * - * @param handler The name of the method within your code that Lambda calls to execute your - * function. This parameter is required. - * @return {@code this} - */ - public CustomRuntime2Function.Builder handler(final String handler) { - this.props.handler(handler); - return this; - } - - /** - * @return a newly built instance of {@link CustomRuntime2Function}. - */ - @Override - public CustomRuntime2Function build() { - final FunctionProps functionProps = this.props.build(); - - checkArgument(functionProps.getCode() != null, "'code' is required"); - checkArgument(isNoneBlank(functionProps.getHandler()), "'handler' is required"); - - checkArgument(functionProps.getTimeout() != null, "'timeout' is required"); - checkArgument(isNoneBlank(functionProps.getDescription()), "'description' is required"); - checkArgument(isNotEmpty(functionProps.getEnvironment()), "'environment' is required"); - - checkArgument(functionProps.getMemorySize() != null, "'memorySize' is required"); - checkArgument(functionProps.getMemorySize().intValue() >= 128 - && functionProps.getMemorySize().intValue() <= 3008, - "'memorySize' must be between 128 and 3008 (inclusive)"); - - checkArgument(functionProps.getRetryAttempts() != null, "'retryAttempts' is required"); - checkArgument(functionProps.getRetryAttempts().intValue() >= 0 - && functionProps.getRetryAttempts().intValue() <= 2, - "'retryAttempts' must be between 0 and 2 (inclusive)"); - - return new CustomRuntime2Function(this.scope, this.id, functionProps); - } - } -} diff --git a/spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/LambdaTest.java b/spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/LambdaTest.java index dd13f8b..d89ccc0 100644 --- a/spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/LambdaTest.java +++ b/spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/LambdaTest.java @@ -18,227 +18,228 @@ package com.coffeebeans.springnativeawslambda.infra; -import static cloud.pianola.cdk.fluent.assertion.CDKStackAssert.*; -import static com.coffeebeans.springnativeawslambda.infra.TagUtils.TAG_VALUE_COST_CENTRE; -import static software.amazon.awscdk.assertions.Match.exact; -import static software.amazon.awscdk.assertions.Match.stringLikeRegexp; - import com.fasterxml.jackson.core.JsonProcessingException; +import org.junit.jupiter.api.Test; + import java.util.List; import java.util.Map; -import org.junit.jupiter.api.Test; + +import static cloud.pianola.cdk.fluent.assertion.CDKStackAssert.assertThat; +import static com.coffeebeans.springnativeawslambda.infra.Constants.KEY_COST_CENTRE; +import static software.amazon.awscdk.assertions.Match.exact; +import static software.amazon.awscdk.assertions.Match.stringLikeRegexp; class LambdaTest extends TemplateSupport { - public static final String TEST = "test"; - - @Test - void should_have_lambda_function() { - - assertThat(template) - .containsFunction("spring-native-aws-lambda-function") - .hasHandler("org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest") - .hasCode("test-cdk-bucket", "(.*).zip") - .hasRole("springnativeawslambdafunctionrole(.*)") - .hasDependency("springnativeawslambdafunctionrole(.*)") - .hasDependency("springnativeawslambdafunctionroleDefaultPolicy(.*)") - .hasTag("COST_CENTRE", TAG_VALUE_COST_CENTRE) - .hasTag("ENV", TEST) - .hasEnvironmentVariable("ENV", TEST) - .hasEnvironmentVariable("SPRING_PROFILES_ACTIVE", TEST) - .hasDescription("Lambda example with spring native") - .hasMemorySize(512) - .hasRuntime("provided.al2023") - .hasTimeout(3); - } - - @Test - void should_have_role_with_AWSLambdaBasicExecutionRole_policy_to_assume_by_lambda() { - final String principal = "lambda.amazonaws.com"; - final String effect = "Allow"; - final String policyDocumentVersion = "2012-10-17"; - final String managedPolicyArn = ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"; - - assertThat(template) - .containsRoleWithManagedPolicyArn(managedPolicyArn) - .hasAssumeRolePolicyDocument(principal, null, effect, policyDocumentVersion, - "sts:AssumeRole"); - } - - @Test - void should_have_default_policy_to_allow_lambda_publish_to_sns() throws JsonProcessingException { - - final String policyName = "springnativeawslambdafunctionroleDefaultPolicy(.*)"; - final String deadLetterTopic = "springnativeawslambdafunctiondeadlettertopic(.*)"; - final String action = "sns:Publish"; - final String effect = "Allow"; - final String policyDocumentVersion = "2012-10-17"; - - assertThat(template) - .containsPolicy(policyName) - .isAssociatedWithRole("springnativeawslambdafunctionrole(.*)") - .hasPolicyDocumentVersion(policyDocumentVersion) - .hasPolicyDocumentStatement(null, - deadLetterTopic, - action, - effect, - policyDocumentVersion); - } - - @Test - void should_have_event_invoke_config_for_success_and_failure() { - - final String functionName = "springnativeawslambdafunction(.*)"; - - assertThat(template) - .containsLambdaEventInvokeConfig(functionName) - .hasLambdaEventInvokeConfigQualifier("$LATEST") - .hasLambdaEventInvokeConfigMaximumRetryAttempts(2); - } - - @Test - void should_have_permission_to_allow_rest_api_root_call_lambda() { - - final List sourceArn = List.of( - "arn:", - Map.of("Ref", exact("AWS::Partition")), - ":execute-api:", - Map.of("Ref", exact("AWS::Region")), - ":", - Map.of("Ref", exact("AWS::AccountId")), - ":", - Map.of("Ref", stringLikeRegexp("springnativeawslambdafunctionrestapi(.*)")), - "/", - Map.of("Ref", stringLikeRegexp("springnativeawslambdafunctionrestapi(.*)")), - "/*/" - ); - - final String action = "lambda:InvokeFunction"; - final String principal = "apigateway.amazonaws.com"; - final String functionName = "springnativeawslambdafunction(.*)"; - - assertThat(template) - .containsLambdaPermission(functionName, action, principal, sourceArn); - } - - @Test - void should_have_permission_to_allow_rest_api_root_test_call_lambda() { - - final List sourceArn = List.of( - "arn:", - Map.of("Ref", exact("AWS::Partition")), - ":execute-api:", - Map.of("Ref", exact("AWS::Region")), - ":", - Map.of("Ref", exact("AWS::AccountId")), - ":", - Map.of("Ref", stringLikeRegexp("springnativeawslambdafunctionrestapi(.*)")), - "/test-invoke-stage/*/*" - ); - - final String action = "lambda:InvokeFunction"; - final String principal = "apigateway.amazonaws.com"; - final String functionName = "springnativeawslambdafunction(.*)"; - - assertThat(template) - .containsLambdaPermission(functionName, action, principal, sourceArn); - } - - @Test - void should_have_permission_to_allow_rest_api_proxy_to_call_lambda() { - - final List sourceArn = List.of( - "arn:", - Map.of("Ref", exact("AWS::Partition")), - ":execute-api:", - Map.of("Ref", exact("AWS::Region")), - ":", - Map.of("Ref", exact("AWS::AccountId")), - ":", - Map.of("Ref", stringLikeRegexp("springnativeawslambdafunctionrestapi(.*)")), - "/", - Map.of("Ref", - stringLikeRegexp("springnativeawslambdafunctionrestapiDeploymentStagetest(.*)")), - "/*/*" - ); - - final String action = "lambda:InvokeFunction"; - final String principal = "apigateway.amazonaws.com"; - final String functionName = "springnativeawslambdafunction(.*)"; - - assertThat(template) - .containsLambdaPermission(functionName, action, principal, sourceArn); - } - - @Test - void should_have_permission_to_allow_rest_api_proxy_test_to_call_lambda() { - - final List sourceArn = List.of( - "arn:", - Map.of("Ref", exact("AWS::Partition")), - ":execute-api:", - Map.of("Ref", exact("AWS::Region")), - ":", - Map.of("Ref", exact("AWS::AccountId")), - ":", - Map.of("Ref", stringLikeRegexp("springnativeawslambdafunctionrestapi(.*)")), - "/test-invoke-stage/*/*" - ); - - final String action = "lambda:InvokeFunction"; - final String principal = "apigateway.amazonaws.com"; - final String functionName = "springnativeawslambdafunction(.*)"; - - assertThat(template) - .containsLambdaPermission(functionName, action, principal, sourceArn); - } - - @Test - void should_have_permission_to_allow_post_rest_api_method_to_call_lambda() { - - final List sourceArn = List.of( - "arn:", - Map.of("Ref", exact("AWS::Partition")), - ":execute-api:", - Map.of("Ref", exact("AWS::Region")), - ":", - Map.of("Ref", exact("AWS::AccountId")), - ":", - Map.of("Ref", stringLikeRegexp("springnativeawslambdafunctionrestapi(.*)")), - "/", - Map.of("Ref", - stringLikeRegexp("springnativeawslambdafunctionrestapiDeploymentStagetest(.*)")), - "/POST/name" - ); - - final String action = "lambda:InvokeFunction"; - final String principal = "apigateway.amazonaws.com"; - final String functionName = "springnativeawslambdafunction(.*)"; - - assertThat(template) - .containsLambdaPermission(functionName, action, principal, sourceArn); - } - - @Test - void should_have_permission_to_allow_post_rest_api_method_test_to_call_lambda() { - - final List sourceArn = List.of( - "arn:", - Map.of("Ref", exact("AWS::Partition")), - ":execute-api:", - Map.of("Ref", exact("AWS::Region")), - ":", - Map.of("Ref", exact("AWS::AccountId")), - ":", - Map.of("Ref", stringLikeRegexp("springnativeawslambdafunctionrestapi(.*)")), - "/test-invoke-stage/POST/name" - ); - - final String action = "lambda:InvokeFunction"; - final String principal = "apigateway.amazonaws.com"; - final String functionName = "springnativeawslambdafunction(.*)"; - - assertThat(template) - .containsLambdaPermission(functionName, action, principal, sourceArn); - } + public static final String TEST = "test"; + + @Test + void should_have_lambda_function() { + + assertThat(template) + .containsFunction("spring-native-aws-lambda-function") + .hasHandler("org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest") + .hasCode("test-cdk-bucket", "(.*).zip") + .hasRole("springnativeawslambdafunctionrole(.*)") + .hasDependency("springnativeawslambdafunctionrole(.*)") + .hasDependency("springnativeawslambdafunctionroleDefaultPolicy(.*)") + .hasTag("COST_CENTRE", KEY_COST_CENTRE) + .hasTag("ENV", TEST) + .hasEnvironmentVariable("ENV", TEST) + .hasEnvironmentVariable("SPRING_PROFILES_ACTIVE", TEST) + .hasDescription("Lambda example with spring native") + .hasMemorySize(512) + .hasRuntime("provided.al2023") + .hasTimeout(3); + } + + @Test + void should_have_role_with_AWSLambdaBasicExecutionRole_policy_to_assume_by_lambda() { + final String principal = "lambda.amazonaws.com"; + final String effect = "Allow"; + final String policyDocumentVersion = "2012-10-17"; + final String managedPolicyArn = ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"; + + assertThat(template) + .containsRoleWithManagedPolicyArn(managedPolicyArn) + .hasAssumeRolePolicyDocument(principal, null, effect, policyDocumentVersion, + "sts:AssumeRole"); + } + + @Test + void should_have_default_policy_to_allow_lambda_publish_to_sns() throws JsonProcessingException { + + final String policyName = "springnativeawslambdafunctionroleDefaultPolicy(.*)"; + final String deadLetterTopic = "springnativeawslambdafunctiondeadlettertopic(.*)"; + final String action = "sns:Publish"; + final String effect = "Allow"; + final String policyDocumentVersion = "2012-10-17"; + + assertThat(template) + .containsPolicy(policyName) + .isAssociatedWithRole("springnativeawslambdafunctionrole(.*)") + .hasPolicyDocumentVersion(policyDocumentVersion) + .hasPolicyDocumentStatement(null, + deadLetterTopic, + action, + effect, + policyDocumentVersion); + } + + @Test + void should_have_event_invoke_config_for_success_and_failure() { + + final String functionName = "springnativeawslambdafunction(.*)"; + + assertThat(template) + .containsLambdaEventInvokeConfig(functionName) + .hasLambdaEventInvokeConfigQualifier("$LATEST") + .hasLambdaEventInvokeConfigMaximumRetryAttempts(2); + } + + @Test + void should_have_permission_to_allow_rest_api_root_call_lambda() { + + final List sourceArn = List.of( + "arn:", + Map.of("Ref", exact("AWS::Partition")), + ":execute-api:", + Map.of("Ref", exact("AWS::Region")), + ":", + Map.of("Ref", exact("AWS::AccountId")), + ":", + Map.of("Ref", stringLikeRegexp("springnativeawslambdafunctionrestapi(.*)")), + "/", + Map.of("Ref", stringLikeRegexp("springnativeawslambdafunctionrestapi(.*)")), + "/*/" + ); + + final String action = "lambda:InvokeFunction"; + final String principal = "apigateway.amazonaws.com"; + final String functionName = "springnativeawslambdafunction(.*)"; + + assertThat(template) + .containsLambdaPermission(functionName, action, principal, sourceArn); + } + + @Test + void should_have_permission_to_allow_rest_api_root_test_call_lambda() { + + final List sourceArn = List.of( + "arn:", + Map.of("Ref", exact("AWS::Partition")), + ":execute-api:", + Map.of("Ref", exact("AWS::Region")), + ":", + Map.of("Ref", exact("AWS::AccountId")), + ":", + Map.of("Ref", stringLikeRegexp("springnativeawslambdafunctionrestapi(.*)")), + "/test-invoke-stage/*/*" + ); + + final String action = "lambda:InvokeFunction"; + final String principal = "apigateway.amazonaws.com"; + final String functionName = "springnativeawslambdafunction(.*)"; + + assertThat(template) + .containsLambdaPermission(functionName, action, principal, sourceArn); + } + + @Test + void should_have_permission_to_allow_rest_api_proxy_to_call_lambda() { + + final List sourceArn = List.of( + "arn:", + Map.of("Ref", exact("AWS::Partition")), + ":execute-api:", + Map.of("Ref", exact("AWS::Region")), + ":", + Map.of("Ref", exact("AWS::AccountId")), + ":", + Map.of("Ref", stringLikeRegexp("springnativeawslambdafunctionrestapi(.*)")), + "/", + Map.of("Ref", + stringLikeRegexp("springnativeawslambdafunctionrestapiDeploymentStagetest(.*)")), + "/*/*" + ); + + final String action = "lambda:InvokeFunction"; + final String principal = "apigateway.amazonaws.com"; + final String functionName = "springnativeawslambdafunction(.*)"; + + assertThat(template) + .containsLambdaPermission(functionName, action, principal, sourceArn); + } + + @Test + void should_have_permission_to_allow_rest_api_proxy_test_to_call_lambda() { + + final List sourceArn = List.of( + "arn:", + Map.of("Ref", exact("AWS::Partition")), + ":execute-api:", + Map.of("Ref", exact("AWS::Region")), + ":", + Map.of("Ref", exact("AWS::AccountId")), + ":", + Map.of("Ref", stringLikeRegexp("springnativeawslambdafunctionrestapi(.*)")), + "/test-invoke-stage/*/*" + ); + + final String action = "lambda:InvokeFunction"; + final String principal = "apigateway.amazonaws.com"; + final String functionName = "springnativeawslambdafunction(.*)"; + + assertThat(template) + .containsLambdaPermission(functionName, action, principal, sourceArn); + } + + @Test + void should_have_permission_to_allow_post_rest_api_method_to_call_lambda() { + + final List sourceArn = List.of( + "arn:", + Map.of("Ref", exact("AWS::Partition")), + ":execute-api:", + Map.of("Ref", exact("AWS::Region")), + ":", + Map.of("Ref", exact("AWS::AccountId")), + ":", + Map.of("Ref", stringLikeRegexp("springnativeawslambdafunctionrestapi(.*)")), + "/", + Map.of("Ref", + stringLikeRegexp("springnativeawslambdafunctionrestapiDeploymentStagetest(.*)")), + "/POST/name" + ); + + final String action = "lambda:InvokeFunction"; + final String principal = "apigateway.amazonaws.com"; + final String functionName = "springnativeawslambdafunction(.*)"; + + assertThat(template) + .containsLambdaPermission(functionName, action, principal, sourceArn); + } + + @Test + void should_have_permission_to_allow_post_rest_api_method_test_to_call_lambda() { + + final List sourceArn = List.of( + "arn:", + Map.of("Ref", exact("AWS::Partition")), + ":execute-api:", + Map.of("Ref", exact("AWS::Region")), + ":", + Map.of("Ref", exact("AWS::AccountId")), + ":", + Map.of("Ref", stringLikeRegexp("springnativeawslambdafunctionrestapi(.*)")), + "/test-invoke-stage/POST/name" + ); + + final String action = "lambda:InvokeFunction"; + final String principal = "apigateway.amazonaws.com"; + final String functionName = "springnativeawslambdafunction(.*)"; + + assertThat(template) + .containsLambdaPermission(functionName, action, principal, sourceArn); + } } \ No newline at end of file diff --git a/spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/RestApiTest.java b/spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/RestApiTest.java index 2ca8896..cb21274 100644 --- a/spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/RestApiTest.java +++ b/spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/RestApiTest.java @@ -18,13 +18,14 @@ package com.coffeebeans.springnativeawslambda.infra; -import static com.coffeebeans.springnativeawslambda.infra.TagUtils.TAG_VALUE_COST_CENTRE; -import static cloud.pianola.cdk.fluent.assertion.CDKStackAssert.*; +import org.junit.jupiter.api.Test; +import software.amazon.awscdk.assertions.Match; import java.util.List; import java.util.Map; -import org.junit.jupiter.api.Test; -import software.amazon.awscdk.assertions.Match; + +import static cloud.pianola.cdk.fluent.assertion.CDKStackAssert.assertThat; +import static com.coffeebeans.springnativeawslambda.infra.Constants.KEY_COST_CENTRE; class RestApiTest extends TemplateSupport { @@ -35,7 +36,7 @@ void should_have_rest_api() { assertThat(template) .containsRestApi("spring-native-aws-lambda-function-rest-api") - .hasTag("COST_CENTRE", TAG_VALUE_COST_CENTRE) + .hasTag("COST_CENTRE", KEY_COST_CENTRE) .hasTag("ENV", TEST); } @@ -72,7 +73,7 @@ void should_have_rest_api_stage() { .hasRestApiId(("springnativeawslambdafunctionrestapi(.*)")) .hasDeploymentId(("springnativeawslambdafunctionrestapiDeployment(.*)")) .hasDependency("springnativeawslambdafunctionrestapiAccount(.*)") - .hasTag("COST_CENTRE", TAG_VALUE_COST_CENTRE) + .hasTag("COST_CENTRE", KEY_COST_CENTRE) .hasTag("ENV", TEST); } diff --git a/spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/TagUtilsTest.java b/spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/TagUtilsTest.java index d0628c1..a3fa472 100644 --- a/spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/TagUtilsTest.java +++ b/spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/TagUtilsTest.java @@ -18,10 +18,13 @@ package com.coffeebeans.springnativeawslambda.infra; -import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.Test; import java.util.Map; -import org.junit.jupiter.api.Test; + +import static com.coffeebeans.springnativeawslambda.infra.Constants.KEY_COST_CENTRE; +import static com.coffeebeans.springnativeawslambda.infra.Constants.KEY_ENV; +import static org.assertj.core.api.Assertions.assertThat; class TagUtilsTest { @@ -35,7 +38,7 @@ void should_create_and_return_tag_map() { final Map tags = TagUtils.createTags(env, costCentre); assertThat(tags) - .containsEntry(TagUtils.TAG_KEY_ENV, env) - .containsEntry(TagUtils.TAG_KEY_COST_CENTRE, costCentre); + .containsEntry(KEY_ENV, env) + .containsEntry(KEY_COST_CENTRE, costCentre); } } \ No newline at end of file diff --git a/spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/TemplateSupport.java b/spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/TemplateSupport.java index 126ea2e..349ff25 100644 --- a/spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/TemplateSupport.java +++ b/spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/TemplateSupport.java @@ -18,13 +18,6 @@ package com.coffeebeans.springnativeawslambda.infra; -import static com.coffeebeans.springnativeawslambda.infra.TagUtils.TAG_VALUE_COST_CENTRE; -import static com.coffeebeans.springnativeawslambda.infra.TagUtils.createTags; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.Map; -import java.util.Objects; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.io.TempDir; @@ -32,34 +25,42 @@ import software.amazon.awscdk.Tags; import software.amazon.awscdk.assertions.Template; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Map; +import java.util.Objects; + +import static com.coffeebeans.springnativeawslambda.infra.Constants.KEY_COST_CENTRE; +import static com.coffeebeans.springnativeawslambda.infra.TagUtils.createTags; + public abstract class TemplateSupport { - public static final String ENV = "test"; - public static final String TEST_CDK_BUCKET = "test-cdk-bucket"; - public static final String QUALIFIER = "test"; - static Template template; - private static final String STACK_NAME = "spring-native-aws-lambda-function-test-stack"; - @TempDir - private static Path TEMP_DIR; + public static final String ENV = "test"; + public static final String TEST_CDK_BUCKET = "test-cdk-bucket"; + public static final String QUALIFIER = "test"; + static Template template; + private static final String STACK_NAME = "spring-native-aws-lambda-function-test-stack"; + @TempDir + private static Path TEMP_DIR; - @BeforeAll - static void initAll() throws IOException { - final Path lambdaCodePath = TestLambdaUtils.getTestLambdaCodePath(TEMP_DIR); + @BeforeAll + static void initAll() throws IOException { + final Path lambdaCodePath = TestLambdaUtils.getTestLambdaCodePath(TEMP_DIR); - final Map tags = createTags(ENV, TAG_VALUE_COST_CENTRE); - final App app = new App(); - final SpringNativeAwsLambdaStack stack = StackUtils.createStack(app, STACK_NAME, - lambdaCodePath.toString(), QUALIFIER, TEST_CDK_BUCKET, ENV); + final Map tags = createTags(ENV, KEY_COST_CENTRE); + final App app = new App(); + final SpringNativeAwsLambdaStack stack = StackUtils.createStack(app, STACK_NAME, + lambdaCodePath.toString(), QUALIFIER, TEST_CDK_BUCKET, ENV); - tags.entrySet().stream() - .filter(tag -> Objects.nonNull(tag.getValue())) - .forEach(tag -> Tags.of(app).add(tag.getKey(), tag.getValue())); + tags.entrySet().stream() + .filter(tag -> Objects.nonNull(tag.getValue())) + .forEach(tag -> Tags.of(app).add(tag.getKey(), tag.getValue())); - template = Template.fromStack(stack); - } + template = Template.fromStack(stack); + } - @AfterAll - static void cleanup() { - template = null; - } + @AfterAll + static void cleanup() { + template = null; + } } diff --git a/spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/TopicTest.java b/spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/TopicTest.java index 431c1cd..401bc02 100644 --- a/spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/TopicTest.java +++ b/spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/TopicTest.java @@ -18,20 +18,20 @@ package com.coffeebeans.springnativeawslambda.infra; -import static com.coffeebeans.springnativeawslambda.infra.TagUtils.TAG_VALUE_COST_CENTRE; -import static cloud.pianola.cdk.fluent.assertion.CDKStackAssert.*; - import org.junit.jupiter.api.Test; +import static cloud.pianola.cdk.fluent.assertion.CDKStackAssert.assertThat; +import static com.coffeebeans.springnativeawslambda.infra.Constants.KEY_COST_CENTRE; + class TopicTest extends TemplateSupport { - public static final String TEST = "test"; + public static final String TEST = "test"; - @Test - void should_have_dead_letter_topic() { - assertThat(template) - .containsTopic("spring-native-aws-lambda-function-dead-letter-topic") - .hasTag("COST_CENTRE", TAG_VALUE_COST_CENTRE) - .hasTag("ENV", TEST); - } + @Test + void should_have_dead_letter_topic() { + assertThat(template) + .containsTopic("spring-native-aws-lambda-function-dead-letter-topic") + .hasTag("COST_CENTRE", KEY_COST_CENTRE) + .hasTag("ENV", TEST); + } } \ No newline at end of file diff --git a/spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/lambda/CustomRuntime2023FunctionTest.java b/spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/lambda/CustomRuntime2023FunctionTest.java new file mode 100644 index 0000000..29a9174 --- /dev/null +++ b/spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/lambda/CustomRuntime2023FunctionTest.java @@ -0,0 +1,268 @@ +package com.coffeebeans.springnativeawslambda.infra.lambda; + +import com.coffeebeans.springnativeawslambda.infra.TestLambdaUtils; +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import software.amazon.awscdk.App; +import software.amazon.awscdk.Duration; +import software.amazon.awscdk.Size; +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.services.codeguruprofiler.ProfilingGroup; +import software.amazon.awscdk.services.ec2.SecurityGroup; +import software.amazon.awscdk.services.ec2.SubnetSelection; +import software.amazon.awscdk.services.ec2.Vpc; +import software.amazon.awscdk.services.iam.PolicyStatement; +import software.amazon.awscdk.services.iam.Role; +import software.amazon.awscdk.services.kms.Key; +import software.amazon.awscdk.services.lambda.Code; +import software.amazon.awscdk.services.lambda.Function; +import software.amazon.awscdk.services.lambda.FunctionProps; +import software.amazon.awscdk.services.lambda.Tracing; +import software.amazon.awscdk.services.lambda.VersionOptions; +import software.amazon.awscdk.services.lambda.destinations.SnsDestination; +import software.amazon.awscdk.services.lambda.eventsources.ApiEventSource; +import software.amazon.awscdk.services.logs.RetentionDays; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static software.amazon.awscdk.services.lambda.Architecture.ARM_64; +import static software.amazon.awscdk.services.lambda.CodeSigningConfig.fromCodeSigningConfigArn; +import static software.amazon.awscdk.services.lambda.Runtime.PROVIDED_AL2; +import static software.amazon.awscdk.services.lambda.Runtime.PROVIDED_AL2023; +import static software.amazon.awscdk.services.sns.Topic.fromTopicArn; + +class CustomRuntime2023FunctionTest { + + public static final @NotNull Duration DEFAULT_TIMEOUT = Duration.seconds(10); + @TempDir + private static Path TEMP_DIR; + private Stack stack; + private String id; + private SnsDestination onFailure; + private SnsDestination onSuccess; + private Path lambdaCodePath; + private FunctionProps.Builder functionPropsBuilder; + + @BeforeEach + void setUp() throws IOException { + lambdaCodePath = TestLambdaUtils.getTestLambdaCodePath(TEMP_DIR); + + stack = new Stack(new App(), "test-stack"); + id = "test-function"; + + onFailure = new SnsDestination( + fromTopicArn(stack, "failure-topic", "arn:aws:sns:us-east-1:***:failure-topic")); + onSuccess = new SnsDestination( + fromTopicArn(stack, "success-topic", "arn:aws:sns:us-east-1:***:success-topic")); + + functionPropsBuilder = FunctionProps.builder() + .runtime(PROVIDED_AL2023) + .maxEventAge(Duration.seconds(514)) + .onFailure(onFailure) + .onSuccess(onSuccess) + .retryAttempts(2) + .allowPublicSubnet(false) + .architecture(ARM_64) + .codeSigningConfig(fromCodeSigningConfigArn(stack, "test-code-signing-config", + "arn:aws:lambda:us-east-1:***:code-signing-config:***")) + .currentVersionOptions(VersionOptions.builder().build()) + .deadLetterQueue(null) + .deadLetterQueueEnabled(false) + .deadLetterTopic(null) + .description("test function") + .environment(Map.of("Account", "***")) + .environmentEncryption( + Key.fromKeyArn(stack, "test-key", "arn:aws:kms:us-east-1:***:key/***")) + .ephemeralStorageSize(Size.gibibytes(1)) + .events(List.of(new ApiEventSource("POST", "/test"))) + .filesystem(null) + .functionName("test-function") + .initialPolicy(List.of(PolicyStatement.Builder.create().build())) + .insightsVersion(null) + .layers(Collections.emptyList()) + .logRetention(RetentionDays.FIVE_DAYS) + .logRetentionRole( + Role.fromRoleArn(stack, "test-log-role", "arn:aws:iam::***:role/test-log-role")) + .memorySize(512) + .profiling(false) + .profilingGroup(ProfilingGroup.fromProfilingGroupName(stack, "test-profiling-group", + "test-profiling-group")) + .reservedConcurrentExecutions(2) + .role(Role.fromRoleArn(stack, "test-role", "arn:aws:iam::***:role/test-role")) + .securityGroups( + List.of(SecurityGroup.fromSecurityGroupId(stack, "test-security-group", "sg-***"))) + .tracing(Tracing.ACTIVE) + .vpc(Vpc.Builder.create(stack, "test-vpc").build()) + .vpcSubnets(SubnetSelection.builder().build()) + .code(Code.fromAsset(lambdaCodePath.toString())) + .handler( + "com.coffeebeans.springnativeawslambda.infra.lambda.CustomRuntime2Function::handleRequest"); + } + + @Test + void should_create_and_return_function() { + final FunctionProps functionProps = functionPropsBuilder.build(); + final Function actual = new CustomRuntime2023Function(stack, id, functionProps).getFunction(); + + assertThat(actual) + .isNotNull(); + + assertThat(actual.getRuntime()) + .isEqualTo(PROVIDED_AL2023); + + assertThat(actual.getTimeout() + .toSeconds()) + .isEqualTo(DEFAULT_TIMEOUT.toSeconds()); + } + + @Test + void should_not_override_provided_al2023() { + final FunctionProps functionProps = functionPropsBuilder + .runtime(PROVIDED_AL2) + .build(); + final Function actual = new CustomRuntime2023Function(stack, id, functionProps).getFunction(); + + assertThat(actual) + .isNotNull(); + + assertThat(actual.getRuntime()) + .isEqualTo(PROVIDED_AL2023); + } + + @Test + void should_create_and_return_function_when_with_non_default_timeout() { + final Duration timeout = Duration.seconds(3); + final FunctionProps functionProps = functionPropsBuilder + .timeout(timeout) + .build(); + final Function actual = new CustomRuntime2023Function(stack, id, functionProps).getFunction(); + + assertThat(actual) + .isNotNull(); + + assertThat(actual.getTimeout() + .toSeconds()) + .isEqualTo(3); + } + + @Test + void should_throw_exception_when_function_handler_is_empty_string() { + final FunctionProps functionProps = functionPropsBuilder + .handler(StringUtils.EMPTY) + .build(); + + assertThatThrownBy(() -> new CustomRuntime2023Function(stack, id, functionProps)) + .isNotNull() + .isInstanceOf(IllegalArgumentException.class) + .hasFieldOrPropertyWithValue("message", "'handler' is required"); + } + + @Test + void should_throw_exception_when_function_description_is_missing() { + final FunctionProps functionProps = functionPropsBuilder + .description(null) + .build(); + + assertThatThrownBy(() -> new CustomRuntime2023Function(stack, id, functionProps)) + .isNotNull() + .isInstanceOf(IllegalArgumentException.class) + .hasFieldOrPropertyWithValue("message", "'description' is required"); + } + + @Test + void should_throw_exception_when_function_description_is_empty_string() { + final FunctionProps functionProps = functionPropsBuilder + .description(StringUtils.EMPTY) + .build(); + + assertThatThrownBy(() -> new CustomRuntime2023Function(stack, id, functionProps)) + .isNotNull() + .isInstanceOf(IllegalArgumentException.class) + .hasFieldOrPropertyWithValue("message", "'description' is required"); + } + + @Test + void should_throw_exception_when_function_environment_is_empty_map() { + final FunctionProps functionProps = functionPropsBuilder + .environment(Collections.emptyMap()) + .build(); + + assertThatThrownBy(() -> new CustomRuntime2023Function(stack, id, functionProps)) + .isNotNull() + .isInstanceOf(IllegalArgumentException.class) + .hasFieldOrPropertyWithValue("message", "'environment' is required"); + } + + @Test + void should_throw_exception_when_function_environment_is_null() { + final FunctionProps functionProps = functionPropsBuilder + .environment(null) + .build(); + + assertThatThrownBy(() -> new CustomRuntime2023Function(stack, id, functionProps)) + .isNotNull() + .isInstanceOf(IllegalArgumentException.class) + .hasFieldOrPropertyWithValue("message", "'environment' is required"); + } + + @Test + void should_throw_exception_when_function_memory_size_is_less_than_128() { + final FunctionProps functionProps = functionPropsBuilder + .memorySize(120) + .build(); + + assertThatThrownBy(() -> new CustomRuntime2023Function(stack, id, functionProps)) + .isNotNull() + .isInstanceOf(IllegalArgumentException.class) + .hasFieldOrPropertyWithValue("message", + "'memorySize' must be between 128 and 3008 (inclusive)"); + } + + @Test + void should_throw_exception_when_function_memory_size_is_larger_than_3008() { + final FunctionProps functionProps = functionPropsBuilder + .memorySize(3500) + .build(); + + assertThatThrownBy(() -> new CustomRuntime2023Function(stack, id, functionProps)) + .isNotNull() + .isInstanceOf(IllegalArgumentException.class) + .hasFieldOrPropertyWithValue("message", + "'memorySize' must be between 128 and 3008 (inclusive)"); + } + + @Test + void should_throw_exception_when_function_memory_size_is_less_than_zero() { + final FunctionProps functionProps = functionPropsBuilder + .retryAttempts(-1) + .build(); + + assertThatThrownBy(() -> new CustomRuntime2023Function(stack, id, functionProps)) + .isNotNull() + .isInstanceOf(IllegalArgumentException.class) + .hasFieldOrPropertyWithValue("message", + "'retryAttempts' must be between 0 and 2 (inclusive)"); + } + + @Test + void should_throw_exception_when_function_memory_size_is_larger_than_two() { + final FunctionProps functionProps = functionPropsBuilder + .retryAttempts(3) + .build(); + + assertThatThrownBy(() -> new CustomRuntime2023Function(stack, id, functionProps)) + .isNotNull() + .isInstanceOf(IllegalArgumentException.class) + .hasFieldOrPropertyWithValue("message", + "'retryAttempts' must be between 0 and 2 (inclusive)"); + } +} \ No newline at end of file diff --git a/spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/lambda/CustomRuntime2FunctionTest.java b/spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/lambda/CustomRuntime2FunctionTest.java deleted file mode 100644 index b06efc2..0000000 --- a/spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/lambda/CustomRuntime2FunctionTest.java +++ /dev/null @@ -1,269 +0,0 @@ -/* - * Licensed to Muhammad Hamadto - * - * 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 - * - * See the NOTICE file distributed with this work for additional information regarding copyright ownership. - * - * 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.coffeebeans.springnativeawslambda.infra.lambda; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static software.amazon.awscdk.services.lambda.Architecture.ARM_64; -import static software.amazon.awscdk.services.lambda.CodeSigningConfig.fromCodeSigningConfigArn; -import static software.amazon.awscdk.services.lambda.Runtime.PROVIDED_AL2023; -import static software.amazon.awscdk.services.sns.Topic.fromTopicArn; - -import com.coffeebeans.springnativeawslambda.infra.TestLambdaUtils; -import com.coffeebeans.springnativeawslambda.infra.lambda.CustomRuntime2Function.Builder; -import java.io.IOException; -import java.nio.file.Path; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import org.apache.commons.lang3.StringUtils; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; -import software.amazon.awscdk.App; -import software.amazon.awscdk.Duration; -import software.amazon.awscdk.Size; -import software.amazon.awscdk.Stack; -import software.amazon.awscdk.services.codeguruprofiler.ProfilingGroup; -import software.amazon.awscdk.services.ec2.SecurityGroup; -import software.amazon.awscdk.services.ec2.SubnetSelection; -import software.amazon.awscdk.services.ec2.Vpc; -import software.amazon.awscdk.services.iam.PolicyStatement; -import software.amazon.awscdk.services.iam.Role; -import software.amazon.awscdk.services.kms.Key; -import software.amazon.awscdk.services.lambda.Code; -import software.amazon.awscdk.services.lambda.Tracing; -import software.amazon.awscdk.services.lambda.VersionOptions; -import software.amazon.awscdk.services.lambda.destinations.SnsDestination; -import software.amazon.awscdk.services.lambda.eventsources.ApiEventSource; -import software.amazon.awscdk.services.logs.RetentionDays; - -class CustomRuntime2FunctionTest { - - public static final @NotNull Duration DEFAULT_TIMEOUT = Duration.seconds(10); - @TempDir - private static Path TEMP_DIR; - private Builder customRuntime2FunctionBuilder; - private Stack stack; - private String id; - private SnsDestination onFailure; - private SnsDestination onSuccess; - private Path lambdaCodePath; - - @BeforeEach - void setUp() throws IOException { - lambdaCodePath = TestLambdaUtils.getTestLambdaCodePath(TEMP_DIR); - - stack = new Stack(new App(), "test-stack"); - id = "test-function"; - - onFailure = new SnsDestination( - fromTopicArn(stack, "failure-topic", "arn:aws:sns:us-east-1:***:failure-topic")); - onSuccess = new SnsDestination( - fromTopicArn(stack, "success-topic", "arn:aws:sns:us-east-1:***:success-topic")); - - customRuntime2FunctionBuilder = Builder.create(stack, id) - .maxEventAge(Duration.seconds(514)) - .onFailure(onFailure) - .onSuccess(onSuccess) - .retryAttempts(2) - // .allowAllOutbound(true) - .allowPublicSubnet(false) - .architecture(ARM_64) - .codeSigningConfig(fromCodeSigningConfigArn(stack, "test-code-signing-config", - "arn:aws:lambda:us-east-1:***:code-signing-config:***")) - .currentVersionOptions(VersionOptions.builder().build()) - .deadLetterQueue(null) - .deadLetterQueueEnabled(false) - .deadLetterTopic(null) - .description("test function") - .environment(Map.of("Account", "***")) - .environmentEncryption( - Key.fromKeyArn(stack, "test-key", "arn:aws:kms:us-east-1:***:key/***")) - .ephemeralStorageSize(Size.gibibytes(1)) - .events(List.of(new ApiEventSource("POST", "/test"))) - .filesystem(null) - .functionName("test-function") - .initialPolicy(List.of(PolicyStatement.Builder.create().build())) - .insightsVersion(null) - .layers(Collections.emptyList()) - .logRetention(RetentionDays.FIVE_DAYS) - .logRetentionRetryOptions() - .logRetentionRole( - Role.fromRoleArn(stack, "test-log-role", "arn:aws:iam::***:role/test-log-role")) - .memorySize(512) - .timeout(Duration.seconds(3)) - .profiling(false) - .profilingGroup(ProfilingGroup.fromProfilingGroupName(stack, "test-profiling-group", - "test-profiling-group")) - .reservedConcurrentExecutions(2) - .role(Role.fromRoleArn(stack, "test-role", "arn:aws:iam::***:role/test-role")) - .securityGroups( - List.of(SecurityGroup.fromSecurityGroupId(stack, "test-security-group", "sg-***"))) - .tracing(Tracing.ACTIVE) - .vpc(Vpc.Builder.create(stack, "test-vpc").build()) - .vpcSubnets(SubnetSelection.builder().build()) - .code(Code.fromAsset(lambdaCodePath.toString())) - .handler( - "com.coffeebeans.springnativeawslambda.infra.lambda.CustomRuntime2Function::handleRequest"); - } - - @Test - void should_create_and_return_function() { - final Duration timeout = Duration.seconds(3); - - final CustomRuntime2Function actual = customRuntime2FunctionBuilder - .timeout(timeout) - .build(); - - assertThat(actual) - .isNotNull(); - - assertThat(actual.getRuntime()) - .isEqualTo(PROVIDED_AL2023); - - assertThat(actual.getTimeout()) - .isEqualTo(timeout); - } - - @Test - void should_create_and_return_function_when_with_default_timeout() { - final CustomRuntime2Function actual = customRuntime2FunctionBuilder - .timeout(null) - .build(); - - assertThat(actual) - .isNotNull(); - - assertThat(actual.getTimeout() - .toSeconds()) // actual.getTimeout() won't be null Builder#timeout() has a default value of 10 seconds - .isEqualTo(DEFAULT_TIMEOUT.toSeconds()); - } - - @Test - void should_throw_exception_when_function_code_is_missing() { - customRuntime2FunctionBuilder.code(null); - - assertThatThrownBy(() -> customRuntime2FunctionBuilder.build()) - .isNotNull() - .isInstanceOf(NullPointerException.class) - .hasFieldOrPropertyWithValue("message", "code is required"); - } - - @Test - void should_throw_exception_when_function_handler_is_missing() { - customRuntime2FunctionBuilder.handler(null); - - assertThatThrownBy(() -> customRuntime2FunctionBuilder.build()) - .isNotNull() - .isInstanceOf(NullPointerException.class) - .hasFieldOrPropertyWithValue("message", "handler is required"); - } - - @Test - void should_throw_exception_when_function_handler_is_empty_string() { - customRuntime2FunctionBuilder.handler(StringUtils.EMPTY); - - assertThatThrownBy(() -> customRuntime2FunctionBuilder.build()) - .isNotNull() - .isInstanceOf(IllegalArgumentException.class) - .hasFieldOrPropertyWithValue("message", "'handler' is required"); - } - - @Test - void should_throw_exception_when_function_description_is_missing() { - customRuntime2FunctionBuilder.description(); - - assertThatThrownBy(() -> customRuntime2FunctionBuilder.build()) - .isNotNull() - .isInstanceOf(IllegalArgumentException.class) - .hasFieldOrPropertyWithValue("message", "'description' is required"); - } - - @Test - void should_throw_exception_when_function_description_is_empty_string() { - customRuntime2FunctionBuilder.description(StringUtils.EMPTY); - - assertThatThrownBy(() -> customRuntime2FunctionBuilder.build()) - .isNotNull() - .isInstanceOf(IllegalArgumentException.class) - .hasFieldOrPropertyWithValue("message", "'description' is required"); - } - - @Test - void should_throw_exception_when_function_environment_is_empty_map() { - customRuntime2FunctionBuilder.environment(Collections.emptyMap()); - - assertThatThrownBy(() -> customRuntime2FunctionBuilder.build()) - .isNotNull() - .isInstanceOf(IllegalArgumentException.class) - .hasFieldOrPropertyWithValue("message", "'environment' is required"); - } - - @Test - void should_throw_exception_when_function_environment_is_null() { - customRuntime2FunctionBuilder.environment(null); - - assertThatThrownBy(() -> customRuntime2FunctionBuilder.build()) - .isNotNull() - .isInstanceOf(IllegalArgumentException.class) - .hasFieldOrPropertyWithValue("message", "'environment' is required"); - } - - @Test - void should_throw_exception_when_function_memory_size_is_less_than_128() { - customRuntime2FunctionBuilder.memorySize(120); - - assertThatThrownBy(() -> customRuntime2FunctionBuilder.build()) - .isNotNull() - .isInstanceOf(IllegalArgumentException.class) - .hasFieldOrPropertyWithValue("message", - "'memorySize' must be between 128 and 3008 (inclusive)"); - } - - @Test - void should_throw_exception_when_function_memory_size_is_larger_than_3008() { - customRuntime2FunctionBuilder.memorySize(3500); - - assertThatThrownBy(() -> customRuntime2FunctionBuilder.build()) - .isNotNull() - .isInstanceOf(IllegalArgumentException.class) - .hasFieldOrPropertyWithValue("message", - "'memorySize' must be between 128 and 3008 (inclusive)"); - } - - @Test - void should_throw_exception_when_function_memory_size_is_less_than_zero() { - customRuntime2FunctionBuilder.retryAttempts(-1); - - assertThatThrownBy(() -> customRuntime2FunctionBuilder.build()) - .isNotNull() - .isInstanceOf(IllegalArgumentException.class) - .hasFieldOrPropertyWithValue("message", - "'retryAttempts' must be between 0 and 2 (inclusive)"); - } - - @Test - void should_throw_exception_when_function_memory_size_is_larger_than_two() { - customRuntime2FunctionBuilder.retryAttempts(3); - - assertThatThrownBy(() -> customRuntime2FunctionBuilder.build()) - .isNotNull() - .isInstanceOf(IllegalArgumentException.class) - .hasFieldOrPropertyWithValue("message", - "'retryAttempts' must be between 0 and 2 (inclusive)"); - } -}