in functions/onboarding-notification/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingNotification.java [75:344]
public Object handleRequest(SNSEvent event, Context context) {
//LOGGER.info(Utils.toJson(event));
String type = null;
String stackId = null;
String stackName = null;
String stackStatus = null;
List<SNSEvent.SNSRecord> records = event.getRecords();
SNSEvent.SNS sns = records.get(0).getSNS();
String message = sns.getMessage();
// Raw SNS message values are escaped JSON strings with \n instead of newlines and
// single quotes instead of doubles around values
for (String keyValue : message.split("\\n")) {
// Each line will look like Key='Value'
// For example ResourceStatus='CREATE_COMPLETE'
// We'll be reckless and use substring instead of a regex to break it apart.
String key = keyValue.substring(0, keyValue.indexOf("="));
String value = keyValue.substring(keyValue.indexOf("=") + 2, keyValue.length() - 1);
//LOGGER.info(key + " => " + value);
switch (key) {
case "ResourceType":
type = value;
break;
case "ResourceStatus":
stackStatus = value;
break;
case "LogicalResourceId":
stackName = value;
break;
case "StackId":
stackId = value;
break;
}
}
// CloudFormation sends SNS notifications for every resource in a stack going through each status change.
// We're only interested in the stack complete event. Now that we have nested stacks for the optional
// "extensions" like RDS and EFS, we have to check the stack name too
if ("AWS::CloudFormation::Stack".equals(type) &&
("CREATE_COMPLETE".equals(stackStatus) || "CREATE_FAILED".equals(stackStatus)
|| "UPDATE_COMPLETE".equals(stackStatus)
|| "DELETE_COMPLETE".equals(stackStatus)
|| "DELETE_FAILED".equals(stackStatus))
&& stackName.startsWith("Tenant-")) {
LOGGER.info(Utils.toJson(event));
LOGGER.info("Beginning post-onboarding flow. Stack " + stackName + " is in status " + stackStatus);
DescribeStacksResponse stacks = cfn.describeStacks(DescribeStacksRequest
.builder()
.stackName(stackId)
.build()
);
Stack stack = stacks.stacks().get(0);
String tenantId = null;
for (Parameter parameter : stack.parameters()) {
if ("TenantId".equals(parameter.parameterKey())) {
tenantId = parameter.parameterValue();
break;
}
}
if ("CREATE_COMPLETE".equals(stackStatus) || "UPDATE_COMPLETE".equals(stackStatus)) {
// We need the parameters and outputs from the stack
String ecrRepo = null;
String dbHost = null;
String albName = null;
String billingPlan = null;
// Gather up the AWS Console URLs for the resources we're interested in
Map<String, String> consoleResources = new HashMap<>();
consoleResources.put(AwsConsoleUrl.CLOUDFORMATION.name(), AwsConsoleUrl.CLOUDFORMATION.formatUrl(AWS_REGION, stack.stackId()));
for (Parameter parameter : stack.parameters()) {
if ("ContainerRepository".equals(parameter.parameterKey())) {
ecrRepo = parameter.parameterValue();
break;
}
}
LOGGER.info("Stack Outputs:");
for (Output output : stack.outputs()) {
LOGGER.info("{} => {}", output.outputKey(), output.outputValue());
if ("RdsEndpoint".equals(output.outputKey())) {
if (Utils.isNotBlank(output.outputValue())) {
dbHost = output.outputValue();
}
}
if ("LoadBalancer".equals(output.outputKey())) {
if (Utils.isNotBlank(output.outputValue())) {
albName = output.outputValue();
}
}
if ("DNSName".equals(output.outputKey())) {
if (Utils.isNotBlank(output.outputValue())) {
consoleResources.put("LOAD_BALANCER_DNSNAME", output.outputValue());
}
}
if ("BillingPlan".equals(output.outputKey())) {
if (Utils.isNotBlank(output.outputValue())) {
billingPlan = output.outputValue();
}
}
}
// And we need the resources from the stack
ListStackResourcesResponse resources = cfn.listStackResources(ListStackResourcesRequest
.builder()
.stackName(stackId)
.build()
);
String pipeline = null;
for (StackResourceSummary resource : resources.stackResourceSummaries()) {
String resourceType = resource.resourceType();
String physicalResourceId = resource.physicalResourceId();
String resourceStatus = resource.resourceStatusAsString();
String logicalId = resource.logicalResourceId();
LOGGER.debug(("processing resource type: " + resourceType));
//LOGGER.info(String.format("%s %s %s", resourceType, physicalResourceId, resourceStatus));
if ("AWS::CodePipeline::Pipeline".equals(resourceType) && "CREATE_COMPLETE".equals(resourceStatus)) {
pipeline = physicalResourceId;
consoleResources.put(AwsConsoleUrl.CODE_PIPELINE.name(), AwsConsoleUrl.CODE_PIPELINE.formatUrl(AWS_REGION, pipeline));
} else if ("AWS::CloudFormation::Stack".equals(resourceType) && "rds".equals(logicalId)) {
//this is the rds sub-stack so get the cluster and instance ids
getRdsResources(physicalResourceId, consoleResources);
} else {
//match on the resource type and build url
for (AwsConsoleUrl consoleUrl : AwsConsoleUrl.values()) {
if (consoleUrl.getResourceType().equalsIgnoreCase(resourceType)) {
if ("AWS::ElasticLoadBalancingV2::Listener".equals(resourceType)) {
//the ALB physical id is the short tenant id
physicalResourceId = "tenant-" + tenantId.split("-")[0];
} else if ("AWS::Logs::LogGroup".equals(resourceType)) {
//need to replace / with $252F for the url path
physicalResourceId = physicalResourceId.replaceAll("/", Matcher.quoteReplacement("$252F"));
}
consoleResources.put(consoleUrl.name(), consoleUrl.formatUrl(AWS_REGION, physicalResourceId));
}
}
}
}
// Persist the tenant specific things as tenant settings
if (dbHost != null) {
LOGGER.info("Saving tenant database host setting");
Map<String, Object> systemApiRequest = new HashMap<>();
systemApiRequest.put("resource", "settings/tenant/" + tenantId + "/DB_HOST");
systemApiRequest.put("method", "PUT");
systemApiRequest.put("body", "{\"name\":\"DB_HOST\", \"value\":\"" + dbHost + "\"}");
publishEvent(systemApiRequest, SYSTEM_API_CALL);
}
if (albName != null) {
LOGGER.info("Saving tenant ALB setting");
Map<String, Object> systemApiRequest = new HashMap<>();
systemApiRequest.put("resource", "settings/tenant/" + tenantId + "/ALB");
systemApiRequest.put("method", "PUT");
systemApiRequest.put("body", "{\"name\":\"ALB\", \"value\":\"" + albName + "\"}");
publishEvent(systemApiRequest, SYSTEM_API_CALL);
}
// Update the onboarding status from provisioning to provisioned
LOGGER.info("Updating onboarding status to {}", "CREATE_COMPLETE".equals(stackStatus) ? "provisioned" : "updated");
Map<String, Object> systemApiRequest = new HashMap<>();
systemApiRequest.put("resource", "onboarding/status");
systemApiRequest.put("method", "PUT");
systemApiRequest.put("body", "{\"tenantId\":\"" + tenantId + "\", \"stackStatus\":\"" + stackStatus + "\"}");
publishEvent(systemApiRequest, SYSTEM_API_CALL);
// Update the tenant resources map
LOGGER.info("Updating tenant resources AWS console links");
//build string of the resources
StringBuilder resourcesSb = new StringBuilder("{");
String prefix = "";
for (Map.Entry<String, String> entry : consoleResources.entrySet()) {
resourcesSb
.append(prefix)
.append("\"")
.append(entry.getKey())
.append("\":\"")
.append(entry.getValue())
.append("\"");
prefix = ",";
}
resourcesSb.append("}");
//LOGGER.info("Console Resources Map String:" + resourcesSb.toString());
Map<String, Object> updateConsoleResourcesEventDetail = new HashMap<>();
updateConsoleResourcesEventDetail.put("tenantId", tenantId);
updateConsoleResourcesEventDetail.put("resources", resourcesSb.toString());
publishEvent(updateConsoleResourcesEventDetail, UPDATE_TENANT_RESOURCES);
// If there's a billing plan for this tenant, publish the event so they get
// wired up to the 3rd party system
if (Utils.isNotBlank(billingPlan)) {
LOGGER.info("Triggering tenant billing setup");
Map<String, Object> updateBillingPlanEventDetail = new HashMap<>();
updateBillingPlanEventDetail.put("tenantId", tenantId);
updateBillingPlanEventDetail.put("planId", billingPlan);
publishEvent(updateBillingPlanEventDetail, BILLING_SETUP);
}
if ("CREATE_COMPLETE". equals(stackStatus)) {
// Invoke this tenant's deployment pipeline... probably should be done through a service API call
LOGGER.info("Triggering tenant deployment pipeline");
// First, build an event object that looks similar to the object generated by
// an ECR image push (which is what the deploy method uses).
Map<String, Object> lambdaEvent = new HashMap<>();
lambdaEvent.put("source", "tenant-onboarding");
lambdaEvent.put("region", AWS_REGION);
lambdaEvent.put("account", context.getInvokedFunctionArn().split(":")[4]);
Map<String, Object> detail = new HashMap<>();
detail.put("repository-name", ecrRepo);
detail.put("image-tag", "latest");
detail.put("tenantId", tenantId);
detail.put("pipeline", pipeline);
lambdaEvent.put("detail", detail);
SdkBytes payload = SdkBytes.fromString(Utils.toJson(lambdaEvent), Charset.forName("UTF-8"));
// Now invoke the deployment lambda async... probably move this to the tenant service
try {
InvokeResponse deployResponse = lambda.invoke(r -> r
.functionName(TENANT_DEPLOY_LAMBDA)
.invocationType(InvocationType.EVENT)
.payload(payload)
);
} catch (SdkServiceException lambdaError) {
LOGGER.error("lambda:Invoke");
LOGGER.error(Utils.getFullStackTrace(lambdaError));
throw lambdaError;
}
}
} else if ("CREATE_FAILED".equals(stackStatus)) {
// Update the onboarding status from provisioning to failed
LOGGER.info("Updating onboarding status to failed");
Map<String, Object> systemApiRequest = new HashMap<>();
systemApiRequest.put("resource", "onboarding/status");
systemApiRequest.put("method", "PUT");
systemApiRequest.put("body", "{\"tenantId\":\"" + tenantId + "\", \"stackStatus\":\"" + stackStatus + "\"}");
publishEvent(systemApiRequest, SYSTEM_API_CALL);
} else if ("DELETE_COMPLETE".equals(stackStatus)) {
// Delete the tenant settings via the Settings service
LOGGER.info("Deleting tenant settings for tenant " + tenantId);
Map<String, Object> deleteTenantSettingsRequest = new HashMap<>();
deleteTenantSettingsRequest.put("resource", "settings/tenant/" + tenantId);
deleteTenantSettingsRequest.put("method", "DELETE");
publishEvent(deleteTenantSettingsRequest, SYSTEM_API_CALL);
//Publish event for tenant billing disable
LOGGER.info("Triggering tenant billing disable event to cancel subscriptions");
Map<String, Object> updateBillingPlanEventDetail = new HashMap<>();
updateBillingPlanEventDetail.put("tenantId", tenantId);
publishEvent(updateBillingPlanEventDetail, BILLING_DISABLE);
// Update the onboarding status to deleted
LOGGER.info("Updating onboarding status to delete completed");
Map<String, Object> updateTenantOnboardingStatusRequest = new HashMap<>();
updateTenantOnboardingStatusRequest.put("resource", "onboarding/status");
updateTenantOnboardingStatusRequest.put("method", "PUT");
updateTenantOnboardingStatusRequest.put("body", "{\"tenantId\":\"" + tenantId + "\", \"stackStatus\":\"" + stackStatus + "\"}");
publishEvent(updateTenantOnboardingStatusRequest, SYSTEM_API_CALL);
} else if ("DELETE_FAILED".equals(stackStatus)) {
// Update the onboarding status to deleted
LOGGER.info("Updating onboarding status to failed");
Map<String, Object> systemApiRequest = new HashMap<>();
systemApiRequest.put("resource", "onboarding/status");
systemApiRequest.put("method", "PUT");
systemApiRequest.put("body", "{\"tenantId\":\"" + tenantId + "\", \"stackStatus\":\"" + stackStatus + "\"}");
publishEvent(systemApiRequest, SYSTEM_API_CALL);
}
} else {
//LOGGER.info("Skipping CloudFormation notification {} {} {} {}", stackId, type, stackName, stackStatus);
}
return null;
}