public Object handleRequest()

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;
    }