From f35e292f7f60ccb9ad2d22288f2ae8a8fd97bcd5 Mon Sep 17 00:00:00 2001 From: Matto Date: Wed, 20 Mar 2024 23:07:41 +1100 Subject: [PATCH 1/6] Renaming some modules --- .dockerignore | 2 +- .github/workflows/release.yml | 4 +- NOTICE | 2 +- README.md | 22 +- cdk.json | 2 +- docker-compose.yml | 12 +- pom.xml | 53 +++- .../function/ExampleFunctionTest.java | 72 ----- .../infra/ApiBaseStack.java | 225 --------------- .../infra/Application.java | 70 ----- .../infra/CoffeeBeansConstruct.java | 8 - .../infra/SpringNativeAwsLambdaStack.java | 81 ------ .../infra/StackUtils.java | 64 ----- .../lambda/CustomRuntime2023Function.java | 100 ------- .../infra/ApiBaseStackTest.java | 225 --------------- .../infra/LambdaTest.java | 245 ---------------- .../infra/RestApiTest.java | 165 ----------- .../lambda/CustomRuntime2023FunctionTest.java | 268 ------------------ .../pom.xml | 17 +- .../infra/Application.java | 66 +++++ .../infra/Constants.java | 8 +- .../infra/CostCentre.java | 36 +-- .../infra/Environment.java | 53 ++++ .../infra/SpringNativeAwsFunctionStack.java | 117 ++++++++ .../springnativeawslambda/infra/TagUtils.java | 10 +- .../infra/ApiBaseStackTest.java | 225 +++++++++++++++ .../infra/LambdaTest.java | 114 ++++++++ .../infra/RestApiTest.java | 166 +++++++++++ .../infra/TagUtilsTest.java | 44 +++ .../infra/TemplateSupport.java | 62 ++-- .../infra/TestLambdaUtils.java | 0 .../infra/TopicTest.java | 4 +- .../org.mockito.plugins.MockMaker | 0 .../pom.xml | 11 +- .../src/assembly/native.xml | 2 +- .../springnativeawslambda/Application.java | 0 .../springnativeawslambda/DynamoDbConfig.java | 21 ++ .../LambdaExceptionHandler.java | 0 .../ReflectionRuntimeHints.java | 14 +- .../ResourcesRuntimeHints.java | 1 + .../springnativeawslambda/entity/Secret.java | 34 +++ .../function/ExampleFunction.java | 20 +- .../springnativeawslambda/model/Request.java | 0 .../springnativeawslambda/model/Response.java | 0 .../repository/SecretRepository.java | 50 ++++ .../src/main/resources/application-local.yml | 8 +- .../src/main/resources/application.yml | 7 +- .../src/shell/native/bootstrap | 2 +- .../springnativeawslambda/ApplicationIT.java | 0 .../function/ExampleFunctionTest.java | 72 +++++ 50 files changed, 1158 insertions(+), 1626 deletions(-) delete mode 100644 spring-native-aws-lambda-function/src/test/java/com/coffeebeans/springnativeawslambda/function/ExampleFunctionTest.java delete mode 100644 spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/ApiBaseStack.java delete mode 100644 spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/Application.java delete mode 100644 spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/CoffeeBeansConstruct.java delete mode 100644 spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/SpringNativeAwsLambdaStack.java delete mode 100644 spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/StackUtils.java delete 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/test/java/com/coffeebeans/springnativeawslambda/infra/ApiBaseStackTest.java delete mode 100644 spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/LambdaTest.java delete mode 100644 spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/RestApiTest.java delete mode 100644 spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/lambda/CustomRuntime2023FunctionTest.java rename {spring-native-aws-lambda-infra => spring-native-aws-service-infra}/pom.xml (89%) create mode 100644 spring-native-aws-service-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/Application.java rename {spring-native-aws-lambda-infra => spring-native-aws-service-infra}/src/main/java/com/coffeebeans/springnativeawslambda/infra/Constants.java (54%) rename spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/TagUtilsTest.java => spring-native-aws-service-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/CostCentre.java (52%) create mode 100644 spring-native-aws-service-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/Environment.java create mode 100644 spring-native-aws-service-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/SpringNativeAwsFunctionStack.java rename {spring-native-aws-lambda-infra => spring-native-aws-service-infra}/src/main/java/com/coffeebeans/springnativeawslambda/infra/TagUtils.java (78%) create mode 100644 spring-native-aws-service-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/ApiBaseStackTest.java create mode 100644 spring-native-aws-service-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/LambdaTest.java create mode 100644 spring-native-aws-service-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/RestApiTest.java create mode 100644 spring-native-aws-service-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/TagUtilsTest.java rename {spring-native-aws-lambda-infra => spring-native-aws-service-infra}/src/test/java/com/coffeebeans/springnativeawslambda/infra/TemplateSupport.java (51%) rename {spring-native-aws-lambda-infra => spring-native-aws-service-infra}/src/test/java/com/coffeebeans/springnativeawslambda/infra/TestLambdaUtils.java (100%) rename {spring-native-aws-lambda-infra => spring-native-aws-service-infra}/src/test/java/com/coffeebeans/springnativeawslambda/infra/TopicTest.java (87%) rename {spring-native-aws-lambda-infra => spring-native-aws-service-infra}/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker (100%) rename {spring-native-aws-lambda-function => spring-native-aws-service}/pom.xml (95%) rename {spring-native-aws-lambda-function => spring-native-aws-service}/src/assembly/native.xml (96%) rename {spring-native-aws-lambda-function => spring-native-aws-service}/src/main/java/com/coffeebeans/springnativeawslambda/Application.java (100%) create mode 100644 spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/DynamoDbConfig.java rename {spring-native-aws-lambda-function => spring-native-aws-service}/src/main/java/com/coffeebeans/springnativeawslambda/LambdaExceptionHandler.java (100%) rename {spring-native-aws-lambda-function => spring-native-aws-service}/src/main/java/com/coffeebeans/springnativeawslambda/ReflectionRuntimeHints.java (75%) rename {spring-native-aws-lambda-function => spring-native-aws-service}/src/main/java/com/coffeebeans/springnativeawslambda/ResourcesRuntimeHints.java (91%) create mode 100644 spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/entity/Secret.java rename {spring-native-aws-lambda-function => spring-native-aws-service}/src/main/java/com/coffeebeans/springnativeawslambda/function/ExampleFunction.java (79%) rename {spring-native-aws-lambda-function => spring-native-aws-service}/src/main/java/com/coffeebeans/springnativeawslambda/model/Request.java (100%) rename {spring-native-aws-lambda-function => spring-native-aws-service}/src/main/java/com/coffeebeans/springnativeawslambda/model/Response.java (100%) create mode 100644 spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/repository/SecretRepository.java rename {spring-native-aws-lambda-function => spring-native-aws-service}/src/main/resources/application-local.yml (86%) rename {spring-native-aws-lambda-function => spring-native-aws-service}/src/main/resources/application.yml (87%) rename {spring-native-aws-lambda-function => spring-native-aws-service}/src/shell/native/bootstrap (95%) rename {spring-native-aws-lambda-function => spring-native-aws-service}/src/test/java/com/coffeebeans/springnativeawslambda/ApplicationIT.java (100%) create mode 100644 spring-native-aws-service/src/test/java/com/coffeebeans/springnativeawslambda/function/ExampleFunctionTest.java diff --git a/.dockerignore b/.dockerignore index 4d096dd..fd40775 100644 --- a/.dockerignore +++ b/.dockerignore @@ -15,5 +15,5 @@ README.md settings-spring.xml -spring-native-aws-lambda-infra +spring-native-aws-service-infra docker-compose.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7b42b3f..e0fbf58 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -58,8 +58,8 @@ jobs: ENV: ${{ env.ENV }} COST_CENTRE: ${{ env.COST_CENTRE }} run: | - ./mvnw -ntp clean verify -DskipTests -pl spring-native-aws-lambda-infra - ./mvnw -ntp clean -Pnative -DskipTests native:compile package -pl spring-native-aws-lambda-function + ./mvnw -ntp clean verify -DskipTests -pl spring-native-aws-service-infra + ./mvnw -ntp clean -Pnative -DskipTests native:compile package -pl spring-native-aws-service - name: cdk diff uses: muhamadto/aws-cdk-github-actions@v5 with: diff --git a/NOTICE b/NOTICE index c0a3d8a..6cf1727 100644 --- a/NOTICE +++ b/NOTICE @@ -1,4 +1,4 @@ -Apache [spring-native-aws-lambda-function] +Apache [spring-native-aws-function] Copyright 2021 Muhammad Hamadto This product includes software developed by: diff --git a/README.md b/README.md index dbea69c..79497c7 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# spring-native-aws-lambda +# spring-native-aws-function [![CodeQL](https://github.com/muhamadto/spring-native-aws-lambda/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/muhamadto/spring-native-aws-lambda/actions/workflows/codeql-analysis.yml) [![Build](https://github.com/muhamadto/spring-native-aws-lambda/actions/workflows/build.yml/badge.svg)](https://github.com/muhamadto/spring-native-aws-lambda/actions/workflows/build.yml) @@ -56,8 +56,8 @@ $ ./mvnw -ntp clean verify -U 1. Run the following commands ```shell $ export SPRING_PROFILES_ACTIVE=local - $ ./mvnw -ntp clean -Pnative -DskipTests native:compile package -pl spring-native-aws-lambda-function - $ ./spring-native-aws-lambda-function/target/spring-native-aws-lambda-function + $ ./mvnw -ntp clean -DskipTests -Pnative native:compile package -pl spring-native-aws-service + $ ./spring-native-aws-service/target/spring-native-aws-service ``` The service starts in less than 100 ms ```shell @@ -134,7 +134,7 @@ and the following trust relationship "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" }, "StringLike": { - "token.actions.githubusercontent.com:sub": "repo:{github-account-or-org}/spring-native-aws-lambda:*" + "token.actions.githubusercontent.com:sub": "repo:{github-account-or-org}/spring-native-aws-service:*" } } } @@ -259,7 +259,7 @@ and the following trust relationship "SNS:UntagResource" ], "Resource": [ - "arn:aws:sns:{aws-region}:{aws-account-number}:spring-native-aws-lambda-function-dead-letter-topic" + "arn:aws:sns:{aws-region}:{aws-account-number}:spring-native-aws-function-dead-letter-topic" ] }, { @@ -281,8 +281,8 @@ and the following trust relationship "lambda:UpdateFunctionConfiguration" ], "Resource": [ - "arn:aws:lambda:{aws-region}:{aws-account-number}:function:spring-native-aws-lambda-function", - "arn:aws:lambda:{aws-region}:{aws-account-number}:function:spring-native-aws-lambda-function:$LATEST" + "arn:aws:lambda:{aws-region}:{aws-account-number}:function:spring-native-aws-function", + "arn:aws:lambda:{aws-region}:{aws-account-number}:function:spring-native-aws-function:$LATEST" ] }, { @@ -310,9 +310,9 @@ and the following trust relationship "iam:DetachRolePolicy" ], "Resource": [ - "arn:aws:iam::{aws-account-number}:role/spring-native-aws-lambda-springnativeawslambdafun-*", - "arn:aws:iam::{aws-account-number}:role/spring-native-aws-lambda-springnativeawslambdares-4FVJBBHF9EL2", - "arn:aws:iam::{aws-account-number}:role/spring-native-aws-lambda-function-rest-api/CloudWatchRole" + "arn:aws:iam::{aws-account-number}:role/spring-native-aws-service-springnativeawslambdafun-*", + "arn:aws:iam::{aws-account-number}:role/spring-native-aws-service-springnativeawslambdares-4FVJBBHF9EL2", + "arn:aws:iam::{aws-account-number}:role/spring-native-aws-function-rest-api/CloudWatchRole" ] }, { @@ -346,7 +346,7 @@ in step 4 Now that the setup is done you can deploy to AWS. 1. Create a new release in - Github [releases page](https://github.com/muhamadto/spring-native-aws-lambda/releases), + Github [releases page](https://github.com/muhamadto/spring-native-aws-service/releases), the [github action](.github/workflows/release.yml) will start and a deployment to AWS environment. 2. Test via curl diff --git a/cdk.json b/cdk.json index 350653f..10fb62e 100644 --- a/cdk.json +++ b/cdk.json @@ -1,3 +1,3 @@ { - "app": "./mvnw exec:java -pl spring-native-aws-lambda-infra -Dexec.mainClass=com.coffeebeans.springnativeawslambda.infra.Application" + "app": "./mvnw exec:java -pl spring-native-aws-service-infra -Dexec.mainClass=com.coffeebeans.springnativeawslambda.infra.Application" } \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index da2dd19..696ff14 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,8 +15,8 @@ version: "3.9" services: - spring-native-aws-lambda-function-infra: - image: muhamadto/spring-native-amazonlinux2-builder:21-amazonlinux2-awscliv2 + spring-native-aws-function-infra: + image: ghcr.io/muhamadto/spring-native-amazonlinux2-builder:21-amazonlinux2-awscliv2 volumes: - ./:/app - ${M2_REPO}:/home/worker/.m2 @@ -29,7 +29,9 @@ services: AWS_SECRET_ACCESS_KEY: local AWS_ENDPOINT_URL: http://localstack:4566 BUILD_ARTIFACT: 'true' - FUNCTION_NAME: spring-native-aws-lambda-function + FUNCTION_NAME: spring-native-aws-service + SPRING_MAIN_WEBAPPLICATIONTYPE: servlet + SPRING_CLOULD_AWS_DYNAMODB_ENDPOINT: http://localstack:4566 STAGE: compose MAVEN_OPTS: | -DskipTests=true @@ -45,7 +47,7 @@ services: - -c command: | ' - function package_spring_native_lambda() { + function package_spring_native_function() { if [ "$$BUILD_ARTIFACT" = "true" ]; then ./mvnw -ntp clean -Pnative -DskipTests native:compile package -pl "$$FUNCTION_NAME" else @@ -61,7 +63,7 @@ services: print_info_message "divider" "Package GraalVM function" && - package_spring_native_lambda && + package_spring_native_function && print_info_message "divider" "Creating LAMBDA function" && lambda_create_function "$$FUNCTION_NAME" provided.al2023 512 ./"$$FUNCTION_NAME"/target/"$$FUNCTION_NAME"-native-zip.zip "$$FUNCTION_NAME" && diff --git a/pom.xml b/pom.xml index 9c355de..71c0988 100644 --- a/pom.xml +++ b/pom.xml @@ -26,7 +26,7 @@ org.springframework.boot spring-boot-starter-parent 3.2.1 - + com.coffeebeans @@ -34,8 +34,7 @@ ${project.artifactId} ${revision} pom - Demo project for Spring cloud function with graalvm native image deployed with cdk - + Demo project for Spring cloud function with graalvm native image deployed with cdk @@ -79,26 +78,47 @@ - spring-native-aws-lambda-function - spring-native-aws-lambda-infra + spring-native-aws-service + spring-native-aws-service-infra + + + io.awspring.cloud + spring-cloud-aws-dependencies + 3.1.1 + pom + import + + com.coffeebeans - spring-native-aws-lambda-function + spring-native-aws-service ${project.version} com.coffeebeans - spring-native-aws-lambda-infra + spring-native-aws-service-infra ${project.version} + + com.coffeebeans + coffeebeans-cdk-core + 1.0-SNAPSHOT + + + + com.coffeebeans + coffeebeans-cdk-assertions + 1.0-SNAPSHOT + + com.amazonaws @@ -187,7 +207,7 @@ software.amazon.awscdk aws-cdk-lib - 2.116.1 + 2.128.0 @@ -213,6 +233,13 @@ test + + org.junit.jupiter + junit-jupiter-params + ${junit-jupiter.version} + test + + org.mockito mockito-core @@ -271,6 +298,16 @@ + + + github + https://public:0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@maven.pkg.github.com/muhamadto/* + + true + + + + Apache License, Version 2.0 diff --git a/spring-native-aws-lambda-function/src/test/java/com/coffeebeans/springnativeawslambda/function/ExampleFunctionTest.java b/spring-native-aws-lambda-function/src/test/java/com/coffeebeans/springnativeawslambda/function/ExampleFunctionTest.java deleted file mode 100644 index 801f406..0000000 --- a/spring-native-aws-lambda-function/src/test/java/com/coffeebeans/springnativeawslambda/function/ExampleFunctionTest.java +++ /dev/null @@ -1,72 +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.function; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; -import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; -import com.coffeebeans.springnativeawslambda.function.ExampleFunction; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Spy; - -class ExampleFunctionTest { - - private ExampleFunction exampleFunction; - - @Spy - private ObjectMapper objectMapper = new ObjectMapper(); - - @BeforeEach - void setUp() { - exampleFunction = new ExampleFunction(objectMapper); - } - - @Test - void should_return_APIGatewayProxyResponseEvent() { - final String requestBody = "{\"name\":\"Coffeebeans\"}"; - final String responseBody = "{\"name\":\"Coffeebeans\",\"saved\":true}"; - - final APIGatewayProxyRequestEvent request = new APIGatewayProxyRequestEvent() - .withBody(requestBody); - - final APIGatewayProxyResponseEvent apiGatewayProxyResponseEvent = new APIGatewayProxyResponseEvent() - .withStatusCode(200) - .withBody(responseBody); - - final APIGatewayProxyResponseEvent actual = exampleFunction.apply(request); - - assertThat(actual) - .isEqualTo(apiGatewayProxyResponseEvent); - } - - @Test - void should_throw_JsonProcessingException() { - - final APIGatewayProxyRequestEvent request = new APIGatewayProxyRequestEvent() - .withBody("Coffeebeans"); - - assertThatThrownBy(() -> exampleFunction.apply(request)) - .isInstanceOf(JsonProcessingException.class); - } -} \ 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 deleted file mode 100644 index fafc682..0000000 --- a/spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/ApiBaseStack.java +++ /dev/null @@ -1,225 +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; - -import com.coffeebeans.springnativeawslambda.infra.lambda.CustomRuntime2023Function; -import org.apache.commons.lang3.StringUtils; -import software.amazon.awscdk.Duration; -import software.amazon.awscdk.Stack; -import software.amazon.awscdk.StackProps; -import software.amazon.awscdk.services.apigateway.LambdaRestApi; -import software.amazon.awscdk.services.apigateway.Resource; -import software.amazon.awscdk.services.apigateway.StageOptions; -import software.amazon.awscdk.services.ec2.IVpc; -import software.amazon.awscdk.services.iam.IRole; -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; -import software.amazon.awscdk.services.sqs.DeadLetterQueue; -import software.amazon.awscdk.services.sqs.DeduplicationScope; -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) { - - 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 deleted file mode 100644 index 15ecaaf..0000000 --- a/spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/Application.java +++ /dev/null @@ -1,70 +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; - -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.createTags; -import static com.google.common.base.Preconditions.checkNotNull; -import static lombok.AccessLevel.PRIVATE; - -@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"; - - public static void main(final String... args) { - final App app = new App(); - - 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 " + 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())); - - 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 deleted file mode 100644 index 141706d..0000000 --- a/spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/CoffeeBeansConstruct.java +++ /dev/null @@ -1,8 +0,0 @@ -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/SpringNativeAwsLambdaStack.java b/spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/SpringNativeAwsLambdaStack.java deleted file mode 100644 index c850914..0000000 --- a/spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/SpringNativeAwsLambdaStack.java +++ /dev/null @@ -1,81 +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; - -import software.amazon.awscdk.StackProps; -import software.amazon.awscdk.services.iam.IManagedPolicy; -import software.amazon.awscdk.services.iam.Role; -import software.amazon.awscdk.services.iam.ServicePrincipal; -import software.amazon.awscdk.services.lambda.AssetCode; -import software.amazon.awscdk.services.lambda.Function; -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"; - private static final String REST_API_ID = LAMBDA_FUNCTION_ID + "-rest-api"; - private static final String DEAD_LETTER_TOPIC_ID = LAMBDA_FUNCTION_ID + "-dead-letter-topic"; - private static final String LAMBDA_HANDLER = "org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest"; - private static final String ENVIRONMENT_VARIABLE_SPRING_PROFILES_ACTIVE = "SPRING_PROFILES_ACTIVE"; - - public SpringNativeAwsLambdaStack( - @NotNull final Construct scope, - @NotBlank final String id, - @NotBlank final String lambdaCodePath, - @NotBlank final String stage, - @NotNull final StackProps props) { - - super(scope, id, props); - - final Topic deadLetterTopic = createTopic(DEAD_LETTER_TOPIC_ID); - - final List managedPolicies = - List.of(fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")); - - final Role role = Role.Builder.create(this, LAMBDA_FUNCTION_ID + "-role") - .assumedBy(new ServicePrincipal("lambda.amazonaws.com")) - .managedPolicies(managedPolicies) - .build(); - - final AssetCode assetCode = fromAsset(lambdaCodePath); - - final Map environment = - Map.of(ENVIRONMENT_VARIABLE_SPRING_PROFILES_ACTIVE, stage, KEY_ENV, stage); - - final Function function = createFunction( - LAMBDA_FUNCTION_ID, - LAMBDA_HANDLER, - assetCode, - deadLetterTopic, - role, - environment); - - createLambdaRestApi(stage, REST_API_ID, "name", "POST", function, true); - } -} \ No newline at end of file diff --git a/spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/StackUtils.java b/spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/StackUtils.java deleted file mode 100644 index d29f592..0000000 --- a/spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/StackUtils.java +++ /dev/null @@ -1,64 +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; - -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotEmpty; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; -import org.jetbrains.annotations.NotNull; -import software.amazon.awscdk.App; -import software.amazon.awscdk.DefaultStackSynthesizer; -import software.amazon.awscdk.StackProps; - -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public final class StackUtils { - - @NotNull - public static SpringNativeAwsLambdaStack createStack( - @NotNull final App app, - @NotBlank final String stackName, - @NotBlank final String lambdaCodePath, - @NotBlank final String qualifier, - @NotBlank final String fileAssetsBucketName, - @NotEmpty final String stage) { - final StackProps stackProps = createStackProps(stackName, qualifier, fileAssetsBucketName); - return new SpringNativeAwsLambdaStack(app, stackName, lambdaCodePath, stage, stackProps); - } - - @NotNull - private static StackProps createStackProps(@NotBlank final String stackName, - @NotBlank final String qualifier, - @NotBlank final String fileAssetsBucketName) { - return StackProps.builder() - .synthesizer(createDefaultStackSynthesizer(qualifier, fileAssetsBucketName)) - .stackName(stackName) - .build(); - } - - @NotNull - private static DefaultStackSynthesizer createDefaultStackSynthesizer( - @NotBlank final String qualifier, - @NotBlank final String fileAssetsBucketName) { - return DefaultStackSynthesizer.Builder.create() - .qualifier(qualifier) - .fileAssetsBucketName(fileAssetsBucketName) - .build(); - } -} 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 deleted file mode 100644 index 99e35de..0000000 --- a/spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/lambda/CustomRuntime2023Function.java +++ /dev/null @@ -1,100 +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 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/test/java/com/coffeebeans/springnativeawslambda/infra/ApiBaseStackTest.java b/spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/ApiBaseStackTest.java deleted file mode 100644 index 8b114d5..0000000 --- a/spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/ApiBaseStackTest.java +++ /dev/null @@ -1,225 +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; - -import static org.assertj.core.api.Assertions.assertThat; -import static software.amazon.awscdk.services.ec2.Vpc.Builder.create; -import static software.amazon.awscdk.services.iam.Role.fromRoleArn; -import static software.amazon.awscdk.services.lambda.Runtime.PROVIDED_AL2023; -import static software.amazon.awscdk.services.sns.Topic.fromTopicArn; -import static software.amazon.awscdk.services.sqs.DeduplicationScope.MESSAGE_GROUP; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.Map; -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.services.apigateway.LambdaRestApi; -import software.amazon.awscdk.services.ec2.Vpc; -import software.amazon.awscdk.services.lambda.Code; -import software.amazon.awscdk.services.lambda.Function; -import software.amazon.awscdk.services.sns.Topic; -import software.amazon.awscdk.services.sqs.DeadLetterQueue; -import software.amazon.awscdk.services.sqs.Queue; - -class ApiBaseStackTest { - - private static final String ENV = "test"; - - private ApiBaseStack apiBaseStack; - - private final App app = new App(); - - private Path lambdaCodePath; - - @TempDir - private static Path TEMP_DIR; - - @BeforeEach - void setUp() throws IOException { - - lambdaCodePath = TestLambdaUtils.getTestLambdaCodePath(TEMP_DIR); - - this.apiBaseStack = StackUtils.createStack(app, "test-stack", lambdaCodePath.toString(), ENV, - "test-cdk-bucket", ENV); - } - - @Test - void should_create_and_return_queue() { - final String queueId = "test-queue"; - - final Queue actual = this.apiBaseStack.createQueue(queueId); - - assertThat(actual) - .isNotNull() - .hasFieldOrProperty("queueName") - .hasFieldOrProperty("queueArn") - .hasFieldOrProperty("queueUrl") - .hasFieldOrProperty("deadLetterQueue") - .extracting("fifo") - .isEqualTo(false); - } - - @Test - void should_create_and_return_fifo_queue() { - final String queueId = "test-queue"; - - final Queue actual = this.apiBaseStack.createFifoQueue(queueId, true, MESSAGE_GROUP); - - assertThat(actual) - .isNotNull() - .hasFieldOrProperty("queueName") - .hasFieldOrProperty("queueArn") - .hasFieldOrProperty("queueUrl") - .hasFieldOrProperty("deadLetterQueue") - .extracting("fifo") - .isEqualTo(true); - - } - - @Test - void should_create_and_return_dead_letter_queue() { - final String deadLetterQueueId = "test-dead-letter-queue"; - - final DeadLetterQueue actual = this.apiBaseStack.createDeadLetterQueue(deadLetterQueueId); - - assertThat(actual) - .isNotNull() - .hasFieldOrProperty("maxReceiveCount") - .hasFieldOrProperty("queue"); - - assertThat(actual.getMaxReceiveCount()) - .isEqualTo(3); - - assertThat(actual.getQueue()) - .hasFieldOrProperty("queueName") - .hasFieldOrProperty("queueArn") - .hasFieldOrProperty("queueUrl") - .extracting("fifo") - .isEqualTo(false); - } - - @Test - void should_create_and_return_fifo_dead_letter_queue() { - final String deadLetterQueueId = "test-fifo-dead-letter-queue"; - - final DeadLetterQueue actual = this.apiBaseStack.createFifoDeadLetterQueue(deadLetterQueueId, - true, MESSAGE_GROUP); - - assertThat(actual) - .isNotNull() - .hasFieldOrProperty("maxReceiveCount") - .hasFieldOrProperty("queue"); - - assertThat(actual.getMaxReceiveCount()) - .isEqualTo(3); - - assertThat(actual.getQueue()) - .isNotNull() - .hasFieldOrProperty("queueName") - .hasFieldOrProperty("queueArn") - .hasFieldOrProperty("queueUrl") - .extracting("fifo") - .isEqualTo(true); - } - - @Test - void should_create_and_return_topic() { - final String topicId = "test-topic"; - - final Topic actual = this.apiBaseStack.createTopic(topicId); - - assertThat(actual) - .isNotNull() - .hasFieldOrProperty("topicName") - .hasFieldOrProperty("topicArn") - .extracting("fifo") - .isEqualTo(false); - } - - @Test - void should_create_and_return_fifo_topic() { - final String topicId = "test-topic"; - - final Topic actual = this.apiBaseStack.createFifoTopic(topicId, true, true); - - assertThat(actual) - .isNotNull() - .hasFieldOrProperty("topicName") - .hasFieldOrProperty("topicArn") - .extracting("fifo") - .isEqualTo(true); - } - - @Test - void should_create_and_return_lambda_function() { - final Vpc vpc = create(this.apiBaseStack, "test-vpc").build(); - - final Function actual = this.apiBaseStack.createFunction(vpc, - "test-function", - "com.coffeebeans.springnativeawslambda.infra.lambda.CustomRuntime2Function::handleRequest", - Code.fromAsset(this.lambdaCodePath.toString()), - fromTopicArn(this.apiBaseStack, "success-topic", "arn:aws:sns:us-east-1:***:success-topic"), - fromRoleArn(this.apiBaseStack, "test-role", "arn:aws:iam::***:role/test-role"), - Map.of("Account", "***")); - - assertThat(actual) - .isNotNull() - .hasFieldOrProperty("functionArn") - .hasFieldOrProperty("role") - .hasFieldOrProperty("functionName") - .hasFieldOrProperty("functionArn") - .hasFieldOrProperty("env") - .hasFieldOrProperty("architecture") - .hasFieldOrProperty("runtime") - .hasFieldOrProperty("timeout"); - - assertThat(actual.getRuntime()) - .isEqualTo(PROVIDED_AL2023); - } - - @Test - void should_create_and_return_lambda_rest_api() { - final Vpc vpc = create(this.apiBaseStack, "test-vpc").build(); - - final Function function = this.apiBaseStack.createFunction(vpc, - "test-function", - "com.coffeebeans.springnativeawslambda.infra.lambda.CustomRuntime2Function::handleRequest", - Code.fromAsset(this.lambdaCodePath.toString()), - fromTopicArn(this.apiBaseStack, "success-topic", "arn:aws:sns:us-east-1:***:success-topic"), - fromRoleArn(this.apiBaseStack, "test-role", "arn:aws:iam::***:role/test-role"), - Map.of("Account", "***")); - - final LambdaRestApi actual = this.apiBaseStack.createLambdaRestApi("test", "rest-api", "name", - "POST", function, false); - - assertThat(actual) - .isNotNull() - .hasFieldOrProperty("deploymentStage") - .hasFieldOrProperty("env") - .hasFieldOrProperty("restApiName") - .hasFieldOrProperty("root") - .hasFieldOrProperty("url") - .hasFieldOrProperty("restApiRootResourceId") - .hasFieldOrProperty("restApiId") - .hasFieldOrProperty("methods"); - } -} \ No newline at end of file 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 deleted file mode 100644 index d89ccc0..0000000 --- a/spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/LambdaTest.java +++ /dev/null @@ -1,245 +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; - -import com.fasterxml.jackson.core.JsonProcessingException; -import org.junit.jupiter.api.Test; - -import java.util.List; -import java.util.Map; - -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", 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 deleted file mode 100644 index cb21274..0000000 --- a/spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/RestApiTest.java +++ /dev/null @@ -1,165 +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; - -import org.junit.jupiter.api.Test; -import software.amazon.awscdk.assertions.Match; - -import java.util.List; -import java.util.Map; - -import static cloud.pianola.cdk.fluent.assertion.CDKStackAssert.assertThat; -import static com.coffeebeans.springnativeawslambda.infra.Constants.KEY_COST_CENTRE; - -class RestApiTest extends TemplateSupport { - - public static final String TEST = "test"; - - @Test - void should_have_rest_api() { - - assertThat(template) - .containsRestApi("spring-native-aws-lambda-function-rest-api") - .hasTag("COST_CENTRE", KEY_COST_CENTRE) - .hasTag("ENV", TEST); - } - - @Test - void should_have_rest_api_account() { - final String cloudWatchRoleArn = "springnativeawslambdafunctionrestapiCloudWatchRole(.*)"; - final String dependency = "springnativeawslambdafunctionrestapi(.*)"; - - assertThat(template) - .containsRestApiAccountWithCloudWatchRoleArn(cloudWatchRoleArn) - .hasDependency(dependency) - .hasUpdateReplacePolicy("Retain") - .hasDeletionPolicy("Retain"); - } - - @Test - void should_have_rest_api_deployment() { - - assertThat(template) - .containsRestApiDeployment("springnativeawslambdafunctionrestapi(.*)") - .hasDependency("springnativeawslambdafunctionrestapiproxyANY(.*)") - .hasDependency("springnativeawslambdafunctionrestapiproxy(.*)") - .hasDependency("springnativeawslambdafunctionrestapiANY(.*)") - .hasDependency("springnativeawslambdafunctionrestapinamePOST(.*)") - .hasDependency("springnativeawslambdafunctionrestapiname(.*)") - .hasDescription("Automatically created by the RestApi construct"); - } - - @Test - void should_have_rest_api_stage() { - - assertThat(template) - .containsRestApiStage("test") - .hasRestApiId(("springnativeawslambdafunctionrestapi(.*)")) - .hasDeploymentId(("springnativeawslambdafunctionrestapiDeployment(.*)")) - .hasDependency("springnativeawslambdafunctionrestapiAccount(.*)") - .hasTag("COST_CENTRE", KEY_COST_CENTRE) - .hasTag("ENV", TEST); - } - - @Test - void should_have_proxy_resource() { - final String restApiId = "springnativeawslambdafunctionrestapi(.*)"; - - final Map> parentId = Map.of( - "Fn::GetAtt", List.of(Match.stringLikeRegexp(restApiId), "RootResourceId") - ); - - assertThat(template) - .containsRestApiResource("{proxy+}", restApiId, parentId) - .hasRestApiId(restApiId) - .hasParentId(restApiId); - } - - @Test - void should_have_account_resource() { - final String restApiId = "springnativeawslambdafunctionrestapi(.*)"; - - final Map> parentId = Map.of( - "Fn::GetAtt", List.of(Match.stringLikeRegexp(restApiId), "RootResourceId") - ); - - assertThat(template) - .containsRestApiResource("name", restApiId, parentId) - .hasRestApiId(restApiId) - .hasParentId(restApiId); - } - - @Test - void should_have_post_method() { - - final String integrationType = "AWS_PROXY"; - final String httpMethod = "POST"; - - assertThat(template) - .containsNonRootRestApiMethod(httpMethod, "springnativeawslambdafunctionrestapiname(.*)") - .hasHttpMethod(httpMethod) - .hasIntegration(httpMethod, integrationType) - .hasAuthorizationType("NONE") - .hasRestApiId(("springnativeawslambdafunctionrestapi(.*)")); - } - - @Test - void should_have_proxy_method() { - - final String method = "ANY"; - final String integrationType = "AWS_PROXY"; - - assertThat(template) - .containsNonRootRestApiMethod(method, "springnativeawslambdafunctionrestapiproxy(.*)") - .hasHttpMethod(method) - .hasIntegration("POST", integrationType) - .hasAuthorizationType("NONE") - .hasRestApiId(("springnativeawslambdafunctionrestapi(.*)")); - } - - @Test - void should_have_root_method() { - - final String method = "ANY"; - final String integrationType = "AWS_PROXY"; - - assertThat(template) - .containsRootRestApiMethod(method, "springnativeawslambdafunctionrestapi(.*)") - .hasHttpMethod(method) - .hasIntegration("POST", integrationType) - .hasAuthorizationType("NONE") - .hasRestApiId(("springnativeawslambdafunctionrestapi(.*)")); - } - - @Test - void should_have_role_with_AmazonAPIGatewayPushToCloudWatchLogs_policy_for_rest_api_to_push_logs_to_cloud_watch() { - final String principal = "apigateway.amazonaws.com"; - final String effect = "Allow"; - final String policyDocumentVersion = "2012-10-17"; - final String managedPolicyArn = ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs"; - - assertThat(template) - .containsRoleWithManagedPolicyArn(managedPolicyArn) - .hasAssumeRolePolicyDocument(principal, - null, - effect, - policyDocumentVersion, - "sts:AssumeRole"); - } -} \ 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 deleted file mode 100644 index 29a9174..0000000 --- a/spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/lambda/CustomRuntime2023FunctionTest.java +++ /dev/null @@ -1,268 +0,0 @@ -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/pom.xml b/spring-native-aws-service-infra/pom.xml similarity index 89% rename from spring-native-aws-lambda-infra/pom.xml rename to spring-native-aws-service-infra/pom.xml index 4cae631..2dc2eb7 100644 --- a/spring-native-aws-lambda-infra/pom.xml +++ b/spring-native-aws-service-infra/pom.xml @@ -28,7 +28,7 @@ ${revision} - spring-native-aws-lambda-infra + spring-native-aws-service-infra ${project.artifactId} jar Infrastructure as Code for deploying a Spring cloud function demo project with @@ -55,6 +55,11 @@ + + com.coffeebeans + coffeebeans-cdk-core + + org.projectlombok @@ -94,8 +99,8 @@ - cloud.pianola - cdk-fluent-assertions + com.coffeebeans + coffeebeans-cdk-assertions test @@ -111,6 +116,12 @@ test + + org.junit.jupiter + junit-jupiter-params + test + + org.mockito mockito-core diff --git a/spring-native-aws-service-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/Application.java b/spring-native-aws-service-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/Application.java new file mode 100644 index 0000000..0ab2b19 --- /dev/null +++ b/spring-native-aws-service-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/Application.java @@ -0,0 +1,66 @@ +/* + * 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; + +import static com.coffeebeans.springnativeawslambda.infra.Constants.KEY_APPLICATION_VALUE; +import static com.coffeebeans.springnativeawslambda.infra.Constants.KEY_ENV; +import static com.coffeebeans.springnativeawslambda.infra.Environment.COFFEE_BEANS_DEV_111111111111_AP_SOUTHEAST_2; +import static com.coffeebeans.springnativeawslambda.infra.Environment.COFFEE_BEANS_PRD_111111111111_AP_SOUTHEAST_2; +import static com.coffeebeans.springnativeawslambda.infra.TagUtils.createTags; +import static com.google.common.base.Preconditions.checkNotNull; +import static lombok.AccessLevel.PRIVATE; + +import com.coffeebeans.cdk.core.AbstractApp; +import com.coffeebeans.cdk.core.type.SafeString; +import java.util.Map; +import java.util.Objects; +import lombok.NoArgsConstructor; +import org.jetbrains.annotations.NotNull; +import software.amazon.awscdk.Tags; + +public final class Application extends AbstractApp { + private static final String ENVIRONMENT_NAME_DEV = "dev"; + private static final String ENVIRONMENT_NAME_PRD = "prd"; + private static final String LAMBDA_CODE_PATH = "spring-native-aws-service/target/spring-native-aws-function-native-zip.zip"; + + public static void main(final String... args) { + final Application app = new Application(); + + final String env = System.getenv(KEY_ENV); + checkNotNull(env, "'env' environment variable is required"); + + switch (env) { + case ENVIRONMENT_NAME_DEV -> new SpringNativeAwsFunctionStack(app, COFFEE_BEANS_DEV_111111111111_AP_SOUTHEAST_2, LAMBDA_CODE_PATH, env); + case ENVIRONMENT_NAME_PRD -> new SpringNativeAwsFunctionStack(app, COFFEE_BEANS_PRD_111111111111_AP_SOUTHEAST_2, LAMBDA_CODE_PATH, env); + default -> throw new IllegalArgumentException("Environment name '%s' is not set to a valid value. Set it to '[dev|prd]'".formatted(KEY_ENV)); + } + + final Map tags = createTags(env, CostCentre.COFFEE_BEANS, app.getApplicationName().getValue()); + tags.entrySet().stream() + .filter(tag -> Objects.nonNull(tag.getValue())) + .forEach(tag -> Tags.of(app).add(tag.getKey(), tag.getValue())); + + app.synth(); + } + + @Override + public @NotNull SafeString getApplicationName() { + return SafeString.of(KEY_APPLICATION_VALUE); + } +} \ No newline at end of file diff --git a/spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/Constants.java b/spring-native-aws-service-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/Constants.java similarity index 54% rename from spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/Constants.java rename to spring-native-aws-service-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/Constants.java index d628ffa..0205f22 100644 --- a/spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/Constants.java +++ b/spring-native-aws-service-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/Constants.java @@ -5,10 +5,8 @@ @NoArgsConstructor(access = AccessLevel.PRIVATE) public class Constants { - public static final String KEY_ENV = "ENV"; + public static final String KEY_ENV = "ENVIRONMENT"; 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"; - + public static final String KEY_APPLICATION_NAME = "APPLICATION_NAME"; + public static final String KEY_APPLICATION_VALUE = "spring-native-aws-function"; } diff --git a/spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/TagUtilsTest.java b/spring-native-aws-service-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/CostCentre.java similarity index 52% rename from spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/TagUtilsTest.java rename to spring-native-aws-service-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/CostCentre.java index a3fa472..6935907 100644 --- a/spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/TagUtilsTest.java +++ b/spring-native-aws-service-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/CostCentre.java @@ -1,5 +1,5 @@ /* - * Licensed to Muhammad Hamadto + * 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. @@ -13,32 +13,24 @@ * 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; -import org.junit.jupiter.api.Test; - -import java.util.Map; - -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 { +import com.coffeebeans.cdk.core.AbstractCostCentre; +import com.coffeebeans.cdk.core.type.AlphanumericString; +import lombok.Getter; +import lombok.experimental.SuperBuilder; - @Test - void should_create_and_return_tag_map() { - // given - final String env = "test"; - final String costCentre = "coffeeBeans-core"; +@Getter +@SuperBuilder +public class CostCentre extends AbstractCostCentre { - // when - final Map tags = TagUtils.createTags(env, costCentre); + public static final CostCentre COFFEE_BEANS = CostCentre.builder() + .value(AlphanumericString.of("cbcore")) + .build(); - assertThat(tags) - .containsEntry(KEY_ENV, env) - .containsEntry(KEY_COST_CENTRE, costCentre); + static { + registerCostCentre(COFFEE_BEANS); } -} \ No newline at end of file +} diff --git a/spring-native-aws-service-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/Environment.java b/spring-native-aws-service-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/Environment.java new file mode 100644 index 0000000..c634adf --- /dev/null +++ b/spring-native-aws-service-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/Environment.java @@ -0,0 +1,53 @@ +package com.coffeebeans.springnativeawslambda.infra; + +import static com.coffeebeans.cdk.core.util.Constants.AWS_REGION_AP_SOUTHEAST_2; + +import com.coffeebeans.cdk.core.AbstractEnvironment; +import com.coffeebeans.cdk.core.type.AWSAccount; +import com.coffeebeans.cdk.core.type.SafeString; +import lombok.Getter; +import lombok.experimental.SuperBuilder; + +@Getter +@SuperBuilder +public class Environment extends AbstractEnvironment { + + public static final Environment COFFEE_BEANS_DEV_111111111111_AP_SOUTHEAST_2; + public static final Environment COFFEE_BEANS_PRD_111111111111_AP_SOUTHEAST_2; + public static final Environment COFFEE_BEANS_TEST_111111111111_AP_SOUTHEAST_2; + + static { + final AWSAccount awsAccount = AWSAccount.of("111111111111"); + final SafeString awsRegion = SafeString.of(AWS_REGION_AP_SOUTHEAST_2); + + final software.amazon.awscdk.Environment awsEnvironment = software.amazon.awscdk.Environment.builder() + .account(awsAccount.getValue()) + .account(awsRegion.getValue()) + .build(); + + COFFEE_BEANS_DEV_111111111111_AP_SOUTHEAST_2 = Environment.builder() + .awsEnvironment(awsEnvironment) + .costCentre(CostCentre.COFFEE_BEANS) + .environmentName(SafeString.of("DEV")) + .environmentKey(SafeString.of("COFFEE_BEANS_TEST_111111111111_AP_SOUTHEAST_2")) + .build(); + + COFFEE_BEANS_PRD_111111111111_AP_SOUTHEAST_2 = Environment.builder() + .awsEnvironment(awsEnvironment) + .costCentre(CostCentre.COFFEE_BEANS) + .environmentName(SafeString.of("PRD")) + .environmentKey(SafeString.of("COFFEE_BEANS_PRD_111111111111_AP_SOUTHEAST_2")) + .build(); + + COFFEE_BEANS_TEST_111111111111_AP_SOUTHEAST_2 = Environment.builder() + .awsEnvironment(awsEnvironment) + .costCentre(CostCentre.COFFEE_BEANS) + .environmentName(SafeString.of("TEST")) + .environmentKey(SafeString.of("COFFEE_BEANS_TEST_111111111111_AP_SOUTHEAST_2")) + .build(); + + registerEnvironment(COFFEE_BEANS_DEV_111111111111_AP_SOUTHEAST_2); + registerEnvironment(COFFEE_BEANS_PRD_111111111111_AP_SOUTHEAST_2); + registerEnvironment(COFFEE_BEANS_TEST_111111111111_AP_SOUTHEAST_2); + } +} diff --git a/spring-native-aws-service-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/SpringNativeAwsFunctionStack.java b/spring-native-aws-service-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/SpringNativeAwsFunctionStack.java new file mode 100644 index 0000000..f50d5c6 --- /dev/null +++ b/spring-native-aws-service-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/SpringNativeAwsFunctionStack.java @@ -0,0 +1,117 @@ +/* + * 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; + +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; + +import com.coffeebeans.cdk.core.AbstractApp; +import com.coffeebeans.cdk.core.AbstractEnvironment; +import com.coffeebeans.cdk.core.construct.BaseStack; +import com.coffeebeans.cdk.core.construct.dynamodb.TableV2; +import com.coffeebeans.cdk.core.construct.dynamodb.TableV2.TableProps; +import com.coffeebeans.cdk.core.construct.lambda.CustomRuntime2023Function; +import com.coffeebeans.cdk.core.construct.lambda.CustomRuntime2023Function.CustomRuntime2023FunctionProps; +import com.coffeebeans.cdk.core.type.KebabCaseString; +import com.coffeebeans.cdk.core.type.SafeString; +import java.util.List; +import java.util.Map; +import javax.validation.constraints.NotBlank; +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; +import software.amazon.awscdk.Duration; +import software.amazon.awscdk.services.apigateway.LambdaRestApi; +import software.amazon.awscdk.services.apigateway.Resource; +import software.amazon.awscdk.services.apigateway.StageOptions; +import software.amazon.awscdk.services.dynamodb.Attribute; +import software.amazon.awscdk.services.dynamodb.AttributeType; +import software.amazon.awscdk.services.iam.IGrantable; +import software.amazon.awscdk.services.iam.IManagedPolicy; +import software.amazon.awscdk.services.iam.Role; +import software.amazon.awscdk.services.iam.ServicePrincipal; +import software.amazon.awscdk.services.lambda.AssetCode; +import software.amazon.awscdk.services.lambda.Function; + +public class SpringNativeAwsFunctionStack extends BaseStack { + + 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 LAMBDA_HANDLER = "org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest"; + private static final String ENVIRONMENT_VARIABLE_SPRING_PROFILES_ACTIVE = "SPRING_PROFILES_ACTIVE"; + + + public SpringNativeAwsFunctionStack(@NotNull final AbstractApp app, + @NotNull final AbstractEnvironment environment, + @NotBlank final String lambdaCodePath, + @NotBlank final String stage) { + super(app, environment); + + final List managedPolicies = + List.of(fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")); + + final Role role = Role.Builder.create(this, "Role") + .assumedBy(new ServicePrincipal("lambda.amazonaws.com")) + .managedPolicies(managedPolicies) + .build(); + + final AssetCode assetCode = fromAsset(lambdaCodePath); + + final Map lambdaEnvironment = Map.of(ENVIRONMENT_VARIABLE_SPRING_PROFILES_ACTIVE, stage, KEY_ENV, stage); + + final CustomRuntime2023FunctionProps functionProps = CustomRuntime2023FunctionProps.builder() + .description("Example of a Spring Native AWS Lambda Function using CDK") + .code(assetCode) + .handler(LAMBDA_HANDLER) + .role(role) + .environment(lambdaEnvironment) + .deadLetterQueueEnabled(true) + .timeout(Duration.seconds(LAMBDA_FUNCTION_TIMEOUT_IN_SECONDS)) + .memorySize(LAMBDA_FUNCTION_MEMORY_SIZE) + .retryAttempts(LAMBDA_FUNCTION_RETRY_ATTEMPTS) + .build(); + + final Function function = new CustomRuntime2023Function<>(this, SafeString.of("Lambda"), functionProps) + .getFunction(); + + final TableProps tableProps = TableProps.builder() + .partitionKey(Attribute.builder().name("id").type(AttributeType.STRING).build()) + .timeToLiveAttribute("creationTime") + .tableName(KebabCaseString.of("secrets")) + .build(); + + final software.amazon.awscdk.services.dynamodb.TableV2 tableV2 = new TableV2(this, SafeString.of("Table"), tableProps).getTable(); + + tableV2.grantWriteData(function); + tableV2.grantReadData(function); + + // point to the lambda + final LambdaRestApi lambdaRestApi = LambdaRestApi.Builder.create(this, "RestApi") + .handler(function) + .proxy(true) + .deployOptions(StageOptions.builder().stageName(stage).build()) + .build(); + + // get root resource to add methods + final Resource resource = lambdaRestApi.getRoot().addResource("variables"); + resource.addMethod(StringUtils.toRootUpperCase("ANY")); + resource.addMethod(StringUtils.toRootUpperCase("GET")); + } +} \ No newline at end of file diff --git a/spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/TagUtils.java b/spring-native-aws-service-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/TagUtils.java similarity index 78% rename from spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/TagUtils.java rename to spring-native-aws-service-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/TagUtils.java index 7153229..67d975c 100644 --- a/spring-native-aws-lambda-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/TagUtils.java +++ b/spring-native-aws-service-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/TagUtils.java @@ -24,6 +24,7 @@ import javax.validation.constraints.NotBlank; import java.util.Map; +import static com.coffeebeans.springnativeawslambda.infra.Constants.KEY_APPLICATION_NAME; import static com.coffeebeans.springnativeawslambda.infra.Constants.KEY_COST_CENTRE; import static com.coffeebeans.springnativeawslambda.infra.Constants.KEY_ENV; @@ -31,7 +32,12 @@ public final class TagUtils { public static Map createTags(@NotBlank final String env, - @NotBlank final String costCentre) { - return Map.of(KEY_ENV, env, KEY_COST_CENTRE, costCentre); + @NotBlank final CostCentre costCentre, + @NotBlank final String applicationName) { + return Map.of( + KEY_ENV, env, + KEY_COST_CENTRE, costCentre.getValueAsString(), + KEY_APPLICATION_NAME, applicationName + ); } } diff --git a/spring-native-aws-service-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/ApiBaseStackTest.java b/spring-native-aws-service-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/ApiBaseStackTest.java new file mode 100644 index 0000000..5e95dbf --- /dev/null +++ b/spring-native-aws-service-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/ApiBaseStackTest.java @@ -0,0 +1,225 @@ +///* +// * 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; +// +//import static org.assertj.core.api.Assertions.assertThat; +//import static software.amazon.awscdk.services.ec2.Vpc.Builder.create; +//import static software.amazon.awscdk.services.iam.Role.fromRoleArn; +//import static software.amazon.awscdk.services.lambda.Runtime.PROVIDED_AL2023; +//import static software.amazon.awscdk.services.sns.Topic.fromTopicArn; +//import static software.amazon.awscdk.services.sqs.DeduplicationScope.MESSAGE_GROUP; +// +//import java.io.IOException; +//import java.nio.file.Path; +//import java.util.Map; +//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.services.apigateway.LambdaRestApi; +//import software.amazon.awscdk.services.ec2.Vpc; +//import software.amazon.awscdk.services.lambda.Code; +//import software.amazon.awscdk.services.lambda.Function; +//import software.amazon.awscdk.services.sns.Topic; +//import software.amazon.awscdk.services.sqs.DeadLetterQueue; +//import software.amazon.awscdk.services.sqs.Queue; +// +//class ApiBaseStackTest { +// +// private static final String ENV = "test"; +// +// private ApiBaseStack apiBaseStack; +// +// private final App app = new App(); +// +// private Path lambdaCodePath; +// +// @TempDir +// private static Path TEMP_DIR; +// +// @BeforeEach +// void setUp() throws IOException { +// +// lambdaCodePath = TestLambdaUtils.getTestLambdaCodePath(TEMP_DIR); +// +// this.apiBaseStack = StackUtils.createStack(app, "test-stack", lambdaCodePath.toString(), ENV, +// "test-cdk-bucket", ENV); +// } +// +// @Test +// void should_create_and_return_queue() { +// final String queueId = "test-queue"; +// +// final Queue actual = this.apiBaseStack.createQueue(queueId); +// +// assertThat(actual) +// .isNotNull() +// .hasFieldOrProperty("queueName") +// .hasFieldOrProperty("queueArn") +// .hasFieldOrProperty("queueUrl") +// .hasFieldOrProperty("deadLetterQueue") +// .extracting("fifo") +// .isEqualTo(false); +// } +// +// @Test +// void should_create_and_return_fifo_queue() { +// final String queueId = "test-queue"; +// +// final Queue actual = this.apiBaseStack.createFifoQueue(queueId, true, MESSAGE_GROUP); +// +// assertThat(actual) +// .isNotNull() +// .hasFieldOrProperty("queueName") +// .hasFieldOrProperty("queueArn") +// .hasFieldOrProperty("queueUrl") +// .hasFieldOrProperty("deadLetterQueue") +// .extracting("fifo") +// .isEqualTo(true); +// +// } +// +// @Test +// void should_create_and_return_dead_letter_queue() { +// final String deadLetterQueueId = "test-dead-letter-queue"; +// +// final DeadLetterQueue actual = this.apiBaseStack.createDeadLetterQueue(deadLetterQueueId); +// +// assertThat(actual) +// .isNotNull() +// .hasFieldOrProperty("maxReceiveCount") +// .hasFieldOrProperty("queue"); +// +// assertThat(actual.getMaxReceiveCount()) +// .isEqualTo(3); +// +// assertThat(actual.getQueue()) +// .hasFieldOrProperty("queueName") +// .hasFieldOrProperty("queueArn") +// .hasFieldOrProperty("queueUrl") +// .extracting("fifo") +// .isEqualTo(false); +// } +// +// @Test +// void should_create_and_return_fifo_dead_letter_queue() { +// final String deadLetterQueueId = "test-fifo-dead-letter-queue"; +// +// final DeadLetterQueue actual = this.apiBaseStack.createFifoDeadLetterQueue(deadLetterQueueId, +// true, MESSAGE_GROUP); +// +// assertThat(actual) +// .isNotNull() +// .hasFieldOrProperty("maxReceiveCount") +// .hasFieldOrProperty("queue"); +// +// assertThat(actual.getMaxReceiveCount()) +// .isEqualTo(3); +// +// assertThat(actual.getQueue()) +// .isNotNull() +// .hasFieldOrProperty("queueName") +// .hasFieldOrProperty("queueArn") +// .hasFieldOrProperty("queueUrl") +// .extracting("fifo") +// .isEqualTo(true); +// } +// +// @Test +// void should_create_and_return_topic() { +// final String topicId = "test-topic"; +// +// final Topic actual = this.apiBaseStack.createTopic(topicId); +// +// assertThat(actual) +// .isNotNull() +// .hasFieldOrProperty("topicName") +// .hasFieldOrProperty("topicArn") +// .extracting("fifo") +// .isEqualTo(false); +// } +// +// @Test +// void should_create_and_return_fifo_topic() { +// final String topicId = "test-topic"; +// +// final Topic actual = this.apiBaseStack.createFifoTopic(topicId, true, true); +// +// assertThat(actual) +// .isNotNull() +// .hasFieldOrProperty("topicName") +// .hasFieldOrProperty("topicArn") +// .extracting("fifo") +// .isEqualTo(true); +// } +// +// @Test +// void should_create_and_return_lambda_function() { +// final Vpc vpc = create(this.apiBaseStack, "test-vpc").build(); +// +// final Function actual = this.apiBaseStack.createFunction(vpc, +// "test-function", +// "com.coffeebeans.springnativeawslambda.infra.lambda.CustomRuntime2Function::handleRequest", +// Code.fromAsset(this.lambdaCodePath.toString()), +// fromTopicArn(this.apiBaseStack, "success-topic", "arn:aws:sns:us-east-1:***:success-topic"), +// fromRoleArn(this.apiBaseStack, "test-role", "arn:aws:iam::***:role/test-role"), +// Map.of("Account", "***")); +// +// assertThat(actual) +// .isNotNull() +// .hasFieldOrProperty("functionArn") +// .hasFieldOrProperty("role") +// .hasFieldOrProperty("functionName") +// .hasFieldOrProperty("functionArn") +// .hasFieldOrProperty("env") +// .hasFieldOrProperty("architecture") +// .hasFieldOrProperty("runtime") +// .hasFieldOrProperty("timeout"); +// +// assertThat(actual.getRuntime()) +// .isEqualTo(PROVIDED_AL2023); +// } +// +// @Test +// void should_create_and_return_lambda_rest_api() { +// final Vpc vpc = create(this.apiBaseStack, "test-vpc").build(); +// +// final Function function = this.apiBaseStack.createFunction(vpc, +// "test-function", +// "com.coffeebeans.springnativeawslambda.infra.lambda.CustomRuntime2Function::handleRequest", +// Code.fromAsset(this.lambdaCodePath.toString()), +// fromTopicArn(this.apiBaseStack, "success-topic", "arn:aws:sns:us-east-1:***:success-topic"), +// fromRoleArn(this.apiBaseStack, "test-role", "arn:aws:iam::***:role/test-role"), +// Map.of("Account", "***")); +// +// final LambdaRestApi actual = this.apiBaseStack.createLambdaRestApi("test", "rest-api", "name", +// "POST", function, false); +// +// assertThat(actual) +// .isNotNull() +// .hasFieldOrProperty("deploymentStage") +// .hasFieldOrProperty("env") +// .hasFieldOrProperty("restApiName") +// .hasFieldOrProperty("root") +// .hasFieldOrProperty("url") +// .hasFieldOrProperty("restApiRootResourceId") +// .hasFieldOrProperty("restApiId") +// .hasFieldOrProperty("methods"); +// } +//} \ No newline at end of file diff --git a/spring-native-aws-service-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/LambdaTest.java b/spring-native-aws-service-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/LambdaTest.java new file mode 100644 index 0000000..1018572 --- /dev/null +++ b/spring-native-aws-service-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/LambdaTest.java @@ -0,0 +1,114 @@ +/* + * 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; + +import static com.coffeebeans.cdk.assertion.CDKStackAssert.assertThat; + +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class LambdaTest extends TemplateSupport { + + public static final String TEST = "test"; + + @Test + void should_have_lambda_function() { + + assertThat(template) + .containsFunction("^Lambda[A-Z0-9]{8}$") + .hasHandler("org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest") + .hasCode("cdk-cbcore-assets-ap-southeast-2-\\$\\{AWS\\:\\:Region\\}", "(.*).zip") + .hasRole("^Role[A-Z0-9]{8}$") + .hasDependency("^RoleDefaultPolicy[A-Z0-9]{8}$") + .hasDependency("^Role[A-Z0-9]{8}$") + .hasTag("COST_CENTRE", "cbcore") + .hasTag("ENVIRONMENT", TEST) + .hasTag("APPLICATION_NAME", "spring-native-aws-function") + .hasEnvironmentVariable("ENVIRONMENT", TEST) + .hasEnvironmentVariable("SPRING_PROFILES_ACTIVE", TEST) + .hasDescription("Example of a Spring Native AWS Lambda Function using CDK") + .hasMemorySize(512) + .hasRuntime("provided.al2023") + .hasTimeout(3) + .hasDeadLetterTarget("^LambdaDeadLetterQueue[A-Z0-9]{8}$"); + } + + @Test + void should_have_role_with_AWSLambdaBasicExecutionRole_policy_to_assume_by_lambda() { + final Map principal = Map.of("Service", "lambda.amazonaws.com"); + final String effect = "Allow"; + final String policyDocumentVersion = "2012-10-17"; + final String managedPolicyArn = ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"; + + assertThat(template) + .containsRole("^Role[A-Z0-9]{8}$") + .hasManagedPolicyArn(managedPolicyArn) + .hasAssumeRolePolicyDocument(principal, null, effect, policyDocumentVersion, "sts:AssumeRole"); + } + + @Test + void should_have_default_policy_to_allow_lambda_send_messages_to_sqs() { + + assertThat(template) + .containsPolicy("^RoleDefaultPolicy[A-Z0-9]{8}$") + .isAssociatedWithRole("^Role[A-Z0-9]{8}$") + .hasPolicyDocumentStatement(null, + "^LambdaDeadLetterQueue[A-Z0-9]{8}$", + "sqs:SendMessage", + "Allow", + "2012-10-17"); + } + + @Test + void should_have_event_invoke_config_for_success_and_failure() { + + assertThat(template) + .containsLambdaEventInvokeConfig("^LambdaEventInvokeConfig[A-Z0-9]{8}$") + .hasQualifier("$LATEST") + .hasFunctionName("^Lambda[A-Z0-9]{8}$") + .hasMaximumEventAgeInSeconds(60) + .hasMaximumRetryAttempts(2); + } + + @ParameterizedTest + @CsvSource( + { + "^RestApiproxyANYApiPermissionCbcoreSpringNativeAwsFunctionStakeRestApi[A-Z0-9]{8}ANYproxy[A-Z0-9]{8}$, ^arn:aws:execute-api::ap-southeast-2:RestApi[A-Z0-9]{8}/RestApiDeploymentStagetest[A-Z0-9]{8}/\\*/\\*$", + "^RestApiproxyANYApiPermissionTestCbcoreSpringNativeAwsFunctionStakeRestApi[A-Z0-9]{8}ANYproxy[A-Z0-9]{8}$, ^arn:aws:execute-api::ap-southeast-2:RestApi[A-Z0-9]{8}/test-invoke-stage/\\*/\\*$", + "^RestApiANYApiPermissionCbcoreSpringNativeAwsFunctionStakeRestApi[A-Z0-9]{8}ANY[A-Z0-9]{8}$, ^arn:aws:execute-api::ap-southeast-2:RestApi[A-Z0-9]{8}/RestApiDeploymentStagetest[A-Z0-9]{8}/\\*/$", + "^RestApiANYApiPermissionTestCbcoreSpringNativeAwsFunctionStakeRestApi[A-Z0-9]{8}ANY[A-Z0-9]{8}$, ^arn:aws:execute-api::ap-southeast-2:RestApi[A-Z0-9]{8}/test-invoke-stage/\\*/$", + "^RestApiproxyANYApiPermissionCbcoreSpringNativeAwsFunctionStakeRestApi[A-Z0-9]{8}ANYproxy[A-Z0-9]{8}$, ^arn:aws:execute-api::ap-southeast-2:RestApi[A-Z0-9]{8}/RestApiDeploymentStagetest[A-Z0-9]{8}/\\*/\\*$", + "^RestApivariablesANYApiPermissionTestCbcoreSpringNativeAwsFunctionStakeRestApi[A-Z0-9]{8}ANYvariables[A-Z0-9]{8}$, ^arn:aws:execute-api::ap-southeast-2:RestApi[A-Z0-9]{8}/test-invoke-stage/\\*/variables$", + "^RestApivariablesGETApiPermissionCbcoreSpringNativeAwsFunctionStakeRestApi[A-Z0-9]{8}GETvariables[A-Z0-9]{8}$, ^arn:aws:execute-api::ap-southeast-2:RestApi[A-Z0-9]{8}/RestApiDeploymentStagetest[A-Z0-9]{8}/GET/variables$", + "^RestApivariablesGETApiPermissionTestCbcoreSpringNativeAwsFunctionStakeRestApi[A-Z0-9]{8}GETvariables[A-Z0-9]{8}$, ^arn:aws:execute-api::ap-southeast-2:RestApi[A-Z0-9]{8}/test-invoke-stage/GET/variables$" + } + ) + void should_have_permission_to_allow_rest_api_to_call_lambda(final String lambdaPermissionResourceId, + final String sourceArnPattern) { + + assertThat(template) + .containsLambdaPermission(lambdaPermissionResourceId) + .hasLambdaPermission("^Lambda[A-Z0-9]{8}$", + "lambda:InvokeFunction", + "apigateway.amazonaws.com", + sourceArnPattern); + } +} \ No newline at end of file diff --git a/spring-native-aws-service-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/RestApiTest.java b/spring-native-aws-service-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/RestApiTest.java new file mode 100644 index 0000000..6a36215 --- /dev/null +++ b/spring-native-aws-service-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/RestApiTest.java @@ -0,0 +1,166 @@ +///* +// * 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; +// +//import org.junit.jupiter.api.Test; +//import software.amazon.awscdk.assertions.Match; +// +//import java.util.List; +//import java.util.Map; +// +//import static com.coffeebeans.cdk.assertion.CDKStackAssert.assertThat; +//import static com.coffeebeans.springnativeawslambda.infra.Constants.KEY_COST_CENTRE; +// +//class RestApiTest extends TemplateSupport { +// +// public static final String TEST = "test"; +// +// @Test +// void should_have_rest_api() { +// +// assertThat(template) +// .containsRestApi("^LambdaRestApi[a-zA-Z0-9]{8}$") +// .hasTag("APPLICATION_NAME", "apigateway-cdk-example") +// .hasTag("COST_CENTRE", "CoffeeBeans") +// .hasTag("ENVIRONMENT", TEST); +// } +// +// @Test +// void should_have_rest_api_account() { +// final String cloudWatchRoleArn = "springnativeawslambdafunctionrestapiCloudWatchRole(.*)"; +// final String dependency = "springnativeawslambdafunctionrestapi(.*)"; +// +// assertThat(template) +// .containsRestApiAccountWithCloudWatchRoleArn(cloudWatchRoleArn) +// .hasDependency(dependency) +// .hasUpdateReplacePolicy("Retain") +// .hasDeletionPolicy("Retain"); +// } +// +// @Test +// void should_have_rest_api_deployment() { +// +// assertThat(template) +// .containsRestApiDeployment("springnativeawslambdafunctionrestapi(.*)") +// .hasDependency("springnativeawslambdafunctionrestapiproxyANY(.*)") +// .hasDependency("springnativeawslambdafunctionrestapiproxy(.*)") +// .hasDependency("springnativeawslambdafunctionrestapiANY(.*)") +// .hasDependency("springnativeawslambdafunctionrestapinamePOST(.*)") +// .hasDependency("springnativeawslambdafunctionrestapiname(.*)") +// .hasDescription("Automatically created by the RestApi construct"); +// } +// +// @Test +// void should_have_rest_api_stage() { +// +// assertThat(template) +// .containsRestApiStage("test") +// .hasRestApiId(("springnativeawslambdafunctionrestapi(.*)")) +// .hasDeploymentId(("springnativeawslambdafunctionrestapiDeployment(.*)")) +// .hasDependency("springnativeawslambdafunctionrestapiAccount(.*)") +// .hasTag("COST_CENTRE", KEY_COST_CENTRE) +// .hasTag("ENV", TEST); +// } +// +// @Test +// void should_have_proxy_resource() { +// final String restApiId = "springnativeawslambdafunctionrestapi(.*)"; +// +// final Map> parentId = Map.of( +// "Fn::GetAtt", List.of(Match.stringLikeRegexp(restApiId), "RootResourceId") +// ); +// +// assertThat(template) +// .containsRestApiResource("{proxy+}", restApiId, parentId) +// .hasRestApiId(restApiId) +// .hasParentId(restApiId); +// } +// +// @Test +// void should_have_account_resource() { +// final String restApiId = "springnativeawslambdafunctionrestapi(.*)"; +// +// final Map> parentId = Map.of( +// "Fn::GetAtt", List.of(Match.stringLikeRegexp(restApiId), "RootResourceId") +// ); +// +// assertThat(template) +// .containsRestApiResource("name", restApiId, parentId) +// .hasRestApiId(restApiId) +// .hasParentId(restApiId); +// } +// +// @Test +// void should_have_post_method() { +// +// final String integrationType = "AWS_PROXY"; +// final String httpMethod = "POST"; +// +// assertThat(template) +// .containsNonRootRestApiMethod(httpMethod, "springnativeawslambdafunctionrestapiname(.*)") +// .hasHttpMethod(httpMethod) +// .hasIntegration(httpMethod, integrationType) +// .hasAuthorizationType("NONE") +// .hasRestApiId(("springnativeawslambdafunctionrestapi(.*)")); +// } +// +// @Test +// void should_have_proxy_method() { +// +// final String method = "ANY"; +// final String integrationType = "AWS_PROXY"; +// +// assertThat(template) +// .containsApiMethod( "springnativeawslambdafunctionrestapiproxy(.*)") +// .hasHttpMethod(method) +// .hasIntegration("POST", integrationType) +// .hasAuthorizationType("NONE") +// .hasRestApiId(("springnativeawslambdafunctionrestapi(.*)")); +// } +// +// @Test +// void should_have_root_method() { +// +// final String method = "ANY"; +// final String integrationType = "AWS_PROXY"; +// +// assertThat(template) +// .containsRootRestApiMethod(method, "springnativeawslambdafunctionrestapi(.*)") +// .hasHttpMethod(method) +// .hasIntegration("POST", integrationType) +// .hasAuthorizationType("NONE") +// .hasRestApiId(("springnativeawslambdafunctionrestapi(.*)")); +// } +// +// @Test +// void should_have_role_with_AmazonAPIGatewayPushToCloudWatchLogs_policy_for_rest_api_to_push_logs_to_cloud_watch() { +// final String principal = "apigateway.amazonaws.com"; +// final String effect = "Allow"; +// final String policyDocumentVersion = "2012-10-17"; +// final String managedPolicyArn = ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs"; +// +// assertThat(template) +// .containsRoleWithManagedPolicyArn(managedPolicyArn) +// .hasAssumeRolePolicyDocument(principal, +// null, +// effect, +// policyDocumentVersion, +// "sts:AssumeRole"); +// } +//} \ No newline at end of file diff --git a/spring-native-aws-service-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/TagUtilsTest.java b/spring-native-aws-service-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/TagUtilsTest.java new file mode 100644 index 0000000..46d95ba --- /dev/null +++ b/spring-native-aws-service-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/TagUtilsTest.java @@ -0,0 +1,44 @@ +///* +// * 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; +// +//import org.junit.jupiter.api.Test; +// +//import java.util.Map; +// +//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 { +// +// @Test +// void should_create_and_return_tag_map() { +// // given +// final String env = "test"; +// final String costCentre = "coffeeBeans-core"; +// +// // when +// final Map tags = TagUtils.createTags(env, costCentre); +// +// assertThat(tags) +// .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-service-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/TemplateSupport.java similarity index 51% rename from spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/TemplateSupport.java rename to spring-native-aws-service-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/TemplateSupport.java index 349ff25..9f5e00f 100644 --- a/spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/TemplateSupport.java +++ b/spring-native-aws-service-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/TemplateSupport.java @@ -18,6 +18,15 @@ package com.coffeebeans.springnativeawslambda.infra; +import static com.coffeebeans.springnativeawslambda.infra.Constants.KEY_APPLICATION_VALUE; +import static com.coffeebeans.springnativeawslambda.infra.Constants.KEY_COST_CENTRE; +import static com.coffeebeans.springnativeawslambda.infra.Environment.COFFEE_BEANS_TEST_111111111111_AP_SOUTHEAST_2; +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; @@ -25,42 +34,33 @@ 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-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, KEY_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, CostCentre.COFFEE_BEANS, KEY_APPLICATION_VALUE); + final Application app = new Application(); + final SpringNativeAwsFunctionStack stack = new SpringNativeAwsFunctionStack(app, COFFEE_BEANS_TEST_111111111111_AP_SOUTHEAST_2, lambdaCodePath.toString(), 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/TestLambdaUtils.java b/spring-native-aws-service-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/TestLambdaUtils.java similarity index 100% rename from spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/TestLambdaUtils.java rename to spring-native-aws-service-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/TestLambdaUtils.java diff --git a/spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/TopicTest.java b/spring-native-aws-service-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/TopicTest.java similarity index 87% rename from spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/TopicTest.java rename to spring-native-aws-service-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/TopicTest.java index 401bc02..70f52fe 100644 --- a/spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/TopicTest.java +++ b/spring-native-aws-service-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/TopicTest.java @@ -20,7 +20,7 @@ import org.junit.jupiter.api.Test; -import static cloud.pianola.cdk.fluent.assertion.CDKStackAssert.assertThat; +import static com.coffeebeans.cdk.assertion.CDKStackAssert.assertThat; import static com.coffeebeans.springnativeawslambda.infra.Constants.KEY_COST_CENTRE; class TopicTest extends TemplateSupport { @@ -30,7 +30,7 @@ class TopicTest extends TemplateSupport { @Test void should_have_dead_letter_topic() { assertThat(template) - .containsTopic("spring-native-aws-lambda-function-dead-letter-topic") + .containsTopic("spring-native-aws-function-dead-letter-topic") .hasTag("COST_CENTRE", KEY_COST_CENTRE) .hasTag("ENV", TEST); } diff --git a/spring-native-aws-lambda-infra/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/spring-native-aws-service-infra/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker similarity index 100% rename from spring-native-aws-lambda-infra/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker rename to spring-native-aws-service-infra/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker diff --git a/spring-native-aws-lambda-function/pom.xml b/spring-native-aws-service/pom.xml similarity index 95% rename from spring-native-aws-lambda-function/pom.xml rename to spring-native-aws-service/pom.xml index 0c4b28c..18ff149 100644 --- a/spring-native-aws-lambda-function/pom.xml +++ b/spring-native-aws-service/pom.xml @@ -28,7 +28,7 @@ ${revision} - spring-native-aws-lambda-function + spring-native-aws-service ${project.artifactId} jar Demo project for Spring cloud function with graalvm native image @@ -100,6 +100,11 @@ org.springframework.boot spring-boot-starter-validation + + + io.awspring.cloud + spring-cloud-aws-starter-dynamodb + @@ -161,6 +166,7 @@ org.graalvm.buildtools native-maven-plugin + 0.10.1 com.coffeebeans.springnativeawslambda.Application @@ -170,6 +176,9 @@ --strict-image-heap -H:+ReportExceptionStackTraces + + true + diff --git a/spring-native-aws-lambda-function/src/assembly/native.xml b/spring-native-aws-service/src/assembly/native.xml similarity index 96% rename from spring-native-aws-lambda-function/src/assembly/native.xml rename to spring-native-aws-service/src/assembly/native.xml index f04e937..dd19a55 100644 --- a/spring-native-aws-lambda-function/src/assembly/native.xml +++ b/spring-native-aws-service/src/assembly/native.xml @@ -40,7 +40,7 @@ true 0775 - spring-native-aws-lambda-function + spring-native-aws-function diff --git a/spring-native-aws-lambda-function/src/main/java/com/coffeebeans/springnativeawslambda/Application.java b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/Application.java similarity index 100% rename from spring-native-aws-lambda-function/src/main/java/com/coffeebeans/springnativeawslambda/Application.java rename to spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/Application.java diff --git a/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/DynamoDbConfig.java b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/DynamoDbConfig.java new file mode 100644 index 0000000..38fa1ca --- /dev/null +++ b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/DynamoDbConfig.java @@ -0,0 +1,21 @@ +package com.coffeebeans.springnativeawslambda; + +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.validation.annotation.Validated; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; + +@Validated +@AutoConfiguration +@ConditionalOnProperty(name = "spring.cloud.aws.dynamodb.enabled", havingValue = "true", matchIfMissing = true) +public class DynamoDbConfig { + + @Bean + public DynamoDbEnhancedClient enhancedClient(final DynamoDbClient dynamoDbClient) { + return DynamoDbEnhancedClient.builder() + .dynamoDbClient(dynamoDbClient) + .build(); + } +} \ No newline at end of file diff --git a/spring-native-aws-lambda-function/src/main/java/com/coffeebeans/springnativeawslambda/LambdaExceptionHandler.java b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/LambdaExceptionHandler.java similarity index 100% rename from spring-native-aws-lambda-function/src/main/java/com/coffeebeans/springnativeawslambda/LambdaExceptionHandler.java rename to spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/LambdaExceptionHandler.java diff --git a/spring-native-aws-lambda-function/src/main/java/com/coffeebeans/springnativeawslambda/ReflectionRuntimeHints.java b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/ReflectionRuntimeHints.java similarity index 75% rename from spring-native-aws-lambda-function/src/main/java/com/coffeebeans/springnativeawslambda/ReflectionRuntimeHints.java rename to spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/ReflectionRuntimeHints.java index 765b03f..d8705f8 100644 --- a/spring-native-aws-lambda-function/src/main/java/com/coffeebeans/springnativeawslambda/ReflectionRuntimeHints.java +++ b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/ReflectionRuntimeHints.java @@ -18,30 +18,38 @@ package com.coffeebeans.springnativeawslambda; +import com.coffeebeans.springnativeawslambda.entity.Secret; +import com.coffeebeans.springnativeawslambda.repository.SecretRepository; +import io.awspring.cloud.dynamodb.DynamoDbTemplate; import java.util.List; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; import com.coffeebeans.springnativeawslambda.model.Request; import com.coffeebeans.springnativeawslambda.model.Response; +import java.util.Map; import org.joda.time.DateTime; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.aot.hint.TypeReference; import org.springframework.lang.Nullable; +import software.amazon.awssdk.enhanced.dynamodb.DefaultAttributeConverterProvider; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.BeanTableSchemaAttributeTags; +import software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema; public class ReflectionRuntimeHints implements RuntimeHintsRegistrar { @Override - public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { + public void registerHints(final RuntimeHints hints, @Nullable final ClassLoader classLoader) { final List typeReferences = List.of( TypeReference.of(DateTime.class), TypeReference.of(Response.class), TypeReference.of(Request.class), TypeReference.of(APIGatewayProxyResponseEvent.class), TypeReference.of(APIGatewayProxyRequestEvent.class), - TypeReference.of(APIGatewayProxyRequestEvent.ProxyRequestContext.class), - TypeReference.of(APIGatewayProxyRequestEvent.RequestIdentity.class)); + TypeReference.of(APIGatewayProxyRequestEvent.ProxyRequestContext.class) + ); hints.reflection().registerTypes(typeReferences, builder -> builder.withMembers(MemberCategory.values())); } diff --git a/spring-native-aws-lambda-function/src/main/java/com/coffeebeans/springnativeawslambda/ResourcesRuntimeHints.java b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/ResourcesRuntimeHints.java similarity index 91% rename from spring-native-aws-lambda-function/src/main/java/com/coffeebeans/springnativeawslambda/ResourcesRuntimeHints.java rename to spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/ResourcesRuntimeHints.java index 9ddaa72..ba11981 100644 --- a/spring-native-aws-lambda-function/src/main/java/com/coffeebeans/springnativeawslambda/ResourcesRuntimeHints.java +++ b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/ResourcesRuntimeHints.java @@ -26,5 +26,6 @@ public class ResourcesRuntimeHints implements RuntimeHintsRegistrar { @Override public void registerHints(final RuntimeHints hints, final ClassLoader classLoader) { hints.resources().registerPattern("com/amazonaws/lambda/thirdparty/org/joda/time/tz/*"); + hints.resources().registerPattern("io/awspring/cloud/core/SpringCloudClientConfiguration.properties"); } } \ No newline at end of file diff --git a/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/entity/Secret.java b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/entity/Secret.java new file mode 100644 index 0000000..b13d784 --- /dev/null +++ b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/entity/Secret.java @@ -0,0 +1,34 @@ +package com.coffeebeans.springnativeawslambda.entity; + +import java.io.Serializable; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@DynamoDbBean +public class Secret implements Serializable { + + private String env; + + private String costCentre; + + private String applicationName; + + private String partitionKey; + + private Map variables; + + @DynamoDbPartitionKey + public String getPartitionKey() { + return env + costCentre + applicationName; + } + +} diff --git a/spring-native-aws-lambda-function/src/main/java/com/coffeebeans/springnativeawslambda/function/ExampleFunction.java b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/function/ExampleFunction.java similarity index 79% rename from spring-native-aws-lambda-function/src/main/java/com/coffeebeans/springnativeawslambda/function/ExampleFunction.java rename to spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/function/ExampleFunction.java index b24fb38..ca59b75 100644 --- a/spring-native-aws-lambda-function/src/main/java/com/coffeebeans/springnativeawslambda/function/ExampleFunction.java +++ b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/function/ExampleFunction.java @@ -20,14 +20,18 @@ import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; +import com.coffeebeans.springnativeawslambda.entity.Secret; import com.coffeebeans.springnativeawslambda.model.Request; import com.coffeebeans.springnativeawslambda.model.Response; +import com.coffeebeans.springnativeawslambda.repository.SecretRepository; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.validation.constraints.NotNull; +import java.util.Map; import java.util.function.Function; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.validation.annotation.Validated; @@ -39,8 +43,15 @@ public class ExampleFunction implements private final ObjectMapper objectMapper; - public ExampleFunction(@NotNull final ObjectMapper objectMapper) { + private SecretRepository secretRepository; + + public ExampleFunction( + @NotNull final SecretRepository secretRepository, + @NotNull final ObjectMapper objectMapper + ) { this.objectMapper = objectMapper; + this.secretRepository = secretRepository; + } /** @@ -62,6 +73,13 @@ public APIGatewayProxyResponseEvent apply(final APIGatewayProxyRequestEvent prox .saved(true) .build(); + this.secretRepository.save(Secret.builder() + .env("dev") + .costCentre("coffeebeans") + .applicationName("native-lambda") + .variables(Map.of("ENV", "dev", "PASSWORD", "changeit")) + .build()); + log.info("Converted request into a response."); return new APIGatewayProxyResponseEvent() diff --git a/spring-native-aws-lambda-function/src/main/java/com/coffeebeans/springnativeawslambda/model/Request.java b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/model/Request.java similarity index 100% rename from spring-native-aws-lambda-function/src/main/java/com/coffeebeans/springnativeawslambda/model/Request.java rename to spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/model/Request.java diff --git a/spring-native-aws-lambda-function/src/main/java/com/coffeebeans/springnativeawslambda/model/Response.java b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/model/Response.java similarity index 100% rename from spring-native-aws-lambda-function/src/main/java/com/coffeebeans/springnativeawslambda/model/Response.java rename to spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/model/Response.java diff --git a/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/repository/SecretRepository.java b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/repository/SecretRepository.java new file mode 100644 index 0000000..b72ff02 --- /dev/null +++ b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/repository/SecretRepository.java @@ -0,0 +1,50 @@ +package com.coffeebeans.springnativeawslambda.repository; + +import com.coffeebeans.springnativeawslambda.entity.Secret; +import jakarta.validation.constraints.NotNull; +import java.util.Map; +import org.springframework.stereotype.Repository; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttribute; +import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags; +import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; + +@Repository +public class SecretRepository { + + private final DynamoDbEnhancedClient enhancedClient; + + public SecretRepository(@NotNull final DynamoDbEnhancedClient enhancedClient) { + this.enhancedClient = enhancedClient; + } + +// public Secret findById(String id) { +// final Key key = Key.builder().partitionValue(Integer.valueOf(id)).build(); +// +// final Secret secret = this.dynamoDbTemplate.load(key, Secret.class); +// return Optional.ofNullable(secret) +// .orElseGet(Secret::new); +// } + + public void save(final Secret secret) { + + final TableSchema tableSchema = StaticTableSchema.builder(Secret.class) + .newItemSupplier(Secret::new) + .addAttribute(String.class, a -> a.name("id") + .getter(Secret::getPartitionKey) + .setter(Secret::setPartitionKey) + .tags(StaticAttributeTags.primaryPartitionKey()) + ) + .addAttribute(EnhancedType.mapOf(String.class, String.class), a -> a.name("variables") + .getter(Secret::getVariables) + .setter(Secret::setVariables)) + .build(); + + final DynamoDbTable mappedTable = enhancedClient.table("dev-coffeebeans-secret", tableSchema); + + mappedTable.putItem(secret); + } +} diff --git a/spring-native-aws-lambda-function/src/main/resources/application-local.yml b/spring-native-aws-service/src/main/resources/application-local.yml similarity index 86% rename from spring-native-aws-lambda-function/src/main/resources/application-local.yml rename to spring-native-aws-service/src/main/resources/application-local.yml index 32d68c5..6261f96 100644 --- a/spring-native-aws-lambda-function/src/main/resources/application-local.yml +++ b/spring-native-aws-service/src/main/resources/application-local.yml @@ -15,4 +15,10 @@ spring: main: - web-application-type: servlet \ No newline at end of file + web-application-type: servlet + cloud: + aws: + dynamodb: + endpoint: http://localhost:8000 + + diff --git a/spring-native-aws-lambda-function/src/main/resources/application.yml b/spring-native-aws-service/src/main/resources/application.yml similarity index 87% rename from spring-native-aws-lambda-function/src/main/resources/application.yml rename to spring-native-aws-service/src/main/resources/application.yml index ec49d88..4279a4a 100644 --- a/spring-native-aws-lambda-function/src/main/resources/application.yml +++ b/spring-native-aws-service/src/main/resources/application.yml @@ -32,4 +32,9 @@ spring: enabled: false debug: false definition: exampleFunction -debug: false \ No newline at end of file + aws: + dynamodb: + enabled: true + endpoint: https://dynamodb.ap-southeast-2.amazonaws.com + table-prefix: dev-coffeebeans- +debug: false diff --git a/spring-native-aws-lambda-function/src/shell/native/bootstrap b/spring-native-aws-service/src/shell/native/bootstrap similarity index 95% rename from spring-native-aws-lambda-function/src/shell/native/bootstrap rename to spring-native-aws-service/src/shell/native/bootstrap index 33d505c..b3118d4 100755 --- a/spring-native-aws-lambda-function/src/shell/native/bootstrap +++ b/spring-native-aws-service/src/shell/native/bootstrap @@ -21,4 +21,4 @@ set -euo pipefail cd ${LAMBDA_TASK_ROOT:-.} -./spring-native-aws-lambda-function \ No newline at end of file +./spring-native-aws-function \ No newline at end of file diff --git a/spring-native-aws-lambda-function/src/test/java/com/coffeebeans/springnativeawslambda/ApplicationIT.java b/spring-native-aws-service/src/test/java/com/coffeebeans/springnativeawslambda/ApplicationIT.java similarity index 100% rename from spring-native-aws-lambda-function/src/test/java/com/coffeebeans/springnativeawslambda/ApplicationIT.java rename to spring-native-aws-service/src/test/java/com/coffeebeans/springnativeawslambda/ApplicationIT.java diff --git a/spring-native-aws-service/src/test/java/com/coffeebeans/springnativeawslambda/function/ExampleFunctionTest.java b/spring-native-aws-service/src/test/java/com/coffeebeans/springnativeawslambda/function/ExampleFunctionTest.java new file mode 100644 index 0000000..9199915 --- /dev/null +++ b/spring-native-aws-service/src/test/java/com/coffeebeans/springnativeawslambda/function/ExampleFunctionTest.java @@ -0,0 +1,72 @@ +///* +// * 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.function; +// +//import static org.assertj.core.api.Assertions.assertThat; +//import static org.assertj.core.api.Assertions.assertThatThrownBy; +// +//import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +//import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; +//import com.coffeebeans.springnativeawslambda.function.ExampleFunction; +//import com.fasterxml.jackson.core.JsonProcessingException; +//import com.fasterxml.jackson.databind.ObjectMapper; +//import org.junit.jupiter.api.BeforeEach; +//import org.junit.jupiter.api.Test; +//import org.mockito.Spy; +// +//class ExampleFunctionTest { +// +// private ExampleFunction exampleFunction; +// +// @Spy +// private ObjectMapper objectMapper = new ObjectMapper(); +// +// @BeforeEach +// void setUp() { +// exampleFunction = new ExampleFunction(objectMapper); +// } +// +// @Test +// void should_return_APIGatewayProxyResponseEvent() { +// final String requestBody = "{\"name\":\"Coffeebeans\"}"; +// final String responseBody = "{\"name\":\"Coffeebeans\",\"saved\":true}"; +// +// final APIGatewayProxyRequestEvent request = new APIGatewayProxyRequestEvent() +// .withBody(requestBody); +// +// final APIGatewayProxyResponseEvent apiGatewayProxyResponseEvent = new APIGatewayProxyResponseEvent() +// .withStatusCode(200) +// .withBody(responseBody); +// +// final APIGatewayProxyResponseEvent actual = exampleFunction.apply(request); +// +// assertThat(actual) +// .isEqualTo(apiGatewayProxyResponseEvent); +// } +// +// @Test +// void should_throw_JsonProcessingException() { +// +// final APIGatewayProxyRequestEvent request = new APIGatewayProxyRequestEvent() +// .withBody("Coffeebeans"); +// +// assertThatThrownBy(() -> exampleFunction.apply(request)) +// .isInstanceOf(JsonProcessingException.class); +// } +//} \ No newline at end of file From 1b0383bfdec2894432e730f83971ca65dbcc5284 Mon Sep 17 00:00:00 2001 From: muhamadto Date: Sun, 24 Mar 2024 14:48:05 +1100 Subject: [PATCH 2/6] Renaming some modules --- .github/workflows/release.yml | 4 +++ PULL_REQUEST_TEMPLATE.md | 2 +- pom.xml | 21 ++--------- spring-native-aws-service-infra/pom.xml | 8 ++--- spring-native-aws-service/pom.xml | 1 - .../ReflectionRuntimeHints.java | 14 ++------ .../springnativeawslambda/entity/Secret.java | 18 +++++++--- .../function/ExampleFunction.java | 21 +++-------- .../springnativeawslambda/model/Request.java | 35 ------------------- .../model/{Response.java => Secret.java} | 27 ++++++++++---- .../repository/SecretRepository.java | 17 +++++++-- 11 files changed, 65 insertions(+), 103 deletions(-) delete mode 100644 spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/model/Request.java rename spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/model/{Response.java => Secret.java} (73%) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e0fbf58..b10cfbd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -51,6 +51,10 @@ jobs: path: ~/.m2 key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} restore-keys: ${{ runner.os }}-m2 + - name: maven-settings-xml-action + uses: whelk-io/maven-settings-xml-action@v22 + with: + servers: '[{ "id": "github", "password": "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" }]' - name: Build with Maven env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index 71c43c0..0cabf7e 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -7,6 +7,6 @@ -- [ ] Describe what you did in the pull request description +- [ ] Describe what you did in the pull secret description - [ ] Add Unit and Integration Tests - at least 80% unit tests for new code. - [ ] Added or updated documentation \ No newline at end of file diff --git a/pom.xml b/pom.xml index 71c0988..67019e0 100644 --- a/pom.xml +++ b/pom.xml @@ -84,7 +84,6 @@ - io.awspring.cloud spring-cloud-aws-dependencies @@ -108,14 +107,8 @@ - com.coffeebeans - coffeebeans-cdk-core - 1.0-SNAPSHOT - - - - com.coffeebeans - coffeebeans-cdk-assertions + io.sandpipers + sandpipers-cdk-bom 1.0-SNAPSHOT @@ -298,16 +291,6 @@ - - - github - https://public:0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@maven.pkg.github.com/muhamadto/* - - true - - - - Apache License, Version 2.0 diff --git a/spring-native-aws-service-infra/pom.xml b/spring-native-aws-service-infra/pom.xml index 2dc2eb7..84f34c7 100644 --- a/spring-native-aws-service-infra/pom.xml +++ b/spring-native-aws-service-infra/pom.xml @@ -56,8 +56,8 @@ - com.coffeebeans - coffeebeans-cdk-core + io.sandpipers + sandpipers-cdk-core @@ -99,8 +99,8 @@ - com.coffeebeans - coffeebeans-cdk-assertions + io.sandpipers + sandpipers-cdk-assertions test diff --git a/spring-native-aws-service/pom.xml b/spring-native-aws-service/pom.xml index 18ff149..5b7677b 100644 --- a/spring-native-aws-service/pom.xml +++ b/spring-native-aws-service/pom.xml @@ -166,7 +166,6 @@ org.graalvm.buildtools native-maven-plugin - 0.10.1 com.coffeebeans.springnativeawslambda.Application diff --git a/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/ReflectionRuntimeHints.java b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/ReflectionRuntimeHints.java index d8705f8..ceeeb72 100644 --- a/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/ReflectionRuntimeHints.java +++ b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/ReflectionRuntimeHints.java @@ -18,25 +18,16 @@ package com.coffeebeans.springnativeawslambda; -import com.coffeebeans.springnativeawslambda.entity.Secret; -import com.coffeebeans.springnativeawslambda.repository.SecretRepository; -import io.awspring.cloud.dynamodb.DynamoDbTemplate; import java.util.List; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; -import com.coffeebeans.springnativeawslambda.model.Request; -import com.coffeebeans.springnativeawslambda.model.Response; -import java.util.Map; +import com.coffeebeans.springnativeawslambda.model.Secret; import org.joda.time.DateTime; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.aot.hint.TypeReference; import org.springframework.lang.Nullable; -import software.amazon.awssdk.enhanced.dynamodb.DefaultAttributeConverterProvider; -import software.amazon.awssdk.enhanced.dynamodb.TableSchema; -import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.BeanTableSchemaAttributeTags; -import software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema; public class ReflectionRuntimeHints implements RuntimeHintsRegistrar { @@ -44,8 +35,7 @@ public class ReflectionRuntimeHints implements RuntimeHintsRegistrar { public void registerHints(final RuntimeHints hints, @Nullable final ClassLoader classLoader) { final List typeReferences = List.of( TypeReference.of(DateTime.class), - TypeReference.of(Response.class), - TypeReference.of(Request.class), + TypeReference.of(Secret.class), TypeReference.of(APIGatewayProxyResponseEvent.class), TypeReference.of(APIGatewayProxyRequestEvent.class), TypeReference.of(APIGatewayProxyRequestEvent.ProxyRequestContext.class) diff --git a/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/entity/Secret.java b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/entity/Secret.java index b13d784..90c58b4 100644 --- a/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/entity/Secret.java +++ b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/entity/Secret.java @@ -6,14 +6,11 @@ import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; -import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; @Data @Builder @NoArgsConstructor @AllArgsConstructor -@DynamoDbBean public class Secret implements Serializable { private String env; @@ -24,11 +21,22 @@ public class Secret implements Serializable { private String partitionKey; - private Map variables; + private Map items; - @DynamoDbPartitionKey public String getPartitionKey() { return env + costCentre + applicationName; } + public static Secret of(final com.coffeebeans.springnativeawslambda.model.Secret secretModel) { + final String env = secretModel.getEnv(); + final String costCentre = secretModel.getCostCentre(); + final String applicationName = secretModel.getApplicationName(); + return Secret.builder() + .env(env) + .costCentre(costCentre) + .applicationName(applicationName) + .items(secretModel.getItems()) + .partitionKey(secretModel.getId()) + .build(); + } } diff --git a/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/function/ExampleFunction.java b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/function/ExampleFunction.java index ca59b75..50e6ecf 100644 --- a/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/function/ExampleFunction.java +++ b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/function/ExampleFunction.java @@ -20,9 +20,7 @@ import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; -import com.coffeebeans.springnativeawslambda.entity.Secret; -import com.coffeebeans.springnativeawslambda.model.Request; -import com.coffeebeans.springnativeawslambda.model.Response; +import com.coffeebeans.springnativeawslambda.model.Secret; import com.coffeebeans.springnativeawslambda.repository.SecretRepository; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -31,7 +29,6 @@ import java.util.function.Function; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.validation.annotation.Validated; @@ -66,24 +63,14 @@ public ExampleFunction( public APIGatewayProxyResponseEvent apply(final APIGatewayProxyRequestEvent proxyRequestEvent) { log.info("Converting request into a response...'"); - final Request request = objectMapper.readValue(proxyRequestEvent.getBody(), Request.class); + final Secret secret = objectMapper.readValue(proxyRequestEvent.getBody(), Secret.class); - final Response response = Response.builder() - .name(request.getName()) - .saved(true) - .build(); - - this.secretRepository.save(Secret.builder() - .env("dev") - .costCentre("coffeebeans") - .applicationName("native-lambda") - .variables(Map.of("ENV", "dev", "PASSWORD", "changeit")) - .build()); + this.secretRepository.save(com.coffeebeans.springnativeawslambda.entity.Secret.of(secret)); log.info("Converted request into a response."); return new APIGatewayProxyResponseEvent() .withStatusCode(200) - .withBody(objectMapper.writeValueAsString(response)); + .withBody(objectMapper.writeValueAsString(secret)); } } \ No newline at end of file diff --git a/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/model/Request.java b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/model/Request.java deleted file mode 100644 index 81d1995..0000000 --- a/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/model/Request.java +++ /dev/null @@ -1,35 +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.model; - -import jakarta.validation.constraints.NotBlank; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class Request { - - @NotBlank - private String name; -} diff --git a/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/model/Response.java b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/model/Secret.java similarity index 73% rename from spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/model/Response.java rename to spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/model/Secret.java index 438e804..deeda7e 100644 --- a/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/model/Response.java +++ b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/model/Secret.java @@ -21,7 +21,8 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty.Access; import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.NotEmpty; +import java.util.Map; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -31,13 +32,25 @@ @Builder @NoArgsConstructor @AllArgsConstructor -public class Response { - +public class Secret { @NotBlank @JsonProperty(access = Access.READ_ONLY) - private String name; + private String id; + + @NotBlank + private String env; + + @NotBlank + private String costCentre; + + @NotBlank + private String applicationName; + + @NotEmpty + private Map items; + + public String getId() { + return "%s-%s-%s".formatted(env, costCentre, applicationName).toLowerCase().replaceAll("[^a-zA-Z0-9]", ""); + } - @NotNull - @JsonProperty(access = Access.READ_ONLY) - private boolean saved; } diff --git a/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/repository/SecretRepository.java b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/repository/SecretRepository.java index b72ff02..5698d35 100644 --- a/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/repository/SecretRepository.java +++ b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/repository/SecretRepository.java @@ -38,9 +38,22 @@ public void save(final Secret secret) { .setter(Secret::setPartitionKey) .tags(StaticAttributeTags.primaryPartitionKey()) ) + .addAttribute(String.class, a -> a.name("env") + .getter(Secret::getEnv) + .setter(Secret::setEnv) + ) + .addAttribute(String.class, a -> a.name("costCentre") + .getter(Secret::getCostCentre) + .setter(Secret::setCostCentre) + ) + .addAttribute(String.class, a -> a.name("applicationName") + .getter(Secret::getApplicationName) + .setter(Secret::setApplicationName) + ) .addAttribute(EnhancedType.mapOf(String.class, String.class), a -> a.name("variables") - .getter(Secret::getVariables) - .setter(Secret::setVariables)) + .getter(Secret::getItems) + .setter(Secret::setItems) + ) .build(); final DynamoDbTable mappedTable = enhancedClient.table("dev-coffeebeans-secret", tableSchema); From 4ffa483ad5824cb278c072785dd0ad7cefdace97 Mon Sep 17 00:00:00 2001 From: muhamadto Date: Sun, 24 Mar 2024 14:48:05 +1100 Subject: [PATCH 3/6] Renaming some modules --- .github/workflows/release.yml | 4 +++ PULL_REQUEST_TEMPLATE.md | 2 +- README.md | 8 ++++- cdk.json | 5 ++- pom.xml | 11 ------ .../infra/Application.java | 4 +-- .../infra/SpringNativeAwsFunctionStack.java | 16 +++++---- spring-native-aws-service/pom.xml | 5 +-- .../ReflectionRuntimeHints.java | 14 ++------ .../springnativeawslambda/entity/Secret.java | 18 +++++++--- .../function/ExampleFunction.java | 21 +++-------- .../springnativeawslambda/model/Request.java | 35 ------------------- .../model/{Response.java => Secret.java} | 27 ++++++++++---- .../repository/SecretRepository.java | 17 +++++++-- 14 files changed, 84 insertions(+), 103 deletions(-) delete mode 100644 spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/model/Request.java rename spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/model/{Response.java => Secret.java} (73%) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e0fbf58..b10cfbd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -51,6 +51,10 @@ jobs: path: ~/.m2 key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} restore-keys: ${{ runner.os }}-m2 + - name: maven-settings-xml-action + uses: whelk-io/maven-settings-xml-action@v22 + with: + servers: '[{ "id": "github", "password": "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" }]' - name: Build with Maven env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index 71c43c0..0cabf7e 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -7,6 +7,6 @@ -- [ ] Describe what you did in the pull request description +- [ ] Describe what you did in the pull secret description - [ ] Add Unit and Integration Tests - at least 80% unit tests for new code. - [ ] Added or updated documentation \ No newline at end of file diff --git a/README.md b/README.md index 79497c7..8b80936 100644 --- a/README.md +++ b/README.md @@ -357,4 +357,10 @@ Now that the setup is done you can deploy to AWS. "name": "CoffeeBeans" }' ``` -3. Et voila! It runs with 500 ms for cold start. \ No newline at end of file +3. Et voila! It runs with 500 ms for cold start. + +```shell + curl --location --request POST 'https://lmk0qo0xrl.execute-api.ap-southeast-2.amazonaws.com/dev/ + --header 'Content-Type: application/json' \ + --data-raw '{ "env": "production", "costCentre": "1234", "applicationName": "some-app", "items": [ { "key": "GITHUB_TOKEN", "value": "WOAH" }, { "key": "AWS_ACCESS_KEY_ID", "value": "OMG" }, { "key": "AWS_SECRET_ACCESS_KEY", "value": "OH, NO" } ] }' +``` \ No newline at end of file diff --git a/cdk.json b/cdk.json index 10fb62e..323e56b 100644 --- a/cdk.json +++ b/cdk.json @@ -1,3 +1,6 @@ { - "app": "./mvnw exec:java -pl spring-native-aws-service-infra -Dexec.mainClass=com.coffeebeans.springnativeawslambda.infra.Application" + "app": "./mvnw --settings /Users/muhammad/.m2/settings-nonqantas.xml exec:java -pl spring-native-aws-service-infra -Dexec.mainClass=com.coffeebeans.springnativeawslambda.infra.Application", + "context": { + "@aws-cdk/core:bootstrapQualifier": "cbcore" + } } \ No newline at end of file diff --git a/pom.xml b/pom.xml index 71c0988..2490eca 100644 --- a/pom.xml +++ b/pom.xml @@ -84,7 +84,6 @@ - io.awspring.cloud spring-cloud-aws-dependencies @@ -298,16 +297,6 @@ - - - github - https://public:0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@maven.pkg.github.com/muhamadto/* - - true - - - - Apache License, Version 2.0 diff --git a/spring-native-aws-service-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/Application.java b/spring-native-aws-service-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/Application.java index 0ab2b19..4e7f87f 100644 --- a/spring-native-aws-service-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/Application.java +++ b/spring-native-aws-service-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/Application.java @@ -37,13 +37,13 @@ public final class Application extends AbstractApp { private static final String ENVIRONMENT_NAME_DEV = "dev"; private static final String ENVIRONMENT_NAME_PRD = "prd"; - private static final String LAMBDA_CODE_PATH = "spring-native-aws-service/target/spring-native-aws-function-native-zip.zip"; + private static final String LAMBDA_CODE_PATH = "spring-native-aws-service/target/spring-native-aws-service-native-zip.zip"; public static void main(final String... args) { final Application app = new Application(); final String env = System.getenv(KEY_ENV); - checkNotNull(env, "'env' environment variable is required"); + checkNotNull(env, "'ENVIRONMENT' environment variable is required"); switch (env) { case ENVIRONMENT_NAME_DEV -> new SpringNativeAwsFunctionStack(app, COFFEE_BEANS_DEV_111111111111_AP_SOUTHEAST_2, LAMBDA_CODE_PATH, env); diff --git a/spring-native-aws-service-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/SpringNativeAwsFunctionStack.java b/spring-native-aws-service-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/SpringNativeAwsFunctionStack.java index f50d5c6..cd0132a 100644 --- a/spring-native-aws-service-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/SpringNativeAwsFunctionStack.java +++ b/spring-native-aws-service-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/SpringNativeAwsFunctionStack.java @@ -22,9 +22,7 @@ import static software.amazon.awscdk.services.iam.ManagedPolicy.fromAwsManagedPolicyName; import static software.amazon.awscdk.services.lambda.Code.fromAsset; -import com.coffeebeans.cdk.core.AbstractApp; import com.coffeebeans.cdk.core.AbstractEnvironment; -import com.coffeebeans.cdk.core.construct.BaseStack; import com.coffeebeans.cdk.core.construct.dynamodb.TableV2; import com.coffeebeans.cdk.core.construct.dynamodb.TableV2.TableProps; import com.coffeebeans.cdk.core.construct.lambda.CustomRuntime2023Function; @@ -36,20 +34,23 @@ import javax.validation.constraints.NotBlank; import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; +import software.amazon.awscdk.DefaultStackSynthesizer; import software.amazon.awscdk.Duration; +import software.amazon.awscdk.RemovalPolicy; +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; import software.amazon.awscdk.services.apigateway.LambdaRestApi; import software.amazon.awscdk.services.apigateway.Resource; import software.amazon.awscdk.services.apigateway.StageOptions; import software.amazon.awscdk.services.dynamodb.Attribute; import software.amazon.awscdk.services.dynamodb.AttributeType; -import software.amazon.awscdk.services.iam.IGrantable; import software.amazon.awscdk.services.iam.IManagedPolicy; import software.amazon.awscdk.services.iam.Role; import software.amazon.awscdk.services.iam.ServicePrincipal; import software.amazon.awscdk.services.lambda.AssetCode; import software.amazon.awscdk.services.lambda.Function; -public class SpringNativeAwsFunctionStack extends BaseStack { +public class SpringNativeAwsFunctionStack extends Stack { private static final int LAMBDA_FUNCTION_TIMEOUT_IN_SECONDS = 3; private static final int LAMBDA_FUNCTION_MEMORY_SIZE = 512; @@ -57,12 +58,12 @@ public class SpringNativeAwsFunctionStack extends BaseStack { private static final String LAMBDA_HANDLER = "org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest"; private static final String ENVIRONMENT_VARIABLE_SPRING_PROFILES_ACTIVE = "SPRING_PROFILES_ACTIVE"; - - public SpringNativeAwsFunctionStack(@NotNull final AbstractApp app, + public SpringNativeAwsFunctionStack(@NotNull final Application app, @NotNull final AbstractEnvironment environment, @NotBlank final String lambdaCodePath, @NotBlank final String stage) { - super(app, environment); + super(app, "SpringNativeAwsFunctionStack", + StackProps.builder().synthesizer(DefaultStackSynthesizer.Builder.create().qualifier("cbcore").build()).build()); final List managedPolicies = List.of(fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")); @@ -95,6 +96,7 @@ public SpringNativeAwsFunctionStack(@NotNull final AbstractApp app, .partitionKey(Attribute.builder().name("id").type(AttributeType.STRING).build()) .timeToLiveAttribute("creationTime") .tableName(KebabCaseString.of("secrets")) + .removalPolicy(RemovalPolicy.RETAIN_ON_UPDATE_OR_DELETE) .build(); final software.amazon.awscdk.services.dynamodb.TableV2 tableV2 = new TableV2(this, SafeString.of("Table"), tableProps).getTable(); diff --git a/spring-native-aws-service/pom.xml b/spring-native-aws-service/pom.xml index 18ff149..32e65c8 100644 --- a/spring-native-aws-service/pom.xml +++ b/spring-native-aws-service/pom.xml @@ -43,7 +43,9 @@ 2023.0.0 - + + 21 + 21 @@ -166,7 +168,6 @@ org.graalvm.buildtools native-maven-plugin - 0.10.1 com.coffeebeans.springnativeawslambda.Application diff --git a/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/ReflectionRuntimeHints.java b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/ReflectionRuntimeHints.java index d8705f8..ceeeb72 100644 --- a/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/ReflectionRuntimeHints.java +++ b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/ReflectionRuntimeHints.java @@ -18,25 +18,16 @@ package com.coffeebeans.springnativeawslambda; -import com.coffeebeans.springnativeawslambda.entity.Secret; -import com.coffeebeans.springnativeawslambda.repository.SecretRepository; -import io.awspring.cloud.dynamodb.DynamoDbTemplate; import java.util.List; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; -import com.coffeebeans.springnativeawslambda.model.Request; -import com.coffeebeans.springnativeawslambda.model.Response; -import java.util.Map; +import com.coffeebeans.springnativeawslambda.model.Secret; import org.joda.time.DateTime; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.aot.hint.TypeReference; import org.springframework.lang.Nullable; -import software.amazon.awssdk.enhanced.dynamodb.DefaultAttributeConverterProvider; -import software.amazon.awssdk.enhanced.dynamodb.TableSchema; -import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.BeanTableSchemaAttributeTags; -import software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema; public class ReflectionRuntimeHints implements RuntimeHintsRegistrar { @@ -44,8 +35,7 @@ public class ReflectionRuntimeHints implements RuntimeHintsRegistrar { public void registerHints(final RuntimeHints hints, @Nullable final ClassLoader classLoader) { final List typeReferences = List.of( TypeReference.of(DateTime.class), - TypeReference.of(Response.class), - TypeReference.of(Request.class), + TypeReference.of(Secret.class), TypeReference.of(APIGatewayProxyResponseEvent.class), TypeReference.of(APIGatewayProxyRequestEvent.class), TypeReference.of(APIGatewayProxyRequestEvent.ProxyRequestContext.class) diff --git a/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/entity/Secret.java b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/entity/Secret.java index b13d784..90c58b4 100644 --- a/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/entity/Secret.java +++ b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/entity/Secret.java @@ -6,14 +6,11 @@ import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; -import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; @Data @Builder @NoArgsConstructor @AllArgsConstructor -@DynamoDbBean public class Secret implements Serializable { private String env; @@ -24,11 +21,22 @@ public class Secret implements Serializable { private String partitionKey; - private Map variables; + private Map items; - @DynamoDbPartitionKey public String getPartitionKey() { return env + costCentre + applicationName; } + public static Secret of(final com.coffeebeans.springnativeawslambda.model.Secret secretModel) { + final String env = secretModel.getEnv(); + final String costCentre = secretModel.getCostCentre(); + final String applicationName = secretModel.getApplicationName(); + return Secret.builder() + .env(env) + .costCentre(costCentre) + .applicationName(applicationName) + .items(secretModel.getItems()) + .partitionKey(secretModel.getId()) + .build(); + } } diff --git a/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/function/ExampleFunction.java b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/function/ExampleFunction.java index ca59b75..50e6ecf 100644 --- a/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/function/ExampleFunction.java +++ b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/function/ExampleFunction.java @@ -20,9 +20,7 @@ import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; -import com.coffeebeans.springnativeawslambda.entity.Secret; -import com.coffeebeans.springnativeawslambda.model.Request; -import com.coffeebeans.springnativeawslambda.model.Response; +import com.coffeebeans.springnativeawslambda.model.Secret; import com.coffeebeans.springnativeawslambda.repository.SecretRepository; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -31,7 +29,6 @@ import java.util.function.Function; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.validation.annotation.Validated; @@ -66,24 +63,14 @@ public ExampleFunction( public APIGatewayProxyResponseEvent apply(final APIGatewayProxyRequestEvent proxyRequestEvent) { log.info("Converting request into a response...'"); - final Request request = objectMapper.readValue(proxyRequestEvent.getBody(), Request.class); + final Secret secret = objectMapper.readValue(proxyRequestEvent.getBody(), Secret.class); - final Response response = Response.builder() - .name(request.getName()) - .saved(true) - .build(); - - this.secretRepository.save(Secret.builder() - .env("dev") - .costCentre("coffeebeans") - .applicationName("native-lambda") - .variables(Map.of("ENV", "dev", "PASSWORD", "changeit")) - .build()); + this.secretRepository.save(com.coffeebeans.springnativeawslambda.entity.Secret.of(secret)); log.info("Converted request into a response."); return new APIGatewayProxyResponseEvent() .withStatusCode(200) - .withBody(objectMapper.writeValueAsString(response)); + .withBody(objectMapper.writeValueAsString(secret)); } } \ No newline at end of file diff --git a/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/model/Request.java b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/model/Request.java deleted file mode 100644 index 81d1995..0000000 --- a/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/model/Request.java +++ /dev/null @@ -1,35 +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.model; - -import jakarta.validation.constraints.NotBlank; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class Request { - - @NotBlank - private String name; -} diff --git a/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/model/Response.java b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/model/Secret.java similarity index 73% rename from spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/model/Response.java rename to spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/model/Secret.java index 438e804..deeda7e 100644 --- a/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/model/Response.java +++ b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/model/Secret.java @@ -21,7 +21,8 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty.Access; import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.NotEmpty; +import java.util.Map; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -31,13 +32,25 @@ @Builder @NoArgsConstructor @AllArgsConstructor -public class Response { - +public class Secret { @NotBlank @JsonProperty(access = Access.READ_ONLY) - private String name; + private String id; + + @NotBlank + private String env; + + @NotBlank + private String costCentre; + + @NotBlank + private String applicationName; + + @NotEmpty + private Map items; + + public String getId() { + return "%s-%s-%s".formatted(env, costCentre, applicationName).toLowerCase().replaceAll("[^a-zA-Z0-9]", ""); + } - @NotNull - @JsonProperty(access = Access.READ_ONLY) - private boolean saved; } diff --git a/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/repository/SecretRepository.java b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/repository/SecretRepository.java index b72ff02..5698d35 100644 --- a/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/repository/SecretRepository.java +++ b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/repository/SecretRepository.java @@ -38,9 +38,22 @@ public void save(final Secret secret) { .setter(Secret::setPartitionKey) .tags(StaticAttributeTags.primaryPartitionKey()) ) + .addAttribute(String.class, a -> a.name("env") + .getter(Secret::getEnv) + .setter(Secret::setEnv) + ) + .addAttribute(String.class, a -> a.name("costCentre") + .getter(Secret::getCostCentre) + .setter(Secret::setCostCentre) + ) + .addAttribute(String.class, a -> a.name("applicationName") + .getter(Secret::getApplicationName) + .setter(Secret::setApplicationName) + ) .addAttribute(EnhancedType.mapOf(String.class, String.class), a -> a.name("variables") - .getter(Secret::getVariables) - .setter(Secret::setVariables)) + .getter(Secret::getItems) + .setter(Secret::setItems) + ) .build(); final DynamoDbTable mappedTable = enhancedClient.table("dev-coffeebeans-secret", tableSchema); From fb7a498b2c109cbdc4d195b69130f3f6869a65c0 Mon Sep 17 00:00:00 2001 From: muhamadto Date: Mon, 25 Mar 2024 01:18:51 +1100 Subject: [PATCH 4/6] Renaming some modules --- README.md | 197 +++++++++++++----- .../infra/Application.java | 2 + .../src/assembly/native.xml | 2 +- 3 files changed, 147 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 8b80936..483585b 100644 --- a/README.md +++ b/README.md @@ -77,17 +77,22 @@ $ ./mvnw -ntp clean verify -U $ curl --location --request POST 'http://localhost:8080' \ --header 'Content-Type: application/json' \ --data-raw '{ - "body": "{ \"name\": \"CoffeeBeans\" }" + "body": "{ \"env\": \"production\", \"costCentre\": \"1234\", \"applicationName\": \"some-app\", \"items\": { \"GITHUB_TOKEN\": \"WOAH\", \"AWS_ACCESS_KEY_ID\": \"OMG\", \"AWS_SECRET_ACCESS_KEY\": \"OH NO\" } }" }' ``` The service responds ```json - [ - { - "name": "CoffeeBeans", - "saved": true - } - ] + { + "id": "production1234someapp", + "env": "production", + "costCentre": "1234", + "applicationName": "some-app", + "items": { + "GITHUB_TOKEN": "WOAH", + "AWS_ACCESS_KEY_ID": "OMG", + "AWS_SECRET_ACCESS_KEY": "OH NO" + } + } ``` ### Github action @@ -150,67 +155,151 @@ and the following trust relationship "Version": "2012-10-17", "Statement": [ { - "Sid": "ECRPermissions", + "Sid": "S3Permissions", + "Effect": "Allow", + "Action": "s3:GetObject", + "Resource": [ + "arn:aws:s3:::cdk-cbcore-assets-718055627712-ap-southeast-2", + "arn:aws:s3:::cdk-cbcore-assets-718055627712-ap-southeast-2/*" + ] + }, + { + "Sid": "AGWPermissions", "Effect": "Allow", "Action": [ - "ecr:CreateRepository", - "ecr:DeleteRepository", - "ecr:SetRepositoryPolicy", - "ecr:DescribeRepositories" + "apigateway:POST", + "apigateway:DELETE", + "apigateway:GET", + "apigateway:PATCH", + "apigateway:PUT" ], - "Resource": "arn:aws:ecr:{aws-region}:{aws-account-number}:repository/cdk-{qualifier}-container-assets-{aws-account-number}-{aws-region}" + "Resource": [ + "arn:aws:apigateway:ap-southeast-2::/restapis", + "arn:aws:apigateway:ap-southeast-2::/restapis/*", + "arn:aws:apigateway:ap-southeast-2::/account", + "arn:aws:apigateway:ap-southeast-2::/tags/arn:aws:apigateway:ap-southeast-2::/restapis/*" + ] }, { - "Sid": "IAMPermissions", + "Sid": "SNSPermissions", "Effect": "Allow", "Action": [ - "iam:GetRole", - "iam:CreateRole", - "iam:DeleteRole", - "iam:AttachRolePolicy", - "iam:PutRolePolicy", - "iam:DetachRolePolicy", - "iam:DeleteRolePolicy" + "SNS:CreateTopic", + "SNS:DeleteTopic", + "SNS:Subscribe", + "SNS:GetTopicAttributes", + "SNS:ListSubscriptionsByTopic", + "SNS:Unsubscribe", + "SNS:TagResource", + "SNS:UntagResource" ], "Resource": [ - "arn:aws:iam::{aws-account-number}:role/cdk-{qualifier}-lookup-role-{aws-account-number}-{aws-region}", - "arn:aws:iam::{aws-account-number}:role/cdk-{qualifier}-file-publishing-role-{aws-account-number}-{aws-region}", - "arn:aws:iam::{aws-account-number}:role/cdk-{qualifier}-image-publishing-role-{aws-account-number}-{aws-region}", - "arn:aws:iam::{aws-account-number}:role/cdk-{qualifier}-cfn-exec-role-{aws-account-number}-{aws-region}", - "arn:aws:iam::{aws-account-number}:role/cdk-{qualifier}-deploy-role-{aws-account-number}-{aws-region}" + "arn:aws:sqs:ap-southeast-2:718055627712:SpringNativeAwsFunctionStack-LambdaDeadLetterTopic*" ] }, { - "Sid": "S3Permissions", + "Sid": "SQSPermissions", "Effect": "Allow", "Action": [ - "s3:PutBucketPublicAccessBlock", - "s3:CreateBucket", - "s3:DeleteBucketPolicy", - "s3:PutEncryptionConfiguration", - "s3:GetEncryptionConfiguration", - "s3:PutBucketPolicy", - "s3:DeleteBucket", - "s3:PutBucketVersioning" + "sqs:GetQueueAttributes", + "sqs:CreateQueue", + "sqs:DeleteQueue", + "sqs:GetQueueUrl", + "sqs:SetQueueAttributes", + "sqs:ListQueues" ], "Resource": [ - "arn:aws:s3:::{qualifier}-cdk-bucket" + "arn:aws:sqs:ap-southeast-2:718055627712:SpringNativeAwsFunctionStack-LambdaDeadLetterQueue*" + ] + }, + { + "Sid": "LambdaPermissions", + "Effect": "Allow", + "Action": [ + "lambda:GetFunction", + "lambda:ListFunctions", + "lambda:DeleteFunction", + "lambda:CreateFunction", + "lambda:TagResource", + "lambda:AddPermission", + "lambda:RemovePermission", + "lambda:PutFunctionEventInvokeConfig", + "lambda:UpdateFunctionEventInvokeConfig", + "lambda:DeleteFunctionEventInvokeConfig", + "lambda:UpdateFunctionCode", + "lambda:ListTags", + "lambda:UpdateFunctionConfiguration" + ], + "Resource": [ + "arn:aws:lambda:ap-southeast-2:718055627712:function:SpringNativeAwsFunctionStack*" ] }, { "Sid": "SSMPermissions", "Effect": "Allow", "Action": [ - "ssm:DeleteParameter", - "ssm:AddTagsToResource", - "ssm:GetParameters", - "ssm:PutParameter" + "ssm:GetParameters" + ], + "Resource": [ + "arn:aws:ssm:ap-southeast-2:718055627712:parameter/cdk-bootstrap/cbcore/version" + ] + }, + { + "Sid": "DynamoDBPermissions", + "Effect": "Allow", + "Action": [ + "dynamodb:DescribeTable", + "dynamodb:CreateTable", + "dynamodb:DeleteTable", + "dynamodb:TagResource", + "dynamodb:UntagResource", + "dynamodb:ListTagsOfResource", + "dynamodb:DescribeTimeToLive", + "dynamodb:DescribeContributorInsights", + "dynamodb:DescribeContinuousBackups", + "dynamodb:DescribeKinesisStreamingDestination" ], - "Resource": "arn:aws:ssm:{aws-region}:{aws-account-number}:parameter/cdk-bootstrap/{qualifier}/version" + "Resource": [ + "arn:aws:dynamodb:ap-southeast-2:718055627712:table/secrets", + "arn:aws:dynamodb:ap-southeast-2:718055627712:table/SpringNativeAwsFunction*" + ] + }, + { + "Sid": "IAMPermissions", + "Effect": "Allow", + "Action": [ + "iam:PassRole", + "iam:GetRole", + "iam:GetRolePolicy", + "iam:CreateRole", + "iam:PutRolePolicy", + "iam:DeleteRole", + "iam:DeleteRolePolicy", + "iam:AttachRolePolicy", + "iam:DetachRolePolicy" + ], + "Resource": [ + "arn:aws:iam::718055627712:role/SpringNativeAwsFunction*" + ] + }, + { + "Sid": "CFNPermissions", + "Effect": "Allow", + "Action": "cloudformation:DescribeStacks", + "Resource": "arn:aws:cloudformation:ap-southeast-2:718055627712:stack/cbcore-example-function-dev-stack/*" + }, + { + "Sid": "ApplicationAutoscalingPermissions", + "Effect": "Allow", + "Action": [ + "application-autoscaling:DeregisterScalableTarget" + ], + "Resource": [ + "arn:aws:application-autoscaling:ap-southeast-2:718055627712:scalable-target/*" + ] } ] -} -``` +}``` 4. Create an IAM managed policy `CoffeebeansCoreCdkExecutionAccess` to be used by `cdk-{qualifier}-cfn-exec-role-{aws-account-number}-{aws-region}` which is gonna be created by @@ -351,16 +440,18 @@ Now that the setup is done you can deploy to AWS. environment. 2. Test via curl ```shell - $ curl --location --request POST 'https://{api-id}.execute-api.ap-southeast-2.amazonaws.com/dev/name' \ - --header 'Content-Type: application/json' \ - --data-raw '{ - "name": "CoffeeBeans" + $ curl --location --request POST 'https://lmk0qo0xrl.execute-api.ap-southeast-2.amazonaws.com/dev/' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "env": "production", + "costCentre": "1234", + "applicationName": "some-app", + "items": { + "GITHUB_TOKEN": "WOAH", + "AWS_ACCESS_KEY_ID": "OMG", + "AWS_SECRET_ACCESS_KEY": "OH NO" + } }' ``` 3. Et voila! It runs with 500 ms for cold start. - -```shell - curl --location --request POST 'https://lmk0qo0xrl.execute-api.ap-southeast-2.amazonaws.com/dev/ - --header 'Content-Type: application/json' \ - --data-raw '{ "env": "production", "costCentre": "1234", "applicationName": "some-app", "items": [ { "key": "GITHUB_TOKEN", "value": "WOAH" }, { "key": "AWS_ACCESS_KEY_ID", "value": "OMG" }, { "key": "AWS_SECRET_ACCESS_KEY", "value": "OH, NO" } ] }' -``` \ No newline at end of file + diff --git a/spring-native-aws-service-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/Application.java b/spring-native-aws-service-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/Application.java index 4e7f87f..e18596b 100644 --- a/spring-native-aws-service-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/Application.java +++ b/spring-native-aws-service-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/Application.java @@ -37,6 +37,8 @@ public final class Application extends AbstractApp { private static final String ENVIRONMENT_NAME_DEV = "dev"; private static final String ENVIRONMENT_NAME_PRD = "prd"; + + private static final String LAMBDA_CODE_PATH = "spring-native-aws-service/target/spring-native-aws-service-native-zip.zip"; public static void main(final String... args) { diff --git a/spring-native-aws-service/src/assembly/native.xml b/spring-native-aws-service/src/assembly/native.xml index dd19a55..91337ea 100644 --- a/spring-native-aws-service/src/assembly/native.xml +++ b/spring-native-aws-service/src/assembly/native.xml @@ -40,7 +40,7 @@ true 0775 - spring-native-aws-function + spring-native-aws-service From 336218b936f3cfe66817a1db6ca23b9e74328d13 Mon Sep 17 00:00:00 2001 From: muhamadto Date: Mon, 25 Mar 2024 01:18:51 +1100 Subject: [PATCH 5/6] Renaming some modules --- README.md | 209 +++++++++++++----- docker-compose.yml | 2 +- .../infra/Application.java | 2 + .../src/assembly/native.xml | 2 +- .../LambdaExceptionHandler.java | 22 ++ .../springnativeawslambda/entity/Secret.java | 4 - .../function/ExampleFunction.java | 46 +++- .../springnativeawslambda/model/Secret.java | 18 +- .../repository/SecretRepository.java | 74 +++---- .../src/main/resources/application-local.yml | 4 +- .../springnativeawslambda/ApplicationIT.java | 16 ++ 11 files changed, 293 insertions(+), 106 deletions(-) diff --git a/README.md b/README.md index 8b80936..e9d60bb 100644 --- a/README.md +++ b/README.md @@ -77,17 +77,34 @@ $ ./mvnw -ntp clean verify -U $ curl --location --request POST 'http://localhost:8080' \ --header 'Content-Type: application/json' \ --data-raw '{ - "body": "{ \"name\": \"CoffeeBeans\" }" + "httpMethod": "POST", + "body": "{ \"env\": \"production\", \"costCentre\": \"1234\", \"applicationName\": \"some-app\", \"items\": { \"GITHUB_TOKEN\": \"WOAH\", \"AWS_ACCESS_KEY_ID\": \"OMG\", \"AWS_SECRET_ACCESS_KEY\": \"OH NO\" } }" }' ``` +```shell +curl --location --request POST 'http://localhost:8080' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "httpMethod": "GET", + "pathParameters": { + "proxy": "production-1234-someapp" + } + }' +``` + The service responds ```json - [ - { - "name": "CoffeeBeans", - "saved": true - } - ] + { + "id": "production1234someapp", + "env": "production", + "costCentre": "1234", + "applicationName": "some-app", + "items": { + "GITHUB_TOKEN": "WOAH", + "AWS_ACCESS_KEY_ID": "OMG", + "AWS_SECRET_ACCESS_KEY": "OH NO" + } + } ``` ### Github action @@ -150,67 +167,151 @@ and the following trust relationship "Version": "2012-10-17", "Statement": [ { - "Sid": "ECRPermissions", + "Sid": "S3Permissions", + "Effect": "Allow", + "Action": "s3:GetObject", + "Resource": [ + "arn:aws:s3:::cdk-cbcore-assets-718055627712-ap-southeast-2", + "arn:aws:s3:::cdk-cbcore-assets-718055627712-ap-southeast-2/*" + ] + }, + { + "Sid": "AGWPermissions", "Effect": "Allow", "Action": [ - "ecr:CreateRepository", - "ecr:DeleteRepository", - "ecr:SetRepositoryPolicy", - "ecr:DescribeRepositories" + "apigateway:POST", + "apigateway:DELETE", + "apigateway:GET", + "apigateway:PATCH", + "apigateway:PUT" ], - "Resource": "arn:aws:ecr:{aws-region}:{aws-account-number}:repository/cdk-{qualifier}-container-assets-{aws-account-number}-{aws-region}" + "Resource": [ + "arn:aws:apigateway:ap-southeast-2::/restapis", + "arn:aws:apigateway:ap-southeast-2::/restapis/*", + "arn:aws:apigateway:ap-southeast-2::/account", + "arn:aws:apigateway:ap-southeast-2::/tags/arn:aws:apigateway:ap-southeast-2::/restapis/*" + ] }, { - "Sid": "IAMPermissions", + "Sid": "SNSPermissions", "Effect": "Allow", "Action": [ - "iam:GetRole", - "iam:CreateRole", - "iam:DeleteRole", - "iam:AttachRolePolicy", - "iam:PutRolePolicy", - "iam:DetachRolePolicy", - "iam:DeleteRolePolicy" + "SNS:CreateTopic", + "SNS:DeleteTopic", + "SNS:Subscribe", + "SNS:GetTopicAttributes", + "SNS:ListSubscriptionsByTopic", + "SNS:Unsubscribe", + "SNS:TagResource", + "SNS:UntagResource" ], "Resource": [ - "arn:aws:iam::{aws-account-number}:role/cdk-{qualifier}-lookup-role-{aws-account-number}-{aws-region}", - "arn:aws:iam::{aws-account-number}:role/cdk-{qualifier}-file-publishing-role-{aws-account-number}-{aws-region}", - "arn:aws:iam::{aws-account-number}:role/cdk-{qualifier}-image-publishing-role-{aws-account-number}-{aws-region}", - "arn:aws:iam::{aws-account-number}:role/cdk-{qualifier}-cfn-exec-role-{aws-account-number}-{aws-region}", - "arn:aws:iam::{aws-account-number}:role/cdk-{qualifier}-deploy-role-{aws-account-number}-{aws-region}" + "arn:aws:sqs:ap-southeast-2:718055627712:SpringNativeAwsFunctionStack-LambdaDeadLetterTopic*" ] }, { - "Sid": "S3Permissions", + "Sid": "SQSPermissions", "Effect": "Allow", "Action": [ - "s3:PutBucketPublicAccessBlock", - "s3:CreateBucket", - "s3:DeleteBucketPolicy", - "s3:PutEncryptionConfiguration", - "s3:GetEncryptionConfiguration", - "s3:PutBucketPolicy", - "s3:DeleteBucket", - "s3:PutBucketVersioning" + "sqs:GetQueueAttributes", + "sqs:CreateQueue", + "sqs:DeleteQueue", + "sqs:GetQueueUrl", + "sqs:SetQueueAttributes", + "sqs:ListQueues" ], "Resource": [ - "arn:aws:s3:::{qualifier}-cdk-bucket" + "arn:aws:sqs:ap-southeast-2:718055627712:SpringNativeAwsFunctionStack-LambdaDeadLetterQueue*" + ] + }, + { + "Sid": "LambdaPermissions", + "Effect": "Allow", + "Action": [ + "lambda:GetFunction", + "lambda:ListFunctions", + "lambda:DeleteFunction", + "lambda:CreateFunction", + "lambda:TagResource", + "lambda:AddPermission", + "lambda:RemovePermission", + "lambda:PutFunctionEventInvokeConfig", + "lambda:UpdateFunctionEventInvokeConfig", + "lambda:DeleteFunctionEventInvokeConfig", + "lambda:UpdateFunctionCode", + "lambda:ListTags", + "lambda:UpdateFunctionConfiguration" + ], + "Resource": [ + "arn:aws:lambda:ap-southeast-2:718055627712:function:SpringNativeAwsFunctionStack*" ] }, { "Sid": "SSMPermissions", "Effect": "Allow", "Action": [ - "ssm:DeleteParameter", - "ssm:AddTagsToResource", - "ssm:GetParameters", - "ssm:PutParameter" + "ssm:GetParameters" + ], + "Resource": [ + "arn:aws:ssm:ap-southeast-2:718055627712:parameter/cdk-bootstrap/cbcore/version" + ] + }, + { + "Sid": "DynamoDBPermissions", + "Effect": "Allow", + "Action": [ + "dynamodb:DescribeTable", + "dynamodb:CreateTable", + "dynamodb:DeleteTable", + "dynamodb:TagResource", + "dynamodb:UntagResource", + "dynamodb:ListTagsOfResource", + "dynamodb:DescribeTimeToLive", + "dynamodb:DescribeContributorInsights", + "dynamodb:DescribeContinuousBackups", + "dynamodb:DescribeKinesisStreamingDestination" + ], + "Resource": [ + "arn:aws:dynamodb:ap-southeast-2:718055627712:table/secrets", + "arn:aws:dynamodb:ap-southeast-2:718055627712:table/SpringNativeAwsFunction*" + ] + }, + { + "Sid": "IAMPermissions", + "Effect": "Allow", + "Action": [ + "iam:PassRole", + "iam:GetRole", + "iam:GetRolePolicy", + "iam:CreateRole", + "iam:PutRolePolicy", + "iam:DeleteRole", + "iam:DeleteRolePolicy", + "iam:AttachRolePolicy", + "iam:DetachRolePolicy" + ], + "Resource": [ + "arn:aws:iam::718055627712:role/SpringNativeAwsFunction*" + ] + }, + { + "Sid": "CFNPermissions", + "Effect": "Allow", + "Action": "cloudformation:DescribeStacks", + "Resource": "arn:aws:cloudformation:ap-southeast-2:718055627712:stack/cbcore-example-function-dev-stack/*" + }, + { + "Sid": "ApplicationAutoscalingPermissions", + "Effect": "Allow", + "Action": [ + "application-autoscaling:DeregisterScalableTarget" ], - "Resource": "arn:aws:ssm:{aws-region}:{aws-account-number}:parameter/cdk-bootstrap/{qualifier}/version" + "Resource": [ + "arn:aws:application-autoscaling:ap-southeast-2:718055627712:scalable-target/*" + ] } ] -} -``` +}``` 4. Create an IAM managed policy `CoffeebeansCoreCdkExecutionAccess` to be used by `cdk-{qualifier}-cfn-exec-role-{aws-account-number}-{aws-region}` which is gonna be created by @@ -351,16 +452,18 @@ Now that the setup is done you can deploy to AWS. environment. 2. Test via curl ```shell - $ curl --location --request POST 'https://{api-id}.execute-api.ap-southeast-2.amazonaws.com/dev/name' \ - --header 'Content-Type: application/json' \ - --data-raw '{ - "name": "CoffeeBeans" + $ curl --location --request POST 'https://lmk0qo0xrl.execute-api.ap-southeast-2.amazonaws.com/dev/' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "env": "production", + "costCentre": "1234", + "applicationName": "some-app", + "items": { + "GITHUB_TOKEN": "WOAH", + "AWS_ACCESS_KEY_ID": "OMG", + "AWS_SECRET_ACCESS_KEY": "OH NO" + } }' ``` 3. Et voila! It runs with 500 ms for cold start. - -```shell - curl --location --request POST 'https://lmk0qo0xrl.execute-api.ap-southeast-2.amazonaws.com/dev/ - --header 'Content-Type: application/json' \ - --data-raw '{ "env": "production", "costCentre": "1234", "applicationName": "some-app", "items": [ { "key": "GITHUB_TOKEN", "value": "WOAH" }, { "key": "AWS_ACCESS_KEY_ID", "value": "OMG" }, { "key": "AWS_SECRET_ACCESS_KEY", "value": "OH, NO" } ] }' -``` \ No newline at end of file + diff --git a/docker-compose.yml b/docker-compose.yml index 696ff14..9e14073 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -49,7 +49,7 @@ services: ' function package_spring_native_function() { if [ "$$BUILD_ARTIFACT" = "true" ]; then - ./mvnw -ntp clean -Pnative -DskipTests native:compile package -pl "$$FUNCTION_NAME" + ./mvnw -ntp clean -Pnative -DskipTests native:compile package -pl "$$FUNCTION_NAME" --settings /home/worker/.m2/settings-nonqantas.xml else print_info_message "plain" "BUILD_ARTIFACT environment variable is not set. Skipping Maven build." fi diff --git a/spring-native-aws-service-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/Application.java b/spring-native-aws-service-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/Application.java index 4e7f87f..e18596b 100644 --- a/spring-native-aws-service-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/Application.java +++ b/spring-native-aws-service-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/Application.java @@ -37,6 +37,8 @@ public final class Application extends AbstractApp { private static final String ENVIRONMENT_NAME_DEV = "dev"; private static final String ENVIRONMENT_NAME_PRD = "prd"; + + private static final String LAMBDA_CODE_PATH = "spring-native-aws-service/target/spring-native-aws-service-native-zip.zip"; public static void main(final String... args) { diff --git a/spring-native-aws-service/src/assembly/native.xml b/spring-native-aws-service/src/assembly/native.xml index dd19a55..91337ea 100644 --- a/spring-native-aws-service/src/assembly/native.xml +++ b/spring-native-aws-service/src/assembly/native.xml @@ -40,7 +40,7 @@ true 0775 - spring-native-aws-function + spring-native-aws-service diff --git a/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/LambdaExceptionHandler.java b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/LambdaExceptionHandler.java index e3764be..f42423d 100644 --- a/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/LambdaExceptionHandler.java +++ b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/LambdaExceptionHandler.java @@ -5,9 +5,12 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.exc.InvalidDefinitionException; import java.util.Map; +import java.util.Set; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpMethod; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.server.MethodNotAllowedException; @Slf4j @RestControllerAdvice @@ -27,6 +30,25 @@ public APIGatewayProxyResponseEvent handleException( + " }"); } + @ExceptionHandler(MethodNotAllowedException.class) + public APIGatewayProxyResponseEvent handleException( + final MethodNotAllowedException e, + final APIGatewayProxyRequestEvent request) { + log.error("Error processing request: {}", e.getMessage()); + + final Set supportedMethods = e.getSupportedMethods(); + + return new APIGatewayProxyResponseEvent() + .withStatusCode(405) + .withHeaders(Map.of("X-Requested-Id", request.getRequestContext().getRequestId())) + .withBody( + ("{\n" + + "\"message\": \"Method not allowed. Supported methods [%s]\",\n" + + " \"errorCode\": \"METHOD_NOT_ALLOWED\n" + + " }").formatted( + supportedMethods)); + } + @ExceptionHandler(InvalidDefinitionException.class) public APIGatewayProxyResponseEvent handleException( final InvalidDefinitionException e, diff --git a/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/entity/Secret.java b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/entity/Secret.java index 90c58b4..5d4a0ee 100644 --- a/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/entity/Secret.java +++ b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/entity/Secret.java @@ -23,10 +23,6 @@ public class Secret implements Serializable { private Map items; - public String getPartitionKey() { - return env + costCentre + applicationName; - } - public static Secret of(final com.coffeebeans.springnativeawslambda.model.Secret secretModel) { final String env = secretModel.getEnv(); final String costCentre = secretModel.getCostCentre(); diff --git a/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/function/ExampleFunction.java b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/function/ExampleFunction.java index 50e6ecf..755e173 100644 --- a/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/function/ExampleFunction.java +++ b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/function/ExampleFunction.java @@ -18,6 +18,9 @@ package com.coffeebeans.springnativeawslambda.function; +import static org.springframework.http.HttpMethod.GET; +import static org.springframework.http.HttpMethod.POST; + import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; import com.coffeebeans.springnativeawslambda.model.Secret; @@ -25,12 +28,16 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.validation.constraints.NotNull; +import java.util.List; import java.util.Map; import java.util.function.Function; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.MethodInvocationException; +import org.springframework.http.HttpMethod; import org.springframework.stereotype.Component; import org.springframework.validation.annotation.Validated; +import org.springframework.web.server.MethodNotAllowedException; @Component @Slf4j @@ -45,7 +52,7 @@ public class ExampleFunction implements public ExampleFunction( @NotNull final SecretRepository secretRepository, @NotNull final ObjectMapper objectMapper - ) { + ) { this.objectMapper = objectMapper; this.secretRepository = secretRepository; @@ -61,14 +68,47 @@ public ExampleFunction( @Override @SneakyThrows(value = JsonProcessingException.class) public APIGatewayProxyResponseEvent apply(final APIGatewayProxyRequestEvent proxyRequestEvent) { - log.info("Converting request into a response...'"); + log.info("Received a request...'"); + + final String httpMethod = proxyRequestEvent.getHttpMethod() == null ? "" : proxyRequestEvent.getHttpMethod().toUpperCase(); + return switch (httpMethod) { + case "POST" -> doPost(proxyRequestEvent); + case "GET" -> doGet(proxyRequestEvent); + case null, default -> throw new MethodNotAllowedException(httpMethod, List.of(POST, GET)); + }; + } + + private APIGatewayProxyResponseEvent doPost(final APIGatewayProxyRequestEvent proxyRequestEvent) throws JsonProcessingException { + log.info("POST: Converting request into a response"); final Secret secret = objectMapper.readValue(proxyRequestEvent.getBody(), Secret.class); this.secretRepository.save(com.coffeebeans.springnativeawslambda.entity.Secret.of(secret)); - log.info("Converted request into a response."); + log.info("POST: Converted request into a response"); + + return new APIGatewayProxyResponseEvent() + .withStatusCode(200) + .withBody(objectMapper.writeValueAsString(secret)); + } + + private APIGatewayProxyResponseEvent doGet(final APIGatewayProxyRequestEvent proxyRequestEvent) throws JsonProcessingException { + log.info("GET: Retrieving response"); + + final Map pathParameters = proxyRequestEvent.getPathParameters(); + final String id = pathParameters.get("proxy"); + + final com.coffeebeans.springnativeawslambda.entity.Secret secretEntity = this.secretRepository.findById(id); + + final Secret secret = Secret.of(secretEntity); + + if (secret == null) { + return new APIGatewayProxyResponseEvent() + .withStatusCode(404) + .withBody("Entity not found"); + } + log.info("GET: response retrieved successfully"); return new APIGatewayProxyResponseEvent() .withStatusCode(200) .withBody(objectMapper.writeValueAsString(secret)); diff --git a/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/model/Secret.java b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/model/Secret.java index deeda7e..9d16fa7 100644 --- a/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/model/Secret.java +++ b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/model/Secret.java @@ -50,7 +50,23 @@ public class Secret { private Map items; public String getId() { - return "%s-%s-%s".formatted(env, costCentre, applicationName).toLowerCase().replaceAll("[^a-zA-Z0-9]", ""); + final String env = this.env.toLowerCase().replaceAll("[^a-zA-Z0-9]", ""); + final String costCentre = this.costCentre.toLowerCase().replaceAll("[^a-zA-Z0-9]", ""); + final String applicationName = this.applicationName.toLowerCase().replaceAll("[^a-zA-Z0-9]", ""); + + return "%s-%s-%s".formatted(env, costCentre, applicationName); } + public static Secret of(final com.coffeebeans.springnativeawslambda.entity.Secret secretEntity) { + final String env = secretEntity.getEnv(); + final String costCentre = secretEntity.getCostCentre(); + final String applicationName = secretEntity.getApplicationName(); + return Secret.builder() + .env(env) + .costCentre(costCentre) + .applicationName(applicationName) + .items(secretEntity.getItems()) + .id(secretEntity.getPartitionKey()) + .build(); + } } diff --git a/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/repository/SecretRepository.java b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/repository/SecretRepository.java index 5698d35..8039642 100644 --- a/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/repository/SecretRepository.java +++ b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/repository/SecretRepository.java @@ -2,62 +2,56 @@ import com.coffeebeans.springnativeawslambda.entity.Secret; import jakarta.validation.constraints.NotNull; -import java.util.Map; import org.springframework.stereotype.Repository; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.Key; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; -import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttribute; import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags; import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; @Repository public class SecretRepository { - private final DynamoDbEnhancedClient enhancedClient; + public static final TableSchema TABLE_SCHEMA = StaticTableSchema.builder(Secret.class) + .newItemSupplier(Secret::new) + .addAttribute(String.class, a -> a.name("id") + .getter(Secret::getPartitionKey) + .setter(Secret::setPartitionKey) + .tags(StaticAttributeTags.primaryPartitionKey()) + ) + .addAttribute(String.class, a -> a.name("env") + .getter(Secret::getEnv) + .setter(Secret::setEnv) + ) + .addAttribute(String.class, a -> a.name("costCentre") + .getter(Secret::getCostCentre) + .setter(Secret::setCostCentre) + ) + .addAttribute(String.class, a -> a.name("applicationName") + .getter(Secret::getApplicationName) + .setter(Secret::setApplicationName) + ) + .addAttribute(EnhancedType.mapOf(String.class, String.class), a -> a.name("variables") + .getter(Secret::getItems) + .setter(Secret::setItems) + ) + .build(); +// private final DynamoDbEnhancedClient enhancedClient; + + private final DynamoDbTable mappedTable; public SecretRepository(@NotNull final DynamoDbEnhancedClient enhancedClient) { - this.enhancedClient = enhancedClient; + this.mappedTable = enhancedClient.table("secrets", TABLE_SCHEMA); } -// public Secret findById(String id) { -// final Key key = Key.builder().partitionValue(Integer.valueOf(id)).build(); -// -// final Secret secret = this.dynamoDbTemplate.load(key, Secret.class); -// return Optional.ofNullable(secret) -// .orElseGet(Secret::new); -// } + public Secret findById(String id) { + final Key key = Key.builder().partitionValue(id).build(); + return this.mappedTable.getItem(r -> r.key(key)); + } public void save(final Secret secret) { - - final TableSchema tableSchema = StaticTableSchema.builder(Secret.class) - .newItemSupplier(Secret::new) - .addAttribute(String.class, a -> a.name("id") - .getter(Secret::getPartitionKey) - .setter(Secret::setPartitionKey) - .tags(StaticAttributeTags.primaryPartitionKey()) - ) - .addAttribute(String.class, a -> a.name("env") - .getter(Secret::getEnv) - .setter(Secret::setEnv) - ) - .addAttribute(String.class, a -> a.name("costCentre") - .getter(Secret::getCostCentre) - .setter(Secret::setCostCentre) - ) - .addAttribute(String.class, a -> a.name("applicationName") - .getter(Secret::getApplicationName) - .setter(Secret::setApplicationName) - ) - .addAttribute(EnhancedType.mapOf(String.class, String.class), a -> a.name("variables") - .getter(Secret::getItems) - .setter(Secret::setItems) - ) - .build(); - - final DynamoDbTable mappedTable = enhancedClient.table("dev-coffeebeans-secret", tableSchema); - - mappedTable.putItem(secret); + this.mappedTable.putItem(secret); } } diff --git a/spring-native-aws-service/src/main/resources/application-local.yml b/spring-native-aws-service/src/main/resources/application-local.yml index 6261f96..c76e11d 100644 --- a/spring-native-aws-service/src/main/resources/application-local.yml +++ b/spring-native-aws-service/src/main/resources/application-local.yml @@ -19,6 +19,4 @@ spring: cloud: aws: dynamodb: - endpoint: http://localhost:8000 - - + endpoint: http://localhost:8000 \ No newline at end of file diff --git a/spring-native-aws-service/src/test/java/com/coffeebeans/springnativeawslambda/ApplicationIT.java b/spring-native-aws-service/src/test/java/com/coffeebeans/springnativeawslambda/ApplicationIT.java index 3232430..d70217e 100644 --- a/spring-native-aws-service/src/test/java/com/coffeebeans/springnativeawslambda/ApplicationIT.java +++ b/spring-native-aws-service/src/test/java/com/coffeebeans/springnativeawslambda/ApplicationIT.java @@ -73,4 +73,20 @@ void should_return_400() throws JsonProcessingException { .isEqualTo(objectMapper.writeValueAsString(response)); } + + @Test + void should_return_405() throws JsonProcessingException { + + final APIGatewayProxyRequestEvent request = new APIGatewayProxyRequestEvent() + .withBody("\"name\":\"Coffeebeans\"}") + .withHttpMethod("PUT"); + + final APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent() + .withStatusCode(405) + .withBody("{\"message\": \"Method not allowed. Supported methods [POST, GET]\",\"errorCode\": \"METHOD_NOT_ALLOWED\"}"); + + assertThat(aws.exchange(objectMapper.writeValueAsString(request)).getPayload()) + .isEqualTo(objectMapper.writeValueAsString(response)); + + } } \ No newline at end of file From 25396adce2d54e6ddd89f685a5a794ed29202814 Mon Sep 17 00:00:00 2001 From: matto Date: Mon, 25 Mar 2024 16:36:40 +1100 Subject: [PATCH 6/6] use `sandpiper` infra --- README.md | 42 ++++++++++--------- cdk.json | 2 +- docker-compose.yml | 2 +- .../infra/SpringNativeAwsFunctionStack.java | 9 ++-- .../ReflectionRuntimeHints.java | 26 ++++++------ .../src/shell/native/bootstrap | 2 +- 6 files changed, 43 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index e9d60bb..3176fa9 100644 --- a/README.md +++ b/README.md @@ -43,12 +43,17 @@ $ ./mvnw -ntp clean verify -U ``` The service responds ```json - [ - { - "name": "CoffeeBeans", - "saved": true - } - ] + { + "id": "production1234someapp", + "env": "production", + "costCentre": "1234", + "applicationName": "some-app", + "items": { + "GITHUB_TOKEN": "WOAH", + "AWS_ACCESS_KEY_ID": "OMG", + "AWS_SECRET_ACCESS_KEY": "OH NO" + } + } ``` #### Using `mvnw` @@ -81,16 +86,6 @@ $ ./mvnw -ntp clean verify -U "body": "{ \"env\": \"production\", \"costCentre\": \"1234\", \"applicationName\": \"some-app\", \"items\": { \"GITHUB_TOKEN\": \"WOAH\", \"AWS_ACCESS_KEY_ID\": \"OMG\", \"AWS_SECRET_ACCESS_KEY\": \"OH NO\" } }" }' ``` -```shell -curl --location --request POST 'http://localhost:8080' \ - --header 'Content-Type: application/json' \ - --data-raw '{ - "httpMethod": "GET", - "pathParameters": { - "proxy": "production-1234-someapp" - } - }' -``` The service responds ```json @@ -451,8 +446,11 @@ Now that the setup is done you can deploy to AWS. the [github action](.github/workflows/release.yml) will start and a deployment to AWS environment. 2. Test via curl - ```shell - $ curl --location --request POST 'https://lmk0qo0xrl.execute-api.ap-southeast-2.amazonaws.com/dev/' \ + +POST + +```shell + $ curl --location --request POST 'https://{apiid}.execute-api.ap-southeast-2.amazonaws.com/dev/' \ --header 'Content-Type: application/json' \ --data-raw '{ "env": "production", @@ -464,6 +462,12 @@ Now that the setup is done you can deploy to AWS. "AWS_SECRET_ACCESS_KEY": "OH NO" } }' - ``` +``` + +GET + + ```shell + curl --request GET 'https://{apiid}.execute-api.ap-southeast-2.amazonaws.com/dev/production-1234-someapp' + ``` 3. Et voila! It runs with 500 ms for cold start. diff --git a/cdk.json b/cdk.json index 323e56b..04ac302 100644 --- a/cdk.json +++ b/cdk.json @@ -1,5 +1,5 @@ { - "app": "./mvnw --settings /Users/muhammad/.m2/settings-nonqantas.xml exec:java -pl spring-native-aws-service-infra -Dexec.mainClass=com.coffeebeans.springnativeawslambda.infra.Application", + "app": "./mvnw exec:java -pl spring-native-aws-service-infra -Dexec.mainClass=com.coffeebeans.springnativeawslambda.infra.Application", "context": { "@aws-cdk/core:bootstrapQualifier": "cbcore" } diff --git a/docker-compose.yml b/docker-compose.yml index 9e14073..696ff14 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -49,7 +49,7 @@ services: ' function package_spring_native_function() { if [ "$$BUILD_ARTIFACT" = "true" ]; then - ./mvnw -ntp clean -Pnative -DskipTests native:compile package -pl "$$FUNCTION_NAME" --settings /home/worker/.m2/settings-nonqantas.xml + ./mvnw -ntp clean -Pnative -DskipTests native:compile package -pl "$$FUNCTION_NAME" else print_info_message "plain" "BUILD_ARTIFACT environment variable is not set. Skipping Maven build." fi diff --git a/spring-native-aws-service-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/SpringNativeAwsFunctionStack.java b/spring-native-aws-service-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/SpringNativeAwsFunctionStack.java index 5ac3173..ace88d6 100644 --- a/spring-native-aws-service-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/SpringNativeAwsFunctionStack.java +++ b/spring-native-aws-service-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/SpringNativeAwsFunctionStack.java @@ -21,17 +21,15 @@ import io.sadpipers.cdk.type.KebabCaseString; import io.sadpipers.cdk.type.SafeString; import io.sandpipers.cdk.core.AbstractEnvironment; +import io.sandpipers.cdk.core.construct.BaseStack; import io.sandpipers.cdk.core.construct.dynamodb.TableV2; import io.sandpipers.cdk.core.construct.dynamodb.TableV2.TableProps; import io.sandpipers.cdk.core.construct.lambda.CustomRuntime2023Function; import io.sandpipers.cdk.core.construct.lambda.CustomRuntime2023Function.CustomRuntime2023FunctionProps; import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; -import software.amazon.awscdk.DefaultStackSynthesizer; import software.amazon.awscdk.Duration; import software.amazon.awscdk.RemovalPolicy; -import software.amazon.awscdk.Stack; -import software.amazon.awscdk.StackProps; import software.amazon.awscdk.services.apigateway.LambdaRestApi; import software.amazon.awscdk.services.apigateway.Resource; import software.amazon.awscdk.services.apigateway.StageOptions; @@ -51,7 +49,7 @@ import static software.amazon.awscdk.services.iam.ManagedPolicy.fromAwsManagedPolicyName; import static software.amazon.awscdk.services.lambda.Code.fromAsset; -public class SpringNativeAwsFunctionStack extends Stack { +public class SpringNativeAwsFunctionStack extends BaseStack { private static final int LAMBDA_FUNCTION_TIMEOUT_IN_SECONDS = 3; private static final int LAMBDA_FUNCTION_MEMORY_SIZE = 512; @@ -63,8 +61,7 @@ public SpringNativeAwsFunctionStack(@NotNull final Application app, @NotNull final AbstractEnvironment environment, @NotBlank final String lambdaCodePath, @NotBlank final String stage) { - super(app, "SpringNativeAwsFunctionStack", - StackProps.builder().synthesizer(DefaultStackSynthesizer.Builder.create().qualifier("cbcore").build()).build()); + super(app, environment); final List managedPolicies = List.of(fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")); diff --git a/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/ReflectionRuntimeHints.java b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/ReflectionRuntimeHints.java index ceeeb72..b9e3771 100644 --- a/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/ReflectionRuntimeHints.java +++ b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/ReflectionRuntimeHints.java @@ -18,7 +18,6 @@ package com.coffeebeans.springnativeawslambda; -import java.util.List; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; import com.coffeebeans.springnativeawslambda.model.Secret; @@ -29,18 +28,21 @@ import org.springframework.aot.hint.TypeReference; import org.springframework.lang.Nullable; +import java.util.List; + public class ReflectionRuntimeHints implements RuntimeHintsRegistrar { - @Override - public void registerHints(final RuntimeHints hints, @Nullable final ClassLoader classLoader) { - final List typeReferences = List.of( - TypeReference.of(DateTime.class), - TypeReference.of(Secret.class), - TypeReference.of(APIGatewayProxyResponseEvent.class), - TypeReference.of(APIGatewayProxyRequestEvent.class), - TypeReference.of(APIGatewayProxyRequestEvent.ProxyRequestContext.class) - ); + @Override + public void registerHints(final RuntimeHints hints, @Nullable final ClassLoader classLoader) { + final List typeReferences = List.of( + TypeReference.of(DateTime.class), + TypeReference.of(Secret.class), + TypeReference.of(APIGatewayProxyResponseEvent.class), + TypeReference.of(APIGatewayProxyRequestEvent.class), + TypeReference.of(APIGatewayProxyRequestEvent.ProxyRequestContext.class), + TypeReference.of(APIGatewayProxyRequestEvent.RequestIdentity.class) + ); - hints.reflection().registerTypes(typeReferences, builder -> builder.withMembers(MemberCategory.values())); - } + hints.reflection().registerTypes(typeReferences, builder -> builder.withMembers(MemberCategory.values())); + } } diff --git a/spring-native-aws-service/src/shell/native/bootstrap b/spring-native-aws-service/src/shell/native/bootstrap index b3118d4..ff8d3b6 100755 --- a/spring-native-aws-service/src/shell/native/bootstrap +++ b/spring-native-aws-service/src/shell/native/bootstrap @@ -21,4 +21,4 @@ set -euo pipefail cd ${LAMBDA_TASK_ROOT:-.} -./spring-native-aws-function \ No newline at end of file +./spring-native-aws-service \ No newline at end of file