Skip to content

Commit 0461f45

Browse files
committed
Additional CloudFormation listener to capture resources from the
tenant-onboarding-app.yaml stack that is run a variable number of times based on what application services have been configured
1 parent 5b68be4 commit 0461f45

File tree

9 files changed

+579
-36
lines changed

9 files changed

+579
-36
lines changed
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
4+
5+
Licensed under the Apache License, Version 2.0 (the "License").
6+
You may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
-->
17+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
18+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
19+
<modelVersion>4.0.0</modelVersion>
20+
<parent>
21+
<groupId>com.amazon.aws.partners.saasfactory.saasboost</groupId>
22+
<artifactId>saasboost-functions</artifactId>
23+
<version>1.0.0</version>
24+
</parent>
25+
<artifactId>OnboardingAppStackListener</artifactId>
26+
<version>1.0.0</version>
27+
<packaging>jar</packaging>
28+
<licenses>
29+
<license>
30+
<name>Apache-2.0</name>
31+
<url>http://www.apache.org/licenses/LICENSE-2.0</url>
32+
</license>
33+
</licenses>
34+
35+
<properties>
36+
<checkstyle.maxAllowedViolations>50</checkstyle.maxAllowedViolations>
37+
</properties>
38+
39+
<build>
40+
<finalName>${project.artifactId}</finalName>
41+
<plugins>
42+
<plugin>
43+
<groupId>org.apache.maven.plugins</groupId>
44+
<artifactId>maven-compiler-plugin</artifactId>
45+
</plugin>
46+
<plugin>
47+
<groupId>org.apache.maven.plugins</groupId>
48+
<artifactId>maven-surefire-plugin</artifactId>
49+
<configuration>
50+
<environmentVariables>
51+
<AWS_REGION>us-east-1</AWS_REGION>
52+
<SAAS_BOOST_ENV>test</SAAS_BOOST_ENV>
53+
</environmentVariables>
54+
</configuration>
55+
</plugin>
56+
<plugin>
57+
<groupId>org.apache.maven.plugins</groupId>
58+
<artifactId>maven-assembly-plugin</artifactId>
59+
</plugin>
60+
<plugin>
61+
<groupId>pl.project13.maven</groupId>
62+
<artifactId>git-commit-id-plugin</artifactId>
63+
<version>4.0.0</version>
64+
<executions>
65+
<execution>
66+
<id>get-the-git-infos</id>
67+
<goals>
68+
<goal>revision</goal>
69+
</goals>
70+
<phase>initialize</phase>
71+
</execution>
72+
</executions>
73+
<configuration>
74+
<generateGitPropertiesFile>true</generateGitPropertiesFile>
75+
<generateGitPropertiesFilename>${project.build.outputDirectory}/git.properties</generateGitPropertiesFilename>
76+
<includeOnlyProperties>
77+
<includeOnlyProperty>^git.commit.id.describe</includeOnlyProperty>
78+
<includeOnlyProperty>^git.commit.id.describe-short</includeOnlyProperty>
79+
<includeOnlyProperty>^git.commit.time</includeOnlyProperty>
80+
<includeOnlyProperty>^git.closest.tag.name</includeOnlyProperty>
81+
</includeOnlyProperties>
82+
<commitIdGenerationMode>full</commitIdGenerationMode>
83+
<dotGitDirectory>../../.git</dotGitDirectory>
84+
<failOnNoGitDirectory>false</failOnNoGitDirectory>
85+
</configuration>
86+
</plugin>
87+
</plugins>
88+
</build>
89+
90+
<dependencies>
91+
<dependency>
92+
<groupId>junit</groupId>
93+
<artifactId>junit</artifactId>
94+
</dependency>
95+
<dependency>
96+
<groupId>org.slf4j</groupId>
97+
<artifactId>slf4j-nop</artifactId>
98+
</dependency>
99+
<dependency>
100+
<groupId>com.amazon.aws.partners.saasfactory.saasboost</groupId>
101+
<artifactId>Utils</artifactId>
102+
<version>1.0.0</version>
103+
<!-- Don't bundle our layer so we get the shared one at runtime -->
104+
<scope>provided</scope>
105+
</dependency>
106+
<dependency>
107+
<groupId>com.amazon.aws.partners.saasfactory.saasboost</groupId>
108+
<artifactId>CloudFormationUtils</artifactId>
109+
<version>1.0.0</version>
110+
<!-- Don't bundle our layer so we get the shared one at runtime -->
111+
<scope>provided</scope>
112+
</dependency>
113+
<dependency>
114+
<groupId>software.amazon.awssdk</groupId>
115+
<artifactId>cloudformation</artifactId>
116+
<version>${aws.java.sdk.version}</version>
117+
<exclusions>
118+
<exclusion>
119+
<groupId>software.amazon.awssdk</groupId>
120+
<artifactId>netty-nio-client</artifactId>
121+
</exclusion>
122+
<exclusion>
123+
<groupId>software.amazon.awssdk</groupId>
124+
<artifactId>apache-client</artifactId>
125+
</exclusion>
126+
</exclusions>
127+
</dependency>
128+
<dependency>
129+
<groupId>software.amazon.awssdk</groupId>
130+
<artifactId>eventbridge</artifactId>
131+
<version>${aws.java.sdk.version}</version>
132+
<exclusions>
133+
<exclusion>
134+
<groupId>software.amazon.awssdk</groupId>
135+
<artifactId>netty-nio-client</artifactId>
136+
</exclusion>
137+
<exclusion>
138+
<groupId>software.amazon.awssdk</groupId>
139+
<artifactId>apache-client</artifactId>
140+
</exclusion>
141+
</exclusions>
142+
</dependency>
143+
</dependencies>
144+
145+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.amazon.aws.partners.saasfactory.saasboost;
18+
19+
import com.amazonaws.services.lambda.runtime.Context;
20+
import com.amazonaws.services.lambda.runtime.RequestHandler;
21+
import com.amazonaws.services.lambda.runtime.events.SNSEvent;
22+
import org.slf4j.Logger;
23+
import org.slf4j.LoggerFactory;
24+
import software.amazon.awssdk.core.exception.SdkServiceException;
25+
import software.amazon.awssdk.services.cloudformation.CloudFormationClient;
26+
import software.amazon.awssdk.services.cloudformation.model.*;
27+
import software.amazon.awssdk.services.cloudformation.model.Stack;
28+
import software.amazon.awssdk.services.eventbridge.EventBridgeClient;
29+
30+
import java.util.*;
31+
import java.util.regex.Pattern;
32+
33+
public class OnboardingAppStackListener implements RequestHandler<SNSEvent, Object> {
34+
35+
private static final Logger LOGGER = LoggerFactory.getLogger(OnboardingAppStackListener.class);
36+
private static final String AWS_REGION = System.getenv("AWS_REGION");
37+
private static final String SAAS_BOOST_ENV = System.getenv("SAAS_BOOST_ENV");
38+
private static final String SAAS_BOOST_EVENT_BUS = System.getenv("SAAS_BOOST_EVENT_BUS");
39+
private static final String UPDATE_TENANT_RESOURCES = "Tenant Update Resources";
40+
private static final String EVENT_SOURCE = "saas-boost";
41+
private static final Pattern STACK_NAME_PATTERN = Pattern
42+
.compile("^sb-" + SAAS_BOOST_ENV + "-tenant-[a-z0-9]{8}-app-.+-.+$");
43+
private static final Collection<String> EVENTS_OF_INTEREST = Collections.unmodifiableCollection(
44+
Arrays.asList("CREATE_COMPLETE", "UPDATE_COMPLETE"));
45+
private final CloudFormationClient cfn;
46+
private final EventBridgeClient eventBridge;
47+
48+
public OnboardingAppStackListener() {
49+
final long startTimeMillis = System.currentTimeMillis();
50+
if (Utils.isBlank(AWS_REGION)) {
51+
throw new IllegalStateException("Missing required environment variable AWS_REGION");
52+
}
53+
if (Utils.isBlank(SAAS_BOOST_EVENT_BUS)) {
54+
throw new IllegalStateException("Missing required environment variable SAAS_BOOST_EVENT_BUS");
55+
}
56+
this.cfn = Utils.sdkClient(CloudFormationClient.builder(), CloudFormationClient.SERVICE_NAME);
57+
this.eventBridge = Utils.sdkClient(EventBridgeClient.builder(), EventBridgeClient.SERVICE_NAME);
58+
LOGGER.info("Constructor init: {}", System.currentTimeMillis() - startTimeMillis);
59+
}
60+
61+
@Override
62+
public Object handleRequest(SNSEvent event, Context context) {
63+
LOGGER.info(Utils.toJson(event));
64+
65+
List<SNSEvent.SNSRecord> records = event.getRecords();
66+
SNSEvent.SNS sns = records.get(0).getSNS();
67+
String message = sns.getMessage();
68+
69+
CloudFormationEvent cloudFormationEvent = CloudFormationEventDeserializer.deserialize(message);
70+
71+
// CloudFormation sends SNS notifications for every resource in a stack going through each status change.
72+
// We want to process the resources of the tenant-onboarding-app.yaml CloudFormation stack only after the
73+
// stack has finished being created or updated so we don't trigger anything downstream prematurely.
74+
if (filter(cloudFormationEvent)) {
75+
String stackName = cloudFormationEvent.getStackName();
76+
String stackStatus = cloudFormationEvent.getResourceStatus();
77+
LOGGER.info("Stack " + stackName + " is in status " + stackStatus);
78+
79+
// We need to get the tenant and the application service this stack was run for
80+
String tenantId = null;
81+
String serviceName = null;
82+
try {
83+
DescribeStacksResponse stacks = cfn.describeStacks(req -> req
84+
.stackName(cloudFormationEvent.getStackId())
85+
);
86+
Stack stack = stacks.stacks().get(0);
87+
for (Parameter parameter : stack.parameters()) {
88+
if ("TenantId".equals(parameter.parameterKey())) {
89+
tenantId = parameter.parameterValue();
90+
}
91+
if ("ServiceName".equals(parameter.parameterKey())) {
92+
serviceName = parameter.parameterValue();
93+
}
94+
}
95+
} catch (SdkServiceException cfnError) {
96+
LOGGER.error("cfn:DescribeStacks error", cfnError);
97+
LOGGER.error(Utils.getFullStackTrace(cfnError));
98+
throw cfnError;
99+
}
100+
101+
// We use these to build the ARN of the resources we're interested in if we don't
102+
// get the ARN straight from the CloudFormation physical resource id
103+
final String[] lambdaArn = context.getInvokedFunctionArn().split(":");
104+
final String partition = lambdaArn[1];
105+
final String accountId = lambdaArn[4];
106+
107+
// We're looking for CodePipeline repository resources in a CREATE_COMPLETE state. There could be
108+
// multiple pipelines provisioned depending on how the application services are configured.
109+
try {
110+
ListStackResourcesResponse resources = cfn.listStackResources(req -> req
111+
.stackName(cloudFormationEvent.getStackId())
112+
);
113+
for (StackResourceSummary resource : resources.stackResourceSummaries()) {
114+
// LOGGER.debug("Processing resource {} {} {} {}", resource.resourceType(),
115+
// resource.resourceStatusAsString(), resource.logicalResourceId(),
116+
// resource.physicalResourceId());
117+
if ("CREATE_COMPLETE".equals(resource.resourceStatusAsString())) {
118+
if ("AWS::CodePipeline::Pipeline".equals(resource.resourceType())) {
119+
String codePipeline = resource.physicalResourceId();
120+
// The resources collection on the tenant object is Map<String, Resource>
121+
// so we need a unique key per service code pipeline. We'll prefix the
122+
// key with SERVICE_ and suffix it with _CODE_PIPELINE so we can find
123+
// all of the tenant's code pipelines later on by looking for that pattern.
124+
String key = serviceNameResourceKey(serviceName, AwsResource.CODE_PIPELINE.name());
125+
LOGGER.info("Publishing update tenant resources event for tenant {} {} {}", tenantId,
126+
key, codePipeline);
127+
128+
Map<String, Object> tenantResource = new HashMap<>();
129+
tenantResource.put(key, Map.of(
130+
"name", codePipeline,
131+
"arn", AwsResource.CODE_PIPELINE.formatArn(partition, AWS_REGION, accountId,
132+
codePipeline),
133+
"consoleUrl", AwsResource.CODE_PIPELINE.formatUrl(AWS_REGION, codePipeline)
134+
));
135+
136+
// The update tenant resources API call is additive, so we don't need to pull the
137+
// current tenant object ourselves.
138+
Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, EVENT_SOURCE,
139+
UPDATE_TENANT_RESOURCES,
140+
Map.of("tenantId", tenantId, "resources", Utils.toJson(tenantResource))
141+
);
142+
}
143+
}
144+
}
145+
} catch (SdkServiceException cfnError) {
146+
LOGGER.error("cfn:ListStackResources error", cfnError);
147+
LOGGER.error(Utils.getFullStackTrace(cfnError));
148+
throw cfnError;
149+
}
150+
}
151+
return null;
152+
}
153+
154+
protected static String serviceNameResourceKey(String serviceName, String resourceType) {
155+
if (Utils.isBlank(serviceName)) {
156+
throw new IllegalArgumentException("Service name must not be blank");
157+
}
158+
if (Utils.isBlank(resourceType)) {
159+
throw new IllegalArgumentException("Resource type must not be blank");
160+
}
161+
return "SERVICE_"
162+
+ serviceName.toUpperCase().replaceAll("\\s+", "_")
163+
+ "_"
164+
+ resourceType;
165+
}
166+
167+
protected static boolean filter(CloudFormationEvent cloudFormationEvent) {
168+
return ("AWS::CloudFormation::Stack".equals(cloudFormationEvent.getResourceType())
169+
&& STACK_NAME_PATTERN.matcher(cloudFormationEvent.getStackName()).matches()
170+
&& EVENTS_OF_INTEREST.contains(cloudFormationEvent.getResourceStatus()));
171+
}
172+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
4+
5+
Licensed under the Apache License, Version 2.0 (the "License").
6+
You may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
-->
17+
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
18+
xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.0.0 http://maven.apache.org/xsd/assembly-2.0.0.xsd">
19+
<id>lambda</id>
20+
<formats>
21+
<format>zip</format>
22+
</formats>
23+
<includeBaseDirectory>false</includeBaseDirectory>
24+
<fileSets>
25+
<fileSet>
26+
<outputDirectory></outputDirectory>
27+
<directory>${project.build.outputDirectory}</directory>
28+
<includes>
29+
<include>com/amazon/aws/partners/saasfactory/**</include>
30+
<include>log4j2.xml</include>
31+
<include>git.properties</include>
32+
</includes>
33+
</fileSet>
34+
</fileSets>
35+
<dependencySets>
36+
<dependencySet>
37+
<useProjectArtifact>false</useProjectArtifact>
38+
<useTransitiveDependencies>true</useTransitiveDependencies>
39+
<outputDirectory>lib</outputDirectory>
40+
</dependencySet>
41+
</dependencySets>
42+
</assembly>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
4+
5+
Licensed under the Apache License, Version 2.0 (the "License").
6+
You may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
-->
17+
<Configuration status="WARN">
18+
<Appenders>
19+
<Lambda name="Lambda">
20+
<PatternLayout>
21+
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %X{AWSRequestId} %-5p %C{1} - %m%n</pattern>
22+
</PatternLayout>
23+
</Lambda>
24+
</Appenders>
25+
<Loggers>
26+
<Root level="INFO">
27+
<AppenderRef ref="Lambda"/>
28+
</Root>
29+
<Logger name="software.amazon.awssdk" level="WARN"/>
30+
<Logger name="software.amazon.awssdk.request" level="INFO"/>
31+
<Logger name="com.amazon.aws.partners.saasfactory" level="DEBUG"/>
32+
</Loggers>
33+
</Configuration>

0 commit comments

Comments
 (0)