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..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 }} @@ -58,8 +62,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/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 dbea69c..3176fa9 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) @@ -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` @@ -56,8 +61,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 @@ -77,17 +82,24 @@ $ ./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\" } }" }' ``` + 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 @@ -134,7 +146,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:*" } } } @@ -150,67 +162,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 @@ -259,7 +355,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 +377,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 +406,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,15 +442,32 @@ 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 - ```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" + +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", + "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. \ No newline at end of file +``` + +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 350653f..04ac302 100644 --- a/cdk.json +++ b/cdk.json @@ -1,3 +1,6 @@ { - "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", + "context": { + "@aws-cdk/core:bootstrapQualifier": "cbcore" + } } \ 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..6a831cd 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,42 @@ - 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} + + io.sandpipers + sandpipers-cdk-bom + 1.0-SNAPSHOT + pom + import + + com.amazonaws @@ -187,7 +202,7 @@ software.amazon.awscdk aws-cdk-lib - 2.116.1 + 2.128.0 @@ -213,6 +228,13 @@ test + + org.junit.jupiter + junit-jupiter-params + ${junit-jupiter.version} + test + + org.mockito mockito-core diff --git a/spring-native-aws-lambda-function/src/main/java/com/coffeebeans/springnativeawslambda/function/ExampleFunction.java b/spring-native-aws-lambda-function/src/main/java/com/coffeebeans/springnativeawslambda/function/ExampleFunction.java deleted file mode 100644 index b24fb38..0000000 --- a/spring-native-aws-lambda-function/src/main/java/com/coffeebeans/springnativeawslambda/function/ExampleFunction.java +++ /dev/null @@ -1,71 +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 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 com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.validation.constraints.NotNull; -import java.util.function.Function; -import lombok.SneakyThrows; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.springframework.validation.annotation.Validated; - -@Component -@Slf4j -@Validated -public class ExampleFunction implements - Function { - - private final ObjectMapper objectMapper; - - public ExampleFunction(@NotNull final ObjectMapper objectMapper) { - this.objectMapper = objectMapper; - } - - /** - * Lambda function handler that takes a request and returns a response. - * - * @param proxyRequestEvent the function argument - * @return {@link APIGatewayProxyResponseEvent} - * @throws JsonProcessingException - */ - @Override - @SneakyThrows(value = JsonProcessingException.class) - public APIGatewayProxyResponseEvent apply(final APIGatewayProxyRequestEvent proxyRequestEvent) { - log.info("Converting request into a response...'"); - - final Request request = objectMapper.readValue(proxyRequestEvent.getBody(), Request.class); - - final Response response = Response.builder() - .name(request.getName()) - .saved(true) - .build(); - - log.info("Converted request into a response."); - - return new APIGatewayProxyResponseEvent() - .withStatusCode(200) - .withBody(objectMapper.writeValueAsString(response)); - } -} \ No newline at end of file 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/TagUtilsTest.java b/spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/TagUtilsTest.java deleted file mode 100644 index a3fa472..0000000 --- a/spring-native-aws-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/TagUtilsTest.java +++ /dev/null @@ -1,44 +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 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/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..84f34c7 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 @@ + + io.sandpipers + sandpipers-cdk-core + + org.projectlombok @@ -94,8 +99,8 @@ - cloud.pianola - cdk-fluent-assertions + io.sandpipers + sandpipers-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..44cc4c7 --- /dev/null +++ b/spring-native-aws-service-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/Application.java @@ -0,0 +1,67 @@ +/* + * 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 io.sadpipers.cdk.type.SafeString; +import io.sandpipers.cdk.core.AbstractApp; +import org.jetbrains.annotations.NotNull; +import software.amazon.awscdk.Tags; + +import java.util.Map; +import java.util.Objects; + +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; + +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) { + final Application app = new Application(); + + final String env = System.getenv(KEY_ENV); + 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); + 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-function/src/main/java/com/coffeebeans/springnativeawslambda/model/Request.java b/spring-native-aws-service-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/CostCentre.java similarity index 57% rename from spring-native-aws-lambda-function/src/main/java/com/coffeebeans/springnativeawslambda/model/Request.java rename to spring-native-aws-service-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/CostCentre.java index 81d1995..06fe7ad 100644 --- a/spring-native-aws-lambda-function/src/main/java/com/coffeebeans/springnativeawslambda/model/Request.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,23 +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.model; +package com.coffeebeans.springnativeawslambda.infra; + +import io.sadpipers.cdk.type.AlphanumericString; +import io.sandpipers.cdk.core.AbstractCostCentre; +import lombok.Getter; +import lombok.experimental.SuperBuilder; -import jakarta.validation.constraints.NotBlank; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; +@Getter +@SuperBuilder +public class CostCentre extends AbstractCostCentre { -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class Request { + public static final CostCentre COFFEE_BEANS = CostCentre.builder() + .value(AlphanumericString.of("cbcore")) + .build(); - @NotBlank - private String name; + static { + registerCostCentre(COFFEE_BEANS); + } } 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..ef8c0e6 --- /dev/null +++ b/spring-native-aws-service-infra/src/main/java/com/coffeebeans/springnativeawslambda/infra/Environment.java @@ -0,0 +1,54 @@ +package com.coffeebeans.springnativeawslambda.infra; + + +import io.sadpipers.cdk.type.AWSAccount; +import io.sadpipers.cdk.type.SafeString; +import io.sandpipers.cdk.core.AbstractEnvironment; +import lombok.Getter; +import lombok.experimental.SuperBuilder; + +import static io.sandpipers.cdk.core.util.Constants.AWS_REGION_AP_SOUTHEAST_2; + +@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..ace88d6 --- /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 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.Duration; +import software.amazon.awscdk.RemovalPolicy; +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.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 javax.validation.constraints.NotBlank; +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 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 Application 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")) + .removalPolicy(RemovalPolicy.RETAIN_ON_UPDATE_OR_DELETE) + .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..40e531e --- /dev/null +++ b/spring-native-aws-service-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/LambdaTest.java @@ -0,0 +1,118 @@ +/* + * 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.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.util.Map; + +import static io.sandpipers.cdk.assertion.CDKStackAssert.assertThat; + +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-\\$\\{AWS\\:\\:AccountId\\}-\\$\\{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 + @Disabled + 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( + { + "^RestApiproxyANYApiPermissionSpringNativeAwsFunctionStackRestApi[A-Z0-9]{8}ANYproxy[A-Z0-9]{8}$, arn:aws:execute-api::AWS::AccountId:RestApi[A-Z0-9]{8}/RestApiDeploymentStagetest[A-Z0-9]{8}/\\*/\\*", + "^RestApiproxyANYApiPermissionTestSpringNativeAwsFunctionStackRestApi[A-Z0-9]{8}ANYproxy[A-Z0-9]{8}$, arn:aws:execute-api::AWS::AccountId:RestApi[A-Z0-9]{8}/test-invoke-stage/\\*/\\*", + "^RestApiANYApiPermissionSpringNativeAwsFunctionStackRestApi[A-Z0-9]{8}ANY[A-Z0-9]{8}$, arn:aws:execute-api::AWS::AccountId:RestApi[A-Z0-9]{8}/RestApiDeploymentStagetest[A-Z0-9]{8}/\\*/", + "^RestApiANYApiPermissionTestSpringNativeAwsFunctionStackRestApi[A-Z0-9]{8}ANY[A-Z0-9]{8}$, arn:aws:execute-api::AWS::AccountId:RestApi[A-Z0-9]{8}/test-invoke-stage/\\*/", + "^RestApivariablesANYApiPermissionSpringNativeAwsFunctionStackRestApi[A-Z0-9]{8}ANYvariables[A-Z0-9]{8}$, arn:aws:execute-api::AWS::AccountId:RestApi[A-Z0-9]{8}/RestApiDeploymentStagetest[A-Z0-9]{8}/\\*/variables", + "^RestApivariablesANYApiPermissionTestSpringNativeAwsFunctionStackRestApi[A-Z0-9]{8}ANYvariables[A-Z0-9]{8}$, arn:aws:execute-api::AWS::AccountId:RestApi[A-Z0-9]{8}/test-invoke-stage/\\*/variables", + "^RestApivariablesGETApiPermissionSpringNativeAwsFunctionStackRestApi[A-Z0-9]{8}GETvariables[A-Z0-9]{8}$, arn:aws:execute-api::AWS::AccountId:RestApi[A-Z0-9]{8}/RestApiDeploymentStagetest[A-Z0-9]{8}/GET/variables", + "^RestApivariablesGETApiPermissionTestSpringNativeAwsFunctionStackRestApi[A-Z0-9]{8}GETvariables[A-Z0-9]{8}$, arn:aws:execute-api::AWS::AccountId: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-lambda-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/TopicTest.java b/spring-native-aws-service-infra/src/test/java/com/coffeebeans/springnativeawslambda/infra/QueueTest.java similarity index 71% 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/QueueTest.java index 401bc02..6b1cda1 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/QueueTest.java @@ -20,18 +20,18 @@ import org.junit.jupiter.api.Test; -import static cloud.pianola.cdk.fluent.assertion.CDKStackAssert.assertThat; -import static com.coffeebeans.springnativeawslambda.infra.Constants.KEY_COST_CENTRE; +import static io.sandpipers.cdk.assertion.CDKStackAssert.assertThat; -class TopicTest extends TemplateSupport { +class QueueTest extends TemplateSupport { public static final String TEST = "test"; @Test void should_have_dead_letter_topic() { assertThat(template) - .containsTopic("spring-native-aws-lambda-function-dead-letter-topic") - .hasTag("COST_CENTRE", KEY_COST_CENTRE) - .hasTag("ENV", TEST); + .containsQueue("^LambdaDeadLetterQueue[A-Z0-9]{8}$") + .hasTag("COST_CENTRE", "cbcore") + .hasTag("ENVIRONMENT", TEST) + .hasTag("APPLICATION_NAME", "spring-native-aws-function"); } -} \ 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/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 93% rename from spring-native-aws-lambda-function/pom.xml rename to spring-native-aws-service/pom.xml index 0c4b28c..32e65c8 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 @@ -43,7 +43,9 @@ 2023.0.0 - + + 21 + 21 @@ -100,6 +102,11 @@ org.springframework.boot spring-boot-starter-validation + + + io.awspring.cloud + spring-cloud-aws-starter-dynamodb + @@ -170,6 +177,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..91337ea 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-service 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 72% 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 index e3764be..f42423d 100644 --- 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 @@ -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-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 62% 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..b9e3771 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,11 +18,9 @@ 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.Request; -import com.coffeebeans.springnativeawslambda.model.Response; +import com.coffeebeans.springnativeawslambda.model.Secret; import org.joda.time.DateTime; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; @@ -30,19 +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(RuntimeHints hints, @Nullable 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)); + @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-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..5d4a0ee --- /dev/null +++ b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/entity/Secret.java @@ -0,0 +1,38 @@ +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; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Secret implements Serializable { + + private String env; + + private String costCentre; + + private String applicationName; + + private String partitionKey; + + private Map items; + + 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 new file mode 100644 index 0000000..755e173 --- /dev/null +++ b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/function/ExampleFunction.java @@ -0,0 +1,116 @@ +/* + * 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.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; +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.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 +@Validated +public class ExampleFunction implements + Function { + + private final ObjectMapper objectMapper; + + private SecretRepository secretRepository; + + public ExampleFunction( + @NotNull final SecretRepository secretRepository, + @NotNull final ObjectMapper objectMapper + ) { + this.objectMapper = objectMapper; + this.secretRepository = secretRepository; + + } + + /** + * Lambda function handler that takes a request and returns a response. + * + * @param proxyRequestEvent the function argument + * @return {@link APIGatewayProxyResponseEvent} + * @throws JsonProcessingException + */ + @Override + @SneakyThrows(value = JsonProcessingException.class) + public APIGatewayProxyResponseEvent apply(final APIGatewayProxyRequestEvent proxyRequestEvent) { + 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("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)); + } +} \ No newline at end of file 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/Secret.java similarity index 50% 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/Secret.java index 438e804..9d16fa7 100644 --- 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/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,41 @@ @Builder @NoArgsConstructor @AllArgsConstructor -public class Response { - +public class Secret { @NotBlank @JsonProperty(access = Access.READ_ONLY) - private String name; + private String id; - @NotNull - @JsonProperty(access = Access.READ_ONLY) - private boolean saved; + @NotBlank + private String env; + + @NotBlank + private String costCentre; + + @NotBlank + private String applicationName; + + @NotEmpty + private Map items; + + public String getId() { + 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 new file mode 100644 index 0000000..eb4b2e9 --- /dev/null +++ b/spring-native-aws-service/src/main/java/com/coffeebeans/springnativeawslambda/repository/SecretRepository.java @@ -0,0 +1,56 @@ +package com.coffeebeans.springnativeawslambda.repository; + +import com.coffeebeans.springnativeawslambda.entity.Secret; +import jakarta.validation.constraints.NotNull; +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.StaticAttributeTags; +import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; + +@Repository +public class SecretRepository { + + 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 DynamoDbTable mappedTable; + + public SecretRepository(@NotNull final DynamoDbEnhancedClient enhancedClient) { + this.mappedTable = enhancedClient.table("secrets", TABLE_SCHEMA); + } + + 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) { + this.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 87% 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..c76e11d 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,8 @@ spring: main: - web-application-type: servlet \ No newline at end of file + web-application-type: servlet + cloud: + aws: + dynamodb: + endpoint: http://localhost:8000 \ No newline at end of file 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 90% 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..2562a8a 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,8 @@ 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 +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..ff8d3b6 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-service \ 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 82% 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 index 3232430..d70217e 100644 --- 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 @@ -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 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