From eb7e9afbb6e2af6cc5500bba8dcec54665d2871b Mon Sep 17 00:00:00 2001
From: sobrab <170367248+sobrab@users.noreply.github.com>
Date: Fri, 22 Nov 2024 23:00:22 +0200
Subject: [PATCH 1/4] added a Java example for an API Gateway with proxy
resources to HTTP backend APIs
---
java/http-proxy-apigateway/.gitignore | 13 ++
java/http-proxy-apigateway/README.md | 54 +++++
java/http-proxy-apigateway/cdk.json | 76 +++++++
java/http-proxy-apigateway/pom.xml | 62 +++++
.../com/myorg/HttpProxyApiGatewayApp.java | 29 +++
.../com/myorg/HttpProxyApiGatewayStack.java | 67 ++++++
.../com/myorg/HttpProxyApiGatewayTest.java | 213 ++++++++++++++++++
.../src/test/java/com/myorg/TestUtils.java | 80 +++++++
8 files changed, 594 insertions(+)
create mode 100644 java/http-proxy-apigateway/.gitignore
create mode 100644 java/http-proxy-apigateway/README.md
create mode 100644 java/http-proxy-apigateway/cdk.json
create mode 100644 java/http-proxy-apigateway/pom.xml
create mode 100644 java/http-proxy-apigateway/src/main/java/com/myorg/HttpProxyApiGatewayApp.java
create mode 100644 java/http-proxy-apigateway/src/main/java/com/myorg/HttpProxyApiGatewayStack.java
create mode 100644 java/http-proxy-apigateway/src/test/java/com/myorg/HttpProxyApiGatewayTest.java
create mode 100644 java/http-proxy-apigateway/src/test/java/com/myorg/TestUtils.java
diff --git a/java/http-proxy-apigateway/.gitignore b/java/http-proxy-apigateway/.gitignore
new file mode 100644
index 0000000000..1db21f1629
--- /dev/null
+++ b/java/http-proxy-apigateway/.gitignore
@@ -0,0 +1,13 @@
+.classpath.txt
+target
+.classpath
+.project
+.idea
+.settings
+.vscode
+*.iml
+
+# CDK asset staging directory
+.cdk.staging
+cdk.out
+
diff --git a/java/http-proxy-apigateway/README.md b/java/http-proxy-apigateway/README.md
new file mode 100644
index 0000000000..5a16df6a95
--- /dev/null
+++ b/java/http-proxy-apigateway/README.md
@@ -0,0 +1,54 @@
+# HTTP Proxy APIGateway
+
+
+
+---
+
+> **This is a stable example. It should successfully build out of the box**
+>
+> This example is built on Construct Libraries marked "Stable" and does not have any infrastructure prerequisites to build.
+---
+
+
+
+This example creates an API Gateway with proxy resources for 2 HTTP backends.
+More HTTP backend APIs can be easily added.
+This is useful for scenarios when incoming requests must be routed to one or more backend API hosts.
+An HTTP proxy integration enables direct interactions between clients and backends without any intervention from API Gateway after the API method is set up.
+
+> For more information on using HTTP proxy integrations with the APIGateway check out this [AWS tutorial](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-create-api-as-simple-proxy-for-http.html).
+
+> For demonstration purposes this CDK example deploys a solution that routes to the [PetsStore API](http://petstore-demo-endpoint.execute-api.com/) (from this [AWS tutorial](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-create-api-as-simple-proxy-for-http.html)) and to the [OpenTrivia API](https://opentdb.com).
+> If you prefer to use your own HTTP backend APIs modify the argument used to call the [`HttpProxyApiGatewayStack`](src/main/java/com/myorg/HttpProxyApiGatewayStack.java) constructor in the [`HttpProxyApiGatewayApp`](src/main/java/com/myorg/HttpProxyApiGatewayApp.java) class.
+
+## Build
+
+To build this example, you need to be in this example's root directory. Then run the following:
+
+```bash
+ npm install -g aws-cdk
+ npm install
+ cdk synth
+```
+
+This will install the necessary CDK, then this example's dependencies, and then build the CloudFormation template. The resulting CloudFormation template will be in the `cdk.out` directory.
+
+## Deploy
+
+Run `cdk deploy`.
+This will deploy / redeploy the Stack to AWS.
+After the CDK deployment is successful, 2 URL examples will be available in the terminal console output:
+
+- One for the `PetStoreProxyEndPointGetRequestExample` stack output
+- One for the `OpenTriviaProxyEndPointGetRequestExample` stack output
+
+At this point, you can copy each of the 2 URLs and paste them in the address bar of a browser to invoke the 2 APIs.
+
+## Useful commands
+
+* `mvn package` compile and run tests
+* `cdk ls` list all stacks in the app
+* `cdk synth` emits the synthesized CloudFormation template
+* `cdk deploy` deploy this stack to your default AWS account/region
+* `cdk diff` compare deployed stack with current state
+* `cdk docs` open CDK documentation
diff --git a/java/http-proxy-apigateway/cdk.json b/java/http-proxy-apigateway/cdk.json
new file mode 100644
index 0000000000..1ce7118872
--- /dev/null
+++ b/java/http-proxy-apigateway/cdk.json
@@ -0,0 +1,76 @@
+{
+ "app": "mvn -e -q compile exec:java",
+ "watch": {
+ "include": [
+ "**"
+ ],
+ "exclude": [
+ "README.md",
+ "cdk*.json",
+ "target",
+ "pom.xml",
+ "src/test"
+ ]
+ },
+ "context": {
+ "@aws-cdk/aws-lambda:recognizeLayerVersion": true,
+ "@aws-cdk/core:checkSecretUsage": true,
+ "@aws-cdk/core:target-partitions": [
+ "aws",
+ "aws-cn"
+ ],
+ "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
+ "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
+ "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
+ "@aws-cdk/aws-iam:minimizePolicies": true,
+ "@aws-cdk/core:validateSnapshotRemovalPolicy": true,
+ "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
+ "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
+ "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
+ "@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
+ "@aws-cdk/core:enablePartitionLiterals": true,
+ "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
+ "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
+ "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
+ "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
+ "@aws-cdk/aws-route53-patters:useCertificate": true,
+ "@aws-cdk/customresources:installLatestAwsSdkDefault": false,
+ "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true,
+ "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true,
+ "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true,
+ "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true,
+ "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true,
+ "@aws-cdk/aws-redshift:columnId": true,
+ "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true,
+ "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true,
+ "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true,
+ "@aws-cdk/aws-kms:aliasNameRef": true,
+ "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true,
+ "@aws-cdk/core:includePrefixInUniqueNameGeneration": true,
+ "@aws-cdk/aws-efs:denyAnonymousAccess": true,
+ "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true,
+ "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true,
+ "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true,
+ "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true,
+ "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true,
+ "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true,
+ "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true,
+ "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true,
+ "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true,
+ "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true,
+ "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true,
+ "@aws-cdk/aws-eks:nodegroupNameAttribute": true,
+ "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true,
+ "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true,
+ "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false,
+ "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false,
+ "@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true,
+ "@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true,
+ "@aws-cdk/aws-ec2:ec2SumTimeoutEnabled": true,
+ "@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true,
+ "@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true,
+ "@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true,
+ "@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true,
+ "@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true
+ }
+}
diff --git a/java/http-proxy-apigateway/pom.xml b/java/http-proxy-apigateway/pom.xml
new file mode 100644
index 0000000000..90e17e056b
--- /dev/null
+++ b/java/http-proxy-apigateway/pom.xml
@@ -0,0 +1,62 @@
+
+
+ 4.0.0
+
+ com.myorg
+ http-proxy-apigateway
+ 0.1
+
+
+ UTF-8
+ 2.167.1
+ [10.0.0,11.0.0)
+ 5.7.1
+ 22
+ 22
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.11.0
+
+ 22
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 3.1.0
+
+ com.myorg.HttpProxyApiGatewayApp
+
+
+
+
+
+
+
+
+ software.amazon.awscdk
+ aws-cdk-lib
+ ${cdk.version}
+
+
+
+ software.constructs
+ constructs
+ ${constructs.version}
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ ${junit.version}
+ test
+
+
+
diff --git a/java/http-proxy-apigateway/src/main/java/com/myorg/HttpProxyApiGatewayApp.java b/java/http-proxy-apigateway/src/main/java/com/myorg/HttpProxyApiGatewayApp.java
new file mode 100644
index 0000000000..0d9cbe6901
--- /dev/null
+++ b/java/http-proxy-apigateway/src/main/java/com/myorg/HttpProxyApiGatewayApp.java
@@ -0,0 +1,29 @@
+package com.myorg;
+
+import software.amazon.awscdk.App;
+import software.amazon.awscdk.StackProps;
+import software.amazon.awscdk.services.apigatewayv2.HttpMethod;
+
+import java.util.List;
+
+import static com.myorg.HttpProxyApiGatewayStack.*;
+
+public class HttpProxyApiGatewayApp {
+ public static void main(final String[] args) {
+ var app = new App();
+ new HttpProxyApiGatewayStack(
+ app,
+ "HttpProxyApiGatewayStack",
+ StackProps.builder()
+ .stackName("HttpProxyApiGatewayStack")
+ .build(),
+ List.of(
+ // Replace with resources corresponding to your own HTTP backend APIs if you want to.
+ // Add more resources if you need to.
+ new ProxyResourceParameters("PetStore", "http://petstore-demo-endpoint.execute-api.com", HttpMethod.ANY.name(), "/petstore/pets?type=fish"),
+ new ProxyResourceParameters("OpenTrivia", "https://opentdb.com", HttpMethod.ANY.name(), "/api.php?amount=10")
+ )
+ );
+ app.synth();
+ }
+}
diff --git a/java/http-proxy-apigateway/src/main/java/com/myorg/HttpProxyApiGatewayStack.java b/java/http-proxy-apigateway/src/main/java/com/myorg/HttpProxyApiGatewayStack.java
new file mode 100644
index 0000000000..986dc5d6bc
--- /dev/null
+++ b/java/http-proxy-apigateway/src/main/java/com/myorg/HttpProxyApiGatewayStack.java
@@ -0,0 +1,67 @@
+package com.myorg;
+
+import software.amazon.awscdk.CfnOutput;
+import software.amazon.awscdk.RemovalPolicy;
+import software.amazon.awscdk.Stack;
+import software.amazon.awscdk.StackProps;
+import software.amazon.awscdk.services.apigateway.*;
+import software.constructs.Construct;
+
+import java.util.List;
+import java.util.Map;
+
+public class HttpProxyApiGatewayStack extends Stack {
+
+ public record ProxyResourceParameters(String resourceId, String baseUrl, String httpMethod, String exampleRequest) {
+
+ }
+
+ private final RestApi restApi;
+
+ public HttpProxyApiGatewayStack(final Construct scope, final String id, final StackProps props, List proxyResourceParameters) {
+ super(scope, id, props);
+ restApi = RestApi.Builder.create(this, "RestApi")
+ .restApiName("RestApi")
+ .cloudWatchRole(true)
+ .cloudWatchRoleRemovalPolicy(RemovalPolicy.DESTROY)
+ .endpointTypes(List.of(EndpointType.EDGE))
+ .build();
+ proxyResourceParameters.forEach(this::createProxyResource);
+ }
+
+ private void createProxyResource(ProxyResourceParameters proxyResourceParameters) {
+ var parentProxyResource = restApi.getRoot().addResource(proxyResourceParameters.resourceId);
+ var proxyResource = ProxyResource.Builder.create(this, proxyResourceParameters.resourceId + "ProxyResource")
+ .parent(parentProxyResource)
+ .anyMethod(false)
+ .build();
+ var integrationOptions = IntegrationOptions.builder()
+ .requestParameters(
+ Map.of(
+ "integration.request.path.proxy", "method.request.path.proxy"
+ )
+ )
+ .build();
+ var httpIntegrationProps = HttpIntegrationProps.builder()
+ .proxy(true)
+ .httpMethod(proxyResourceParameters.httpMethod)
+ .options(integrationOptions)
+ .build();
+ var methodOptions = MethodOptions.builder()
+ .requestParameters(
+ Map.of(
+ "method.request.path.proxy", true
+ )
+ )
+ .build();
+ var httpIntegration = new HttpIntegration(proxyResourceParameters.baseUrl + "/{proxy}", httpIntegrationProps);
+ proxyResource.addMethod(proxyResourceParameters.httpMethod, httpIntegration, methodOptions);
+ var proxyResourceUrl = restApi.urlForPath(proxyResource.getPath());
+ CfnOutput.Builder.create(this, proxyResourceParameters.resourceId + "ProxyEndPointAnyRequestTemplate")
+ .value(proxyResourceUrl)
+ .build();
+ CfnOutput.Builder.create(this, proxyResourceParameters.resourceId + "ProxyEndPointGetRequestExample")
+ .value(proxyResourceUrl.replace("/{proxy+}", proxyResourceParameters.exampleRequest))
+ .build();
+ }
+}
diff --git a/java/http-proxy-apigateway/src/test/java/com/myorg/HttpProxyApiGatewayTest.java b/java/http-proxy-apigateway/src/test/java/com/myorg/HttpProxyApiGatewayTest.java
new file mode 100644
index 0000000000..731d6da045
--- /dev/null
+++ b/java/http-proxy-apigateway/src/test/java/com/myorg/HttpProxyApiGatewayTest.java
@@ -0,0 +1,213 @@
+package com.myorg;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.platform.commons.util.StringUtils;
+import software.amazon.awscdk.App;
+import software.amazon.awscdk.StackProps;
+import software.amazon.awscdk.cxapi.CloudFormationStackArtifact;
+import software.amazon.awscdk.services.apigatewayv2.HttpMethod;
+
+import java.util.*;
+import java.util.stream.Stream;
+
+import static com.myorg.HttpProxyApiGatewayStack.*;
+import static com.myorg.TestUtils.*;
+
+public class HttpProxyApiGatewayTest {
+
+ private static final Map stackResourcesMap = new HashMap<>();
+
+ @BeforeAll
+ public static void setUp() {
+ var app = App.Builder.create().build();
+ var httpProxyApiGatewayStack = new HttpProxyApiGatewayStack(
+ app,
+ "HttpProxyApiGatewayStack",
+ StackProps.builder()
+ .stackName("HttpProxyApiGatewayStack")
+ .build(),
+ List.of(
+ new ProxyResourceParameters("PetStore", "http://petstore-demo-endpoint.execute-api.com", HttpMethod.ANY.name(), "/petstore/pets?type=fish"),
+ new ProxyResourceParameters("OpenTrivia", "https://opentdb.com", HttpMethod.ANY.name(), "/api.php?amount=10")
+ )
+ );
+ Optional.of(app)
+ .map(App::synth)
+ .flatMap(
+ cloudAssembly -> Optional.of(httpProxyApiGatewayStack)
+ .map(HttpProxyApiGatewayStack::getArtifactId)
+ .map(cloudAssembly::getStackArtifact)
+ .map(CloudFormationStackArtifact::getTemplate)
+ ).flatMap(
+ template -> Optional.of(new ObjectMapper())
+ .map(objectMapper -> objectMapper.valueToTree(template))
+ .map(jsonNode -> jsonNode.at("/Resources"))
+ .map(JsonNode::fields)
+ ).ifPresent(stackResourceIterator -> stackResourceIterator.forEachRemaining(
+ stackResourceEntry -> {
+ if (stackResourceEntry != null &&
+ StringUtils.isNotBlank(stackResourceEntry.getKey()) &&
+ stackResourceEntry.getValue() != null &&
+ !stackResourceEntry.getValue().isEmpty()
+ ) {
+ stackResourcesMap.put(stackResourceEntry.getKey(), stackResourceEntry.getValue());
+ }
+ })
+ );
+ }
+
+ @Test
+ @DisplayName("Test if the expected REST API is present in the resources of the stack.")
+ public void testRestApi() {
+ var restApiMatchMap = Map.of(
+ "/Type", "AWS::ApiGateway::RestApi",
+ "/Properties/EndpointConfiguration/Types/0", "EDGE",
+ "/Properties/Name", "RestApi"
+ );
+ Assertions.assertNotNull(findResource(stackResourcesMap, restApiMatchMap));
+ }
+
+ @Test
+ @DisplayName("Test if the expected IAM role for the REST API is present in the resources of the stack.")
+ public void testIamRole() {
+ var iamRoleMatchMap = Map.of(
+ "/Type", "AWS::IAM::Role",
+ "/Properties/AssumeRolePolicyDocument/Statement/0/Action", "sts:AssumeRole",
+ "/Properties/AssumeRolePolicyDocument/Statement/0/Effect", "Allow",
+ "/Properties/AssumeRolePolicyDocument/Statement/0/Principal/Service", "apigateway.amazonaws.com",
+ "/Properties/AssumeRolePolicyDocument/Version", "2012-10-17",
+ "/Properties/ManagedPolicyArns/0/Fn::Join/0", "",
+ "/Properties/ManagedPolicyArns/0/Fn::Join/1/0", "arn:",
+ "/Properties/ManagedPolicyArns/0/Fn::Join/1/1/Ref", "AWS::Partition",
+ "/Properties/ManagedPolicyArns/0/Fn::Join/1/2", ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs",
+ "/DeletionPolicy", "Delete"
+ );
+ Assertions.assertNotNull(findResource(stackResourcesMap, iamRoleMatchMap));
+ }
+
+ @Test
+ @DisplayName("Test if the expected API Gateway account is present in the resources of the stack.")
+ public void testApiGatewayAccount() {
+ var restApiMatchMap = Map.of(
+ "/Type", "AWS::ApiGateway::RestApi"
+ );
+ var restApiId = findResourceId(stackResourcesMap, restApiMatchMap);
+ var iamRoleMatchMap = Map.of(
+ "/Type", "AWS::IAM::Role"
+ );
+ var iamRoleId = findResourceId(stackResourcesMap, iamRoleMatchMap);
+ var apiGatewayAccountMatchMap = Map.of(
+ "/Type", "AWS::ApiGateway::Account",
+ "/Properties/CloudWatchRoleArn/Fn::GetAtt/0", iamRoleId,
+ "/Properties/CloudWatchRoleArn/Fn::GetAtt/1", "Arn",
+ "/DependsOn/0", restApiId,
+ "/UpdateReplacePolicy", "Delete",
+ "/DeletionPolicy", "Delete"
+ );
+ Assertions.assertNotNull(findResource(stackResourcesMap, apiGatewayAccountMatchMap));
+ }
+
+ @Test
+ @DisplayName("Test if the expected API Gateway deployment is present in the resources of the stack.")
+ public void testApiGatewayDeployment() {
+ var restApiMatchMap = Map.of(
+ "/Type", "AWS::ApiGateway::RestApi"
+ );
+ var restApiId = findResourceId(stackResourcesMap, restApiMatchMap);
+ var apiGatewayDeploymentMatchMap = Map.of(
+ "/Type", "AWS::ApiGateway::Deployment",
+ "/Properties/RestApiId/Ref", restApiId
+ );
+ var apiGatewayDeployment = findResource(stackResourcesMap, apiGatewayDeploymentMatchMap);
+ Assertions.assertNotNull(apiGatewayDeployment);
+ var dependsOnActualIds = extractAsStringArray(apiGatewayDeployment, "/DependsOn");
+ var dependsOnExpectedIds = Stream.of(
+ findResourcesIds(stackResourcesMap, Map.of("/Type", "AWS::ApiGateway::Resource")),
+ findResourcesIds(stackResourcesMap, Map.of("/Type", "AWS::ApiGateway::Method"))
+ )
+ .flatMap(Set::stream)
+ .collect(HashSet::new, Set::add, Set::addAll);
+ Assertions.assertEquals(dependsOnExpectedIds, dependsOnActualIds);
+ }
+
+ @Test
+ @DisplayName("Test if the expected API Gateway stage is present in the resources of the stack.")
+ public void testApiGatewayStage() {
+ var restApiMatchMap = Map.of(
+ "/Type", "AWS::ApiGateway::RestApi"
+ );
+ var restApiId = findResourceId(stackResourcesMap, restApiMatchMap);
+ var apiGatewayDeploymentMatchMap = Map.of(
+ "/Type", "AWS::ApiGateway::Deployment"
+ );
+ var apiGatewayDeploymentId = findResourceId(stackResourcesMap, apiGatewayDeploymentMatchMap);
+ var apiGatewayAccount = Map.of(
+ "/Type", "AWS::ApiGateway::Account"
+ );
+ var apiGatewayAccountId = findResourceId(stackResourcesMap, apiGatewayAccount);
+ var stageMatchMap = Map.of(
+ "/Type", "AWS::ApiGateway::Stage",
+ "/Properties/RestApiId/Ref", restApiId,
+ "/Properties/DeploymentId/Ref", apiGatewayDeploymentId,
+ "/Properties/StageName", "prod",
+ "/DependsOn/0", apiGatewayAccountId
+ );
+ Assertions.assertNotNull(findResource(stackResourcesMap, stageMatchMap));
+ }
+
+ @Test
+ @DisplayName("Test if the expected API Gateway resources are present in the resources of the stack.")
+ public void testApiGatewayResources() {
+ var restApiMatchMap = Map.of(
+ "/Type", "AWS::ApiGateway::RestApi"
+ );
+ var restApiId = findResourceId(stackResourcesMap, restApiMatchMap);
+ var apiGatewayResourcesMatchMap = Map.of(
+ "/Type", "AWS::ApiGateway::Resource",
+ "/Properties/RestApiId/Ref", restApiId
+ );
+ Long expectedApiGatewayResources = 4L;
+ Assertions.assertTrue(
+ Optional.ofNullable(findResources(stackResourcesMap, apiGatewayResourcesMatchMap))
+ .map(Map::entrySet)
+ .map(Set::stream)
+ .map(Stream::count)
+ .filter(resourcesCont -> resourcesCont.equals(expectedApiGatewayResources))
+ .isPresent()
+ );
+ }
+
+ @Test
+ @DisplayName("Test if the expected API Gateway methods are present in the resources of the stack.")
+ public void testApiGatewayMethods() {
+ var restApiMatchMap = Map.of(
+ "/Type", "AWS::ApiGateway::RestApi"
+ );
+ var restApiId = findResourceId(stackResourcesMap, restApiMatchMap);
+ var apiGatewayMethodsMatchMap = Map.of(
+ "/Type", "AWS::ApiGateway::Method",
+ "/Properties/AuthorizationType", "NONE",
+ "/Properties/HttpMethod", "ANY",
+ "/Properties/Integration/IntegrationHttpMethod", "ANY",
+ "/Properties/Integration/RequestParameters/integration.request.path.proxy", "method.request.path.proxy",
+ "/Properties/Integration/Type", "HTTP_PROXY",
+ "/Properties/RequestParameters/method.request.path.proxy", "true",
+ "/Properties/RestApiId/Ref", restApiId
+ );
+ Long expectedPermissionsCount = 2L;
+ Assertions.assertTrue(
+ Optional.ofNullable(findResources(stackResourcesMap, apiGatewayMethodsMatchMap))
+ .map(Map::entrySet)
+ .map(Set::stream)
+ .map(Stream::count)
+ .filter(methodsCont -> methodsCont.equals(expectedPermissionsCount))
+ .isPresent()
+ );
+ }
+
+}
diff --git a/java/http-proxy-apigateway/src/test/java/com/myorg/TestUtils.java b/java/http-proxy-apigateway/src/test/java/com/myorg/TestUtils.java
new file mode 100644
index 0000000000..4dd130c267
--- /dev/null
+++ b/java/http-proxy-apigateway/src/test/java/com/myorg/TestUtils.java
@@ -0,0 +1,80 @@
+package com.myorg;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+import java.util.*;
+import java.util.Map.Entry;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import static org.junit.platform.commons.util.StringUtils.isNotBlank;
+
+public class TestUtils {
+
+ public static String findResourceId(Map stackResources, Map matchMap) {
+ return Optional.ofNullable(findResource(stackResources, matchMap))
+ .map(Entry::getKey)
+ .orElse(null);
+ }
+
+ public static Entry findResource(Map stackResources, Map matchMap) {
+ return stackResources.entrySet().stream()
+ .filter(createResourcePredicate(matchMap))
+ .findAny()
+ .orElse(null);
+ }
+
+ public static Set findResourcesIds(Map stackResources, Map matchMap) {
+ return Optional.ofNullable(findResources(stackResources, matchMap))
+ .map(Map::keySet)
+ .orElse(null);
+ }
+
+ public static Map findResources(Map stackResources, Map matchMap) {
+ return stackResources.entrySet().stream()
+ .filter(createResourcePredicate(matchMap))
+ .collect(Collectors.toMap(
+ Entry::getKey,
+ Entry::getValue
+ ));
+ }
+
+ public static Set extractAsStringArray(Entry stackResource, String jsonPath) {
+ return Optional.ofNullable(stackResource)
+ .map(Entry::getValue)
+ .flatMap(
+ resource -> Optional.ofNullable(jsonPath)
+ .map(resource::at)
+ .map(JsonNode::elements)
+ .map(iterator -> {
+ var set = new HashSet();
+ iterator.forEachRemaining(item -> set.add(item.asText()));
+ return set;
+ })
+ ).orElseGet(HashSet::new);
+ }
+
+ public static Predicate> createResourcePredicate(String expectedJsonPath, String expectedJsonValue) {
+ return stackResourceEntry -> Optional.ofNullable(stackResourceEntry)
+ .map(Entry::getValue)
+ .flatMap(
+ resource -> Optional.ofNullable(expectedJsonPath)
+ .map(resource::at)
+ .map(JsonNode::asText)
+ ).filter(propertyValue -> propertyValue.equals(expectedJsonValue))
+ .isPresent();
+ }
+
+ public static Predicate> createResourcePredicate(Map matchMap) {
+ return Optional.ofNullable(matchMap)
+ .map(Map::entrySet)
+ .flatMap(
+ matchEntrySet -> matchEntrySet.stream()
+ .filter(Objects::nonNull)
+ .filter(matchEntry -> isNotBlank(matchEntry.getKey()))
+ .filter(matchEntry -> isNotBlank(matchEntry.getValue()))
+ .map(matchEntry -> createResourcePredicate(matchEntry.getKey(), matchEntry.getValue()))
+ .reduce(Predicate::and)
+ ).orElseGet(() -> _ -> false);
+ }
+}
From b3c8732fda0d28a2cbbc5bf6ffa402bba02eb3d1 Mon Sep 17 00:00:00 2001
From: sobrab <170367248+sobrab@users.noreply.github.com>
Date: Sun, 8 Dec 2024 12:20:24 +0200
Subject: [PATCH 2/4] some changes to make this example independent of
preexisting external APIs
---
java/http-proxy-apigateway/README.md | 16 +-
java/http-proxy-apigateway/pom.xml | 2 +-
.../com/myorg/HttpProxyApiGatewayApp.java | 22 +--
.../com/myorg/HttpProxyApiGatewayStack.java | 60 ++++++-
.../src/main/resources/lambdas/ByeFunction.py | 7 +
.../main/resources/lambdas/HelloFunction.py | 7 +
.../com/myorg/HttpProxyApiGatewayTest.java | 155 ++++++++++++++++--
7 files changed, 221 insertions(+), 48 deletions(-)
create mode 100644 java/http-proxy-apigateway/src/main/resources/lambdas/ByeFunction.py
create mode 100644 java/http-proxy-apigateway/src/main/resources/lambdas/HelloFunction.py
diff --git a/java/http-proxy-apigateway/README.md b/java/http-proxy-apigateway/README.md
index 5a16df6a95..d06a316ae8 100644
--- a/java/http-proxy-apigateway/README.md
+++ b/java/http-proxy-apigateway/README.md
@@ -12,14 +12,15 @@
This example creates an API Gateway with proxy resources for 2 HTTP backends.
-More HTTP backend APIs can be easily added.
This is useful for scenarios when incoming requests must be routed to one or more backend API hosts.
-An HTTP proxy integration enables direct interactions between clients and backends without any intervention from API Gateway after the API method is set up.
+An HTTP proxy integration enables direct interactions between clients and backends without any intervention from the API Gateway after the API method is set up.
> For more information on using HTTP proxy integrations with the APIGateway check out this [AWS tutorial](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-create-api-as-simple-proxy-for-http.html).
-> For demonstration purposes this CDK example deploys a solution that routes to the [PetsStore API](http://petstore-demo-endpoint.execute-api.com/) (from this [AWS tutorial](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-create-api-as-simple-proxy-for-http.html)) and to the [OpenTrivia API](https://opentdb.com).
-> If you prefer to use your own HTTP backend APIs modify the argument used to call the [`HttpProxyApiGatewayStack`](src/main/java/com/myorg/HttpProxyApiGatewayStack.java) constructor in the [`HttpProxyApiGatewayApp`](src/main/java/com/myorg/HttpProxyApiGatewayApp.java) class.
+> For demonstration purposes this CDK example deploys a solution that routes to a couple of test HTTP APIs.
+> The 2 test HTTP APIs are implemented using lambdas exposed through function URLs.
+> This example can be modified though, if you prefer to use your own HTTP backend APIs.
+> To do that you can modify the `createHTTPTestAPIs` method in the [`HttpProxyApiGatewayStack`](src/main/java/com/myorg/HttpProxyApiGatewayStack.java) class to return a list of `ProxyResourceParameters` corresponding to your own resources.
## Build
@@ -37,12 +38,13 @@ This will install the necessary CDK, then this example's dependencies, and then
Run `cdk deploy`.
This will deploy / redeploy the Stack to AWS.
-After the CDK deployment is successful, 2 URL examples will be available in the terminal console output:
+After the CDK deployment is successful, 2 URL examples will be available in the terminal console:
-- One for the `PetStoreProxyEndPointGetRequestExample` stack output
-- One for the `OpenTriviaProxyEndPointGetRequestExample` stack output
+- One for the `HttpProxyApiGatewayStack.HelloFunctionResourceExample` output
+- One for the `HttpProxyApiGatewayStack.ByeFunctionResourceExample` output
At this point, you can copy each of the 2 URLs and paste them in the address bar of a browser to invoke the 2 APIs.
+Also note that both URLs have the same host (the DNS of the new API Gateway created).
## Useful commands
diff --git a/java/http-proxy-apigateway/pom.xml b/java/http-proxy-apigateway/pom.xml
index 90e17e056b..ad9f96f840 100644
--- a/java/http-proxy-apigateway/pom.xml
+++ b/java/http-proxy-apigateway/pom.xml
@@ -9,7 +9,7 @@
UTF-8
- 2.167.1
+ 2.171.1
[10.0.0,11.0.0)
5.7.1
22
diff --git a/java/http-proxy-apigateway/src/main/java/com/myorg/HttpProxyApiGatewayApp.java b/java/http-proxy-apigateway/src/main/java/com/myorg/HttpProxyApiGatewayApp.java
index 0d9cbe6901..3c13b21c21 100644
--- a/java/http-proxy-apigateway/src/main/java/com/myorg/HttpProxyApiGatewayApp.java
+++ b/java/http-proxy-apigateway/src/main/java/com/myorg/HttpProxyApiGatewayApp.java
@@ -2,28 +2,14 @@
import software.amazon.awscdk.App;
import software.amazon.awscdk.StackProps;
-import software.amazon.awscdk.services.apigatewayv2.HttpMethod;
-
-import java.util.List;
-
-import static com.myorg.HttpProxyApiGatewayStack.*;
public class HttpProxyApiGatewayApp {
public static void main(final String[] args) {
var app = new App();
- new HttpProxyApiGatewayStack(
- app,
- "HttpProxyApiGatewayStack",
- StackProps.builder()
- .stackName("HttpProxyApiGatewayStack")
- .build(),
- List.of(
- // Replace with resources corresponding to your own HTTP backend APIs if you want to.
- // Add more resources if you need to.
- new ProxyResourceParameters("PetStore", "http://petstore-demo-endpoint.execute-api.com", HttpMethod.ANY.name(), "/petstore/pets?type=fish"),
- new ProxyResourceParameters("OpenTrivia", "https://opentdb.com", HttpMethod.ANY.name(), "/api.php?amount=10")
- )
- );
+ var stackProps = StackProps.builder()
+ .stackName("HttpProxyApiGatewayStack")
+ .build();
+ new HttpProxyApiGatewayStack(app, "HttpProxyApiGatewayStack", stackProps);
app.synth();
}
}
diff --git a/java/http-proxy-apigateway/src/main/java/com/myorg/HttpProxyApiGatewayStack.java b/java/http-proxy-apigateway/src/main/java/com/myorg/HttpProxyApiGatewayStack.java
index 986dc5d6bc..3597280eee 100644
--- a/java/http-proxy-apigateway/src/main/java/com/myorg/HttpProxyApiGatewayStack.java
+++ b/java/http-proxy-apigateway/src/main/java/com/myorg/HttpProxyApiGatewayStack.java
@@ -5,11 +5,19 @@
import software.amazon.awscdk.Stack;
import software.amazon.awscdk.StackProps;
import software.amazon.awscdk.services.apigateway.*;
+import software.amazon.awscdk.services.lambda.*;
import software.constructs.Construct;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import static software.amazon.awscdk.services.apigatewayv2.HttpMethod.ANY;
+import static software.amazon.awscdk.services.lambda.Runtime.PYTHON_3_12;
+
public class HttpProxyApiGatewayStack extends Stack {
public record ProxyResourceParameters(String resourceId, String baseUrl, String httpMethod, String exampleRequest) {
@@ -18,7 +26,7 @@ public record ProxyResourceParameters(String resourceId, String baseUrl, String
private final RestApi restApi;
- public HttpProxyApiGatewayStack(final Construct scope, final String id, final StackProps props, List proxyResourceParameters) {
+ public HttpProxyApiGatewayStack(final Construct scope, final String id, final StackProps props) {
super(scope, id, props);
restApi = RestApi.Builder.create(this, "RestApi")
.restApiName("RestApi")
@@ -26,7 +34,41 @@ public HttpProxyApiGatewayStack(final Construct scope, final String id, final St
.cloudWatchRoleRemovalPolicy(RemovalPolicy.DESTROY)
.endpointTypes(List.of(EndpointType.EDGE))
.build();
- proxyResourceParameters.forEach(this::createProxyResource);
+ createHTTPTestAPIs().forEach(this::createProxyResource);
+ }
+
+ private List createHTTPTestAPIs() {
+ return Map.of(
+ "HelloFunction", "/hello?from=AWS",
+ "ByeFunction", "/bye?from=AWS"
+ ).entrySet().stream()
+ .map(functionEntry -> {
+ var functionName = functionEntry.getKey();
+ var parameters = functionEntry.getValue();
+ var function = Function.Builder.create(this, functionName)
+ .functionName(functionName)
+ .code(InlineCode.fromInline(getInlineCode("src/main/resources/lambdas/" + functionName + ".py")))
+ .handler("index.handler")
+ .runtime(PYTHON_3_12)
+ .build();
+ var lambdaFunctionAlias = Alias.Builder.create(this, functionName + "ProdAlias")
+ .aliasName("Prod")
+ .version(function.getCurrentVersion())
+ .build();
+ var functionURL = FunctionUrl.Builder.create(this, functionName + "Url")
+ .function(lambdaFunctionAlias)
+ .authType(FunctionUrlAuthType.NONE)
+ .invokeMode(InvokeMode.BUFFERED)
+ .cors(
+ FunctionUrlCorsOptions.builder()
+ .allowedOrigins(List.of("*"))
+ .allowedMethods(List.of(HttpMethod.GET))
+ .allowedHeaders(List.of("*"))
+ .build()
+ )
+ .build();
+ return new ProxyResourceParameters(functionName + "Resource", functionURL.getUrl(), ANY.name(), parameters);
+ }).collect(ArrayList::new, List::add, List::addAll);
}
private void createProxyResource(ProxyResourceParameters proxyResourceParameters) {
@@ -54,14 +96,22 @@ private void createProxyResource(ProxyResourceParameters proxyResourceParameters
)
)
.build();
- var httpIntegration = new HttpIntegration(proxyResourceParameters.baseUrl + "/{proxy}", httpIntegrationProps);
+ var httpIntegration = new HttpIntegration(proxyResourceParameters.baseUrl + "{proxy}", httpIntegrationProps);
proxyResource.addMethod(proxyResourceParameters.httpMethod, httpIntegration, methodOptions);
var proxyResourceUrl = restApi.urlForPath(proxyResource.getPath());
- CfnOutput.Builder.create(this, proxyResourceParameters.resourceId + "ProxyEndPointAnyRequestTemplate")
+ CfnOutput.Builder.create(this, proxyResourceParameters.resourceId + "ProxyUrl")
.value(proxyResourceUrl)
.build();
- CfnOutput.Builder.create(this, proxyResourceParameters.resourceId + "ProxyEndPointGetRequestExample")
+ CfnOutput.Builder.create(this, proxyResourceParameters.resourceId + "Example")
.value(proxyResourceUrl.replace("/{proxy+}", proxyResourceParameters.exampleRequest))
.build();
}
+
+ private String getInlineCode(String path) {
+ try {
+ return new String(Files.readAllBytes(Path.of(path)));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
}
diff --git a/java/http-proxy-apigateway/src/main/resources/lambdas/ByeFunction.py b/java/http-proxy-apigateway/src/main/resources/lambdas/ByeFunction.py
new file mode 100644
index 0000000000..b75e729d22
--- /dev/null
+++ b/java/http-proxy-apigateway/src/main/resources/lambdas/ByeFunction.py
@@ -0,0 +1,7 @@
+import json
+def handler(event, context):
+ fromValue = event.get('queryStringParameters', {}).get('from', 'Lambda')
+ return {
+ 'statusCode': 200,
+ 'body': json.dumps('Bye from ' + fromValue + '!')
+ }
diff --git a/java/http-proxy-apigateway/src/main/resources/lambdas/HelloFunction.py b/java/http-proxy-apigateway/src/main/resources/lambdas/HelloFunction.py
new file mode 100644
index 0000000000..847668e69c
--- /dev/null
+++ b/java/http-proxy-apigateway/src/main/resources/lambdas/HelloFunction.py
@@ -0,0 +1,7 @@
+import json
+def handler(event, context):
+ fromValue = event.get('queryStringParameters', {}).get('from', 'Lambda')
+ return {
+ 'statusCode': 200,
+ 'body': json.dumps('Hello from ' + fromValue + '!')
+ }
diff --git a/java/http-proxy-apigateway/src/test/java/com/myorg/HttpProxyApiGatewayTest.java b/java/http-proxy-apigateway/src/test/java/com/myorg/HttpProxyApiGatewayTest.java
index 731d6da045..e1d06d774c 100644
--- a/java/http-proxy-apigateway/src/test/java/com/myorg/HttpProxyApiGatewayTest.java
+++ b/java/http-proxy-apigateway/src/test/java/com/myorg/HttpProxyApiGatewayTest.java
@@ -10,12 +10,10 @@
import software.amazon.awscdk.App;
import software.amazon.awscdk.StackProps;
import software.amazon.awscdk.cxapi.CloudFormationStackArtifact;
-import software.amazon.awscdk.services.apigatewayv2.HttpMethod;
import java.util.*;
import java.util.stream.Stream;
-import static com.myorg.HttpProxyApiGatewayStack.*;
import static com.myorg.TestUtils.*;
public class HttpProxyApiGatewayTest {
@@ -25,17 +23,10 @@ public class HttpProxyApiGatewayTest {
@BeforeAll
public static void setUp() {
var app = App.Builder.create().build();
- var httpProxyApiGatewayStack = new HttpProxyApiGatewayStack(
- app,
- "HttpProxyApiGatewayStack",
- StackProps.builder()
- .stackName("HttpProxyApiGatewayStack")
- .build(),
- List.of(
- new ProxyResourceParameters("PetStore", "http://petstore-demo-endpoint.execute-api.com", HttpMethod.ANY.name(), "/petstore/pets?type=fish"),
- new ProxyResourceParameters("OpenTrivia", "https://opentdb.com", HttpMethod.ANY.name(), "/api.php?amount=10")
- )
- );
+ var stackProps = StackProps.builder()
+ .stackName("HttpProxyApiGatewayStack")
+ .build();
+ var httpProxyApiGatewayStack = new HttpProxyApiGatewayStack(app, "HttpProxyApiGatewayStack", stackProps);
Optional.of(app)
.map(App::synth)
.flatMap(
@@ -74,7 +65,7 @@ public void testRestApi() {
@Test
@DisplayName("Test if the expected IAM role for the REST API is present in the resources of the stack.")
- public void testIamRole() {
+ public void testRestAPIIamRole() {
var iamRoleMatchMap = Map.of(
"/Type", "AWS::IAM::Role",
"/Properties/AssumeRolePolicyDocument/Statement/0/Action", "sts:AssumeRole",
@@ -98,7 +89,16 @@ public void testApiGatewayAccount() {
);
var restApiId = findResourceId(stackResourcesMap, restApiMatchMap);
var iamRoleMatchMap = Map.of(
- "/Type", "AWS::IAM::Role"
+ "/Type", "AWS::IAM::Role",
+ "/Properties/AssumeRolePolicyDocument/Statement/0/Action", "sts:AssumeRole",
+ "/Properties/AssumeRolePolicyDocument/Statement/0/Effect", "Allow",
+ "/Properties/AssumeRolePolicyDocument/Statement/0/Principal/Service", "apigateway.amazonaws.com",
+ "/Properties/AssumeRolePolicyDocument/Version", "2012-10-17",
+ "/Properties/ManagedPolicyArns/0/Fn::Join/0", "",
+ "/Properties/ManagedPolicyArns/0/Fn::Join/1/0", "arn:",
+ "/Properties/ManagedPolicyArns/0/Fn::Join/1/1/Ref", "AWS::Partition",
+ "/Properties/ManagedPolicyArns/0/Fn::Join/1/2", ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs",
+ "/DeletionPolicy", "Delete"
);
var iamRoleId = findResourceId(stackResourcesMap, iamRoleMatchMap);
var apiGatewayAccountMatchMap = Map.of(
@@ -199,13 +199,134 @@ public void testApiGatewayMethods() {
"/Properties/RequestParameters/method.request.path.proxy", "true",
"/Properties/RestApiId/Ref", restApiId
);
- Long expectedPermissionsCount = 2L;
+ Long expectedApiGatewayMethodsCount = 2L;
Assertions.assertTrue(
Optional.ofNullable(findResources(stackResourcesMap, apiGatewayMethodsMatchMap))
.map(Map::entrySet)
.map(Set::stream)
.map(Stream::count)
- .filter(methodsCont -> methodsCont.equals(expectedPermissionsCount))
+ .filter(methodsCont -> methodsCont.equals(expectedApiGatewayMethodsCount))
+ .isPresent()
+ );
+ }
+
+ @Test
+ @DisplayName("Test if the expected IAM roles for the test lambdas are present in the resources of the stack.")
+ public void testLambdaFunctionsIAMRoles() {
+ var lambdaFunctionsIAMRolesMatchMap = Map.of(
+ "/Type", "AWS::IAM::Role",
+ "/Properties/AssumeRolePolicyDocument/Statement/0/Action", "sts:AssumeRole",
+ "/Properties/AssumeRolePolicyDocument/Statement/0/Effect", "Allow",
+ "/Properties/AssumeRolePolicyDocument/Statement/0/Principal/Service", "lambda.amazonaws.com",
+ "/Properties/AssumeRolePolicyDocument/Version", "2012-10-17",
+ "/Properties/ManagedPolicyArns/0/Fn::Join/0", "",
+ "/Properties/ManagedPolicyArns/0/Fn::Join/1/0", "arn:",
+ "/Properties/ManagedPolicyArns/0/Fn::Join/1/1/Ref", "AWS::Partition",
+ "/Properties/ManagedPolicyArns/0/Fn::Join/1/2", ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
+ );
+ Long expectedLambdaFunctionsIAMRolesCount = 2L;
+ Assertions.assertTrue(
+ Optional.ofNullable(findResources(stackResourcesMap, lambdaFunctionsIAMRolesMatchMap))
+ .map(Map::entrySet)
+ .map(Set::stream)
+ .map(Stream::count)
+ .filter(methodsCont -> methodsCont.equals(expectedLambdaFunctionsIAMRolesCount))
+ .isPresent()
+ );
+ }
+
+ @Test
+ @DisplayName("Test if the expected lambda functions are present in the resources of the stack.")
+ public void testLambdaFunctions() {
+ var lambdaFunctionsMatchMap = Map.of(
+ "/Type", "AWS::Lambda::Function",
+ "/Properties/Runtime", "python3.12"
+ );
+ Long expectedLambdaFunctionsCount = 2L;
+ Assertions.assertTrue(
+ Optional.ofNullable(findResources(stackResourcesMap, lambdaFunctionsMatchMap))
+ .map(Map::entrySet)
+ .map(Set::stream)
+ .map(Stream::count)
+ .filter(methodsCont -> methodsCont.equals(expectedLambdaFunctionsCount))
+ .isPresent()
+ );
+ }
+
+ @Test
+ @DisplayName("Test if the expected lambda versions are present in the resources of the stack.")
+ public void testLambdaVersions() {
+ var lambdaVersionsMatchMap = Map.of(
+ "/Type", "AWS::Lambda::Version"
+ );
+ Long expectedLambdaVersionsCount = 2L;
+ Assertions.assertTrue(
+ Optional.ofNullable(findResources(stackResourcesMap, lambdaVersionsMatchMap))
+ .map(Map::entrySet)
+ .map(Set::stream)
+ .map(Stream::count)
+ .filter(methodsCont -> methodsCont.equals(expectedLambdaVersionsCount))
+ .isPresent()
+ );
+ }
+
+ @Test
+ @DisplayName("Test if the expected lambda aliases are present in the resources of the stack.")
+ public void testLambdaAliases() {
+ var lambdaAliasesMatchMap = Map.of(
+ "/Type", "AWS::Lambda::Alias",
+ "/Properties/Name", "Prod"
+ );
+ Long expectedLambdaAliasesCount = 2L;
+ Assertions.assertTrue(
+ Optional.ofNullable(findResources(stackResourcesMap, lambdaAliasesMatchMap))
+ .map(Map::entrySet)
+ .map(Set::stream)
+ .map(Stream::count)
+ .filter(methodsCont -> methodsCont.equals(expectedLambdaAliasesCount))
+ .isPresent()
+ );
+ }
+
+ @Test
+ @DisplayName("Test if the expected lambda permissions are present in the resources of the stack.")
+ public void testLambdaPermissions() {
+ var lambdaPermissionsMatchMap = Map.of(
+ "/Type", "AWS::Lambda::Permission",
+ "/Properties/Action", "lambda:InvokeFunctionUrl",
+ "/Properties/FunctionUrlAuthType", "NONE",
+ "/Properties/Principal", "*"
+ );
+ Long expectedLambdaPermissionsCount = 2L;
+ Assertions.assertTrue(
+ Optional.ofNullable(findResources(stackResourcesMap, lambdaPermissionsMatchMap))
+ .map(Map::entrySet)
+ .map(Set::stream)
+ .map(Stream::count)
+ .filter(methodsCont -> methodsCont.equals(expectedLambdaPermissionsCount))
+ .isPresent()
+ );
+ }
+
+ @Test
+ @DisplayName("Test if the expected lambda function URLs are present in the resources of the stack.")
+ public void testLambdaFunctionURLs() {
+ var lambdaFunctionsURLsMatchMap = Map.of(
+ "/Type", "AWS::Lambda::Url",
+ "/Properties/AuthType", "NONE",
+ "/Properties/InvokeMode", "BUFFERED",
+ "/Properties/Qualifier", "Prod",
+ "/Properties/Cors/AllowHeaders/0", "*",
+ "/Properties/Cors/AllowMethods/0", "GET",
+ "/Properties/Cors/AllowOrigins/0", "*"
+ );
+ Long expectedLambdaFunctionsURLsCount = 2L;
+ Assertions.assertTrue(
+ Optional.ofNullable(findResources(stackResourcesMap, lambdaFunctionsURLsMatchMap))
+ .map(Map::entrySet)
+ .map(Set::stream)
+ .map(Stream::count)
+ .filter(methodsCont -> methodsCont.equals(expectedLambdaFunctionsURLsCount))
.isPresent()
);
}
From ac986ea142b39ca1aeb614207bfb8a5569057759 Mon Sep 17 00:00:00 2001
From: Michael Kaiser
Date: Fri, 27 Dec 2024 14:18:59 -0600
Subject: [PATCH 3/4] Update pom.xml
Supported release version
---
java/http-proxy-apigateway/pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/java/http-proxy-apigateway/pom.xml b/java/http-proxy-apigateway/pom.xml
index ad9f96f840..ade7ee1e74 100644
--- a/java/http-proxy-apigateway/pom.xml
+++ b/java/http-proxy-apigateway/pom.xml
@@ -23,7 +23,7 @@
maven-compiler-plugin
3.11.0
- 22
+ 17
From fa090a613dc884c64e0bee49e6c4742d81386299 Mon Sep 17 00:00:00 2001
From: Michael Kaiser
Date: Fri, 27 Dec 2024 14:24:15 -0600
Subject: [PATCH 4/4] Update TestUtils.java
Fix _ not being allowed
---
.../src/test/java/com/myorg/TestUtils.java | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/java/http-proxy-apigateway/src/test/java/com/myorg/TestUtils.java b/java/http-proxy-apigateway/src/test/java/com/myorg/TestUtils.java
index 4dd130c267..dc945f3f79 100644
--- a/java/http-proxy-apigateway/src/test/java/com/myorg/TestUtils.java
+++ b/java/http-proxy-apigateway/src/test/java/com/myorg/TestUtils.java
@@ -75,6 +75,7 @@ public static Predicate> createResourcePredicate(Map isNotBlank(matchEntry.getValue()))
.map(matchEntry -> createResourcePredicate(matchEntry.getKey(), matchEntry.getValue()))
.reduce(Predicate::and)
- ).orElseGet(() -> _ -> false);
- }
+ ).orElseGet(() -> entry -> false);
+}
+
}