Skip to content

Commit c15cf13

Browse files
committed
New Lambda layer for CloudFormation utility classes used by SNS stack
notification subscribers
1 parent d262ceb commit c15cf13

File tree

12 files changed

+414
-56
lines changed

12 files changed

+414
-56
lines changed

functions/core-stack-listener/pom.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,13 @@ limitations under the License.
9797
<!-- Don't bundle our layer so we get the shared one at runtime -->
9898
<scope>provided</scope>
9999
</dependency>
100+
<dependency>
101+
<groupId>com.amazon.aws.partners.saasfactory.saasboost</groupId>
102+
<artifactId>CloudFormationUtils</artifactId>
103+
<version>1.0.0</version>
104+
<!-- Don't bundle our layer so we get the shared one at runtime -->
105+
<scope>provided</scope>
106+
</dependency>
100107
<dependency>
101108
<groupId>software.amazon.awssdk</groupId>
102109
<artifactId>cloudformation</artifactId>

functions/core-stack-listener/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CoreStackListener.java

Lines changed: 25 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,9 @@
2424
import software.amazon.awssdk.core.exception.SdkServiceException;
2525
import software.amazon.awssdk.services.cloudformation.CloudFormationClient;
2626
import software.amazon.awssdk.services.cloudformation.model.*;
27-
import software.amazon.awssdk.services.cloudformation.model.Stack;
2827
import software.amazon.awssdk.services.eventbridge.EventBridgeClient;
29-
import software.amazon.awssdk.services.eventbridge.model.PutEventsRequestEntry;
30-
import software.amazon.awssdk.services.eventbridge.model.PutEventsResponse;
31-
import software.amazon.awssdk.services.eventbridge.model.PutEventsResultEntry;
3228

3329
import java.util.*;
34-
import java.util.regex.Matcher;
3530

3631
public class CoreStackListener implements RequestHandler<SNSEvent, Object> {
3732

@@ -41,6 +36,8 @@ public class CoreStackListener implements RequestHandler<SNSEvent, Object> {
4136
private static final String SAAS_BOOST_EVENT_BUS = System.getenv("SAAS_BOOST_EVENT_BUS");
4237
private static final String SYSTEM_API_CALL = "System API Call";
4338
private static final String EVENT_SOURCE = "saas-boost";
39+
private static final Collection<String> EVENTS_OF_INTEREST = Collections.unmodifiableCollection(
40+
Arrays.asList("CREATE_COMPLETE", "UPDATE_COMPLETE"));
4441
private final CloudFormationClient cfn;
4542
private final EventBridgeClient eventBridge;
4643

@@ -67,39 +64,35 @@ public Object handleRequest(SNSEvent event, Context context) {
6764

6865
CloudFormationEvent cloudFormationEvent = CloudFormationEventDeserializer.deserialize(message);
6966

70-
String type = cloudFormationEvent.getResourceType();
71-
String stackId = cloudFormationEvent.getStackId();
72-
String stackName = cloudFormationEvent.getStackName();
73-
String stackStatus = cloudFormationEvent.getResourceStatus();
74-
7567
// CloudFormation sends SNS notifications for every resource in a stack going through each status change.
76-
// We're only interested in the stack complete event.
77-
List<String> eventsOfInterest = Arrays.asList("CREATE_COMPLETE", "UPDATE_COMPLETE");
78-
if ("AWS::CloudFormation::Stack".equals(type) && stackName.startsWith("sb-" + SAAS_BOOST_ENV + "-core-")
79-
&& eventsOfInterest.contains(stackStatus)) {
80-
LOGGER.info(Utils.toJson(event));
68+
// We want to process the resources of the saas-boost-core.yaml CloudFormation stack only after the stack
69+
// has finished being created or updated so we don't trigger anything downstream prematurely.
70+
if (filter(cloudFormationEvent)) {
71+
String stackName = cloudFormationEvent.getStackName();
72+
String stackStatus = cloudFormationEvent.getResourceStatus();
8173
LOGGER.info("Stack " + stackName + " is in status " + stackStatus);
82-
final String stackIdName = stackId;
8374
try {
84-
ListStackResourcesResponse resources = cfn.listStackResources(req -> req.stackName(stackIdName));
75+
ListStackResourcesResponse resources = cfn.listStackResources(req -> req
76+
.stackName(cloudFormationEvent.getStackId())
77+
);
78+
// We're looking for ECR repository resources in a CREATE_COMPLETE state. There could be multiple
79+
// ECR repos provisioned depending on how the application services are configured.
8580
for (StackResourceSummary resource : resources.stackResourceSummaries()) {
86-
String resourceType = resource.resourceType();
87-
String ecrRepo = resource.physicalResourceId();
88-
String resourceStatus = resource.resourceStatusAsString();
89-
String serviceName = resource.logicalResourceId();
90-
LOGGER.info("Processing resource {} {} {} {}", resourceType, resourceStatus, serviceName,
91-
ecrRepo);
92-
if ("CREATE_COMPLETE".equals(resourceStatus)) {
93-
if ("AWS::ECR::Repository".equals(resourceType)) {
81+
// LOGGER.debug("Processing resource {} {} {} {}", resource.resourceType(),
82+
// resource.resourceStatusAsString(), resource.logicalResourceId(),
83+
// resource.physicalResourceId());
84+
if ("CREATE_COMPLETE".equals(resource.resourceStatusAsString())) {
85+
if ("AWS::ECR::Repository".equals(resource.resourceType())) {
86+
String ecrRepo = resource.physicalResourceId();
87+
String serviceName = resource.logicalResourceId();
9488
LOGGER.info("Publishing appConfig update event for ECR repository {} {}", serviceName,
9589
ecrRepo);
96-
// Logical ID is the service name
97-
// Physical ID is the repo name
9890
Map<String, Object> systemApiRequest = new HashMap<>();
9991
systemApiRequest.put("resource", "settings/config/" + serviceName + "/ECR_REPO");
10092
systemApiRequest.put("method", "PUT");
10193
systemApiRequest.put("body", Utils.toJson(Map.of("value", ecrRepo)));
102-
publishEvent(systemApiRequest, SYSTEM_API_CALL);
94+
Utils.publishEvent(eventBridge, SAAS_BOOST_EVENT_BUS, EVENT_SOURCE, SYSTEM_API_CALL,
95+
systemApiRequest);
10396
}
10497
}
10598
}
@@ -108,38 +101,14 @@ public Object handleRequest(SNSEvent event, Context context) {
108101
LOGGER.error(Utils.getFullStackTrace(cfnError));
109102
throw cfnError;
110103
}
111-
} else {
112-
//LOGGER.info("Skipping CloudFormation notification {} {} {} {}", stackId, type, stackName, stackStatus);
113104
}
114105
return null;
115106
}
116107

117-
protected boolean snsMessageFilter(String message) {
118-
return true;
108+
protected static boolean filter(CloudFormationEvent cloudFormationEvent) {
109+
return ("AWS::CloudFormation::Stack".equals(cloudFormationEvent.getResourceType())
110+
&& cloudFormationEvent.getStackName().startsWith("sb-" + SAAS_BOOST_ENV + "-core-")
111+
&& EVENTS_OF_INTEREST.contains(cloudFormationEvent.getResourceStatus()));
119112
}
120113

121-
protected void publishEvent(Map<String, Object> eventBridgeDetail, String detailType) {
122-
try {
123-
PutEventsRequestEntry systemEvent = PutEventsRequestEntry.builder()
124-
.eventBusName(SAAS_BOOST_EVENT_BUS)
125-
.detailType(detailType)
126-
.source(EVENT_SOURCE)
127-
.detail(Utils.toJson(eventBridgeDetail))
128-
.build();
129-
PutEventsResponse eventBridgeResponse = eventBridge.putEvents(r -> r
130-
.entries(systemEvent)
131-
);
132-
for (PutEventsResultEntry entry : eventBridgeResponse.entries()) {
133-
if (entry.eventId() != null && !entry.eventId().isEmpty()) {
134-
LOGGER.info("Put event success {} {}", entry.toString(), systemEvent.toString());
135-
} else {
136-
LOGGER.error("Put event failed {}", entry.toString());
137-
}
138-
}
139-
} catch (SdkServiceException eventBridgeError) {
140-
LOGGER.error("events::PutEvents", eventBridgeError);
141-
LOGGER.error(Utils.getFullStackTrace(eventBridgeError));
142-
throw eventBridgeError;
143-
}
144-
}
145114
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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-layers</artifactId>
23+
<version>1.0.0</version>
24+
</parent>
25+
<artifactId>CloudFormationUtils</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>7</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+
</plugin>
50+
<plugin>
51+
<groupId>org.apache.maven.plugins</groupId>
52+
<artifactId>maven-assembly-plugin</artifactId>
53+
</plugin>
54+
</plugins>
55+
</build>
56+
57+
<dependencies>
58+
<dependency>
59+
<groupId>junit</groupId>
60+
<artifactId>junit</artifactId>
61+
</dependency>
62+
<dependency>
63+
<groupId>org.slf4j</groupId>
64+
<artifactId>slf4j-nop</artifactId>
65+
</dependency>
66+
<dependency>
67+
<groupId>com.amazon.aws.partners.saasfactory.saasboost</groupId>
68+
<artifactId>Utils</artifactId>
69+
<version>1.0.0</version>
70+
<!-- Don't bundle our layer so we get the shared one at runtime -->
71+
<scope>provided</scope>
72+
</dependency>
73+
<dependency>
74+
<groupId>com.amazonaws</groupId>
75+
<artifactId>aws-lambda-java-core</artifactId>
76+
</dependency>
77+
</dependencies>
78+
</project>
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package com.amazon.aws.partners.saasfactory.saasboost;
2+
3+
import org.slf4j.Logger;
4+
import org.slf4j.LoggerFactory;
5+
6+
import java.util.IllegalFormatException;
7+
8+
public enum AwsResource {
9+
10+
RDS_CLUSTER("https://%s.console.aws.amazon.com/rds/home#database:id=%s;is-cluster=true",
11+
"",
12+
"AWS::RDS::DBCluster", false),
13+
RDS_INSTANCE("https://%s.console.aws.amazon.com/rds/home?region=%s#dbinstance:id=%s",
14+
"",
15+
"AWS::RDS::DBInstance", true),
16+
ECS_CLUSTER("https://%s.console.aws.amazon.com/ecs/home#/clusters/%s",
17+
"arn:%s:ecs:%s:%s:cluster/%s",
18+
"AWS::ECS::Cluster", false),
19+
LOG_GROUP("https://%s.console.aws.amazon.com/cloudwatch/home?region=%s#logsV2:log-groups/log-group/%s",
20+
"",
21+
"AWS::Logs::LogGroup", true),
22+
VPC("https://%s.console.aws.amazon.com/vpc/home?region=%s#vpcs:search=%s",
23+
"arn:%s:ec2:%s:%s:vpc/%s",
24+
"AWS::EC2::VPC", true),
25+
PRIVATE_SUBNET_A("https://%s.console.aws.amazon.com/vpc/home?region=%s#SubnetDetails:subnetId=%s",
26+
"arn:%s:ec2:%s:%s:subnet/%s",
27+
"AWS::EC2::Subnet", true),
28+
PRIVATE_SUBNET_B("https://%s.console.aws.amazon.com/vpc/home?region=%s#SubnetDetails:subnetId=%s",
29+
"arn:%s:ec2:%s:%s:subnet/%s",
30+
"AWS::EC2::Subnet", true),
31+
CODE_PIPELINE("https://%s.console.aws.amazon.com/codesuite/codepipeline/pipelines/%s/view",
32+
"",
33+
"AWS::CodePipeline::Pipeline", false),
34+
ECR_REPO("https://%s.console.aws.amazon.com/ecr/repositories/%s/",
35+
"",
36+
"AWS::ECR::Repository", false),
37+
LOAD_BALANCER("https://%s.console.aws.amazon.com/ec2/v2/home?region=%s#LoadBalancers:search=%s",
38+
"",
39+
"AWS::ElasticLoadBalancingV2::LoadBalancer", true),
40+
HTTP_LISTENER("https://%s.console.aws.amazon.com/ec2/v2/home?region=%s#LoadBalancers:search=%s",
41+
"",
42+
"AWS::ElasticLoadBalancingV2::Listener", true),
43+
HTTPS_LISTENER("https://%s.console.aws.amazon.com/ec2/v2/home?region=%s#LoadBalancers:search=%s",
44+
"",
45+
"AWS::ElasticLoadBalancingV2::Listener", true),
46+
CLOUDFORMATION("https://%s.console.aws.amazon.com/cloudformation/home?region=%s#/stacks/stackinfo?filteringStatus=active&viewNested=true&hideStacks=false&stackId=%s",
47+
"",
48+
"AWS::CloudFormation::Stack", true),
49+
ECS_SECURITY_GROUP("https://%s.console.aws.amazon.com/ec2/v2/home?region=%s#SecurityGroup:groupId=%s",
50+
"arn:%s:ec2:%s:%s:security-group/%s",
51+
"AWS::EC2::SecurityGroup", true);
52+
53+
private static final Logger LOGGER = LoggerFactory.getLogger(AwsResource.class);
54+
55+
private final String urlFormat;
56+
private final String arnFormat;
57+
private final String resourceType;
58+
private final boolean repeatRegion;
59+
60+
AwsResource(String urlFormat, String arnFormat, String resourceType, boolean repeatRegion) {
61+
this.urlFormat = urlFormat;
62+
this.arnFormat = arnFormat;
63+
this.resourceType = resourceType;
64+
this.repeatRegion = repeatRegion;
65+
}
66+
67+
public String getUrlFormat() {
68+
return this.urlFormat;
69+
}
70+
71+
public String getArnFormat() {
72+
return arnFormat;
73+
}
74+
75+
public String getResourceType() {
76+
return this.resourceType;
77+
}
78+
79+
public String formatUrl(String region, String resourceId) {
80+
String url;
81+
try {
82+
if (this.repeatRegion) {
83+
url = String.format(this.urlFormat, region, region, resourceId);
84+
} else {
85+
url = String.format(this.urlFormat, region, resourceId);
86+
}
87+
} catch (IllegalFormatException e) {
88+
LOGGER.error("Error formatting URL for {}", this.name(), e);
89+
LOGGER.error(Utils.getFullStackTrace(e));
90+
throw new RuntimeException(e);
91+
}
92+
return url;
93+
}
94+
95+
public String formatArn(String partition, String region, String accountId, String resourceId) {
96+
return String.format(this.arnFormat, partition, region, accountId, resourceId);
97+
}
98+
99+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
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+
117
package com.amazon.aws.partners.saasfactory.saasboost;
218

319
import java.util.LinkedHashMap;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
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+
117
package com.amazon.aws.partners.saasfactory.saasboost;
218

319
import java.util.LinkedHashMap;

0 commit comments

Comments
 (0)