public APIGatewayProxyResponseEvent provisionTenant()

in services/onboarding-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingService.java [467:866]


    public APIGatewayProxyResponseEvent provisionTenant(Map<String, Object> event, Context context) {
        if (Utils.warmup(event)) {
            //LOGGER.info("Warming up");
            return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(200);
        }
        if (Utils.isBlank(SAAS_BOOST_ENV)) {
            throw new IllegalStateException("Missing required environment variable SAAS_BOOST_ENV");
        }
        if (Utils.isBlank(API_GATEWAY_HOST)) {
            throw new IllegalStateException("Missing required environment variable API_GATEWAY_HOST");
        }
        if (Utils.isBlank(API_GATEWAY_STAGE)) {
            throw new IllegalStateException("Missing required environment variable API_GATEWAY_STAGE");
        }
        if (Utils.isBlank(API_TRUST_ROLE)) {
            throw new IllegalStateException("Missing required environment variable API_TRUST_ROLE");
        }

        Utils.logRequestEvent(event);
        long startTimeMillis = System.currentTimeMillis();
        LOGGER.info("OnboardingService::provisionTenant");
        APIGatewayProxyResponseEvent response = null;

        Map<String, Object> requestBody = (Map<String, Object>) event.get("body");
        if (requestBody.isEmpty()) {
            response = new APIGatewayProxyResponseEvent()
                    .withStatusCode(400)
                    .withHeaders(CORS)
                    .withBody("{\"message\":\"Empty request body.\"}");
        } else {
            // The invocation event sent to us must contain the tenant we're
            // provisioning for and the onboarding job that's tracking it
            UUID onboardingId = UUID.fromString((String) requestBody.get("onboardingId"));
            Map<String, Object> tenant = Utils.fromJson((String) requestBody.get("tenant"), HashMap.class);
            if (null == tenant) {
                return new APIGatewayProxyResponseEvent()
                        .withStatusCode(400)
                        .withHeaders(CORS)
                        .withBody("{\"message\": \"Invalid request body.\"}");
            }
            UUID tenantId = UUID.fromString(((String) tenant.get("id")).toLowerCase());

            // Get the settings for this SaaS Boost install for this SaaS Boost "environment"
            Map<String, String> settings = null;
            ApiRequest getSettingsRequest = ApiRequest.builder()
                    .resource("settings")
                    .method("GET")
                    .build();
            SdkHttpFullRequest getSettingsApiRequest = ApiGatewayHelper.getApiRequest(API_GATEWAY_HOST, API_GATEWAY_STAGE, getSettingsRequest);
            try {
                String getSettingsResponseBody = ApiGatewayHelper.signAndExecuteApiRequest(getSettingsApiRequest, API_TRUST_ROLE, context.getAwsRequestId());
                ArrayList<Map<String, String>> getSettingsResponse = Utils.fromJson(getSettingsResponseBody, ArrayList.class);
                if (null == getSettingsResponse) {
                    return new APIGatewayProxyResponseEvent()
                            .withStatusCode(400)
                            .withHeaders(CORS)
                            .withBody("{\"message\": \"Invalid settings response.\"}");
                }
                settings = getSettingsResponse
                        .stream()
                        .collect(Collectors.toMap(
                                setting -> setting.get("name"), setting -> setting.get("value")
                        ));
            } catch (Exception e) {
                LOGGER.error("Error invoking API settings");
                dal.updateStatus(onboardingId, OnboardingStatus.failed);
                LOGGER.error(Utils.getFullStackTrace(e));
                throw new RuntimeException(e);
            }

            // We can't continue if any of the SaaS Boost settings are blank
            if (settings == null || settings.isEmpty()) {
                LOGGER.error("One or more required SaaS Boost parameters is missing.");
                dal.updateStatus(onboardingId, OnboardingStatus.failed);
                throw new RuntimeException("SaaS Boost parameters are missing.");
            }

            // And parameters specific to this tenant
            String cidrPrefix = null;
            try {
                String cidrBlock = dal.assignCidrBlock(tenantId.toString());
                cidrPrefix = cidrBlock.substring(0, cidrBlock.indexOf(".", cidrBlock.indexOf(".") + 1));
            } catch (Exception e) {
                dal.updateStatus(onboardingId, OnboardingStatus.failed);
                throw e;
            }

            String taskMemory = settings.get("TASK_MEMORY");
            if (tenant.get("memory") != null) {
                try {
                    taskMemory = ((Integer) tenant.get("memory")).toString();
                    LOGGER.info("Override default task memory with {}", taskMemory);
                } catch (NumberFormatException nfe) {
                    LOGGER.error("Can't parse tenant task memory from {}", tenant.get("memory"));
                    dal.updateStatus(onboardingId, OnboardingStatus.failed);
                    LOGGER.error(Utils.getFullStackTrace(nfe));
                }
            }
            String taskCpu = settings.get("TASK_CPU");
            if (tenant.get("cpu") != null) {
                try {
                    taskCpu = ((Integer) tenant.get("cpu")).toString();
                    LOGGER.info("Override default task CPU with {}", taskCpu);
                } catch (NumberFormatException nfe) {
                    LOGGER.error("Can't parse tenant task CPU from {}", tenant.get("cpu"));
                    dal.updateStatus(onboardingId, OnboardingStatus.failed);
                    LOGGER.error(Utils.getFullStackTrace(nfe));
                }
            }
            String taskCount = settings.get("MIN_COUNT");
            if (tenant.get("minCount") != null) {
                try {
                    taskCount = ((Integer) tenant.get("minCount")).toString();
                } catch (NumberFormatException nfe) {
                    LOGGER.error("Can't parse tenant min task count from {}", tenant.get("minCount"));
                    dal.updateStatus(onboardingId, OnboardingStatus.failed);
                    LOGGER.error(Utils.getFullStackTrace(nfe));
                }
            }
            String maxTaskCount = settings.get("MAX_COUNT");
            if (tenant.get("maxCount") != null) {
                try {
                    maxTaskCount = ((Integer) tenant.get("maxCount")).toString();
                } catch (NumberFormatException nfe) {
                    LOGGER.error("Can't parse tenant max task count from {}", tenant.get("maxCount"));
                    dal.updateStatus(onboardingId, OnboardingStatus.failed);
                    LOGGER.error(Utils.getFullStackTrace(nfe));
                }
            }
            String tenantSubdomain = (String) tenant.get("subdomain");
            if (tenantSubdomain == null) {
                tenantSubdomain = "";
            }

            // Did the ISV configure the application for a shared filesystem?

            Boolean enableEfs = Boolean.FALSE;
            Boolean enableFSx = Boolean.FALSE;
            String mountPoint = "";
            Boolean encryptFilesystem = Boolean.FALSE;
            String filesystemLifecycle = "NEVER";
            String fileSystemType = settings.get("FILE_SYSTEM_TYPE");
            String fsxStorageGb = "0";
            String fsxThroughputMbs = "0";
            String fsxBackupRetentionDays = "7";
            String fsxDailyBackupTime = "";
            String fsxWeeklyMaintenanceTime = "";
            String fsxWindowsMountDrive = "";


            if (null != fileSystemType && !fileSystemType.isEmpty()) {
                mountPoint = settings.get("FILE_SYSTEM_MOUNT_POINT");
                if ("FSX".equals(fileSystemType)) {
                    enableFSx = true;
                    fsxStorageGb = settings.get("FSX_STORAGE_GB"); // GB 32 to 65,536
                    if (tenant.get("fsxStorageGb") != null) {
                        try {
                            fsxStorageGb = ((Integer) tenant.get("fsxStorageGb")).toString();
                            LOGGER.info("Override default FSX Storage GB with {}", fsxStorageGb);
                        } catch (NumberFormatException nfe) {
                            LOGGER.error("Can't parse tenant task FSX Storage GB from {}", tenant.get("fsxStorageGb"));
                            dal.updateStatus(onboardingId, OnboardingStatus.failed);
                            LOGGER.error(Utils.getFullStackTrace(nfe));
                        }
                    }

                    fsxThroughputMbs = settings.get("FSX_THROUGHPUT_MBS"); // MB/s
                    if (tenant.get("fsxThroughputMbs") != null) {
                        try {
                            fsxThroughputMbs = ((Integer) tenant.get("fsxThroughputMbs")).toString();
                            LOGGER.info("Override default FSX Throughput with {}", fsxThroughputMbs);
                        } catch (NumberFormatException nfe) {
                            LOGGER.error("Can't parse tenant task FSX Throughput from {}", tenant.get("fsxThroughputMbs"));
                            dal.updateStatus(onboardingId, OnboardingStatus.failed);
                            LOGGER.error(Utils.getFullStackTrace(nfe));
                        }
                    }

                    fsxBackupRetentionDays = settings.get("FSX_BACKUP_RETENTION_DAYS"); // 7 to 35
                    if (tenant.get("fsxBackupRetentionDays") != null) {
                        try {
                            fsxBackupRetentionDays = ((Integer) tenant.get("fsxBackupRetentionDays")).toString();
                            LOGGER.info("Override default FSX Throughput with {}", fsxBackupRetentionDays);
                        } catch (NumberFormatException nfe) {
                            LOGGER.error("Can't parse tenant task FSX Throughput from {}", tenant.get("fsxBackupRetentionDays"));
                            dal.updateStatus(onboardingId, OnboardingStatus.failed);
                            LOGGER.error(Utils.getFullStackTrace(nfe));
                        }
                    }

                    fsxDailyBackupTime = settings.get("FSX_DAILY_BACKUP_TIME"); //HH:MM in UTC
                    if (tenant.get("fsxDailyBackupTime") != null) {
                        fsxDailyBackupTime = (String) tenant.get("fsxDailyBackupTime");
                            LOGGER.info("Override default FSX Daily Backup time with {}", fsxDailyBackupTime);
                    }

                    fsxWeeklyMaintenanceTime = settings.get("FSX_WEEKLY_MAINTENANCE_TIME");//d:HH:MM in UTC
                    if (tenant.get("fsxWeeklyMaintenanceTime") != null) {
                        fsxWeeklyMaintenanceTime = (String) tenant.get("fsxWeeklyMaintenanceTime");
                        LOGGER.info("Override default FSX Weekly Maintenance time with {}", fsxWeeklyMaintenanceTime);
                    }

                    fsxWindowsMountDrive = settings.get("FSX_WINDOWS_MOUNT_DRIVE");
                    //Note:  Do not want to override the FSX_WINDOWS_MOUNT_DRIVE as that should be same for all tenants

                } else { //this is for EFS file system
                    enableEfs = true;
                    encryptFilesystem = Boolean.valueOf(settings.get("FILE_SYSTEM_ENCRYPT"));
                    filesystemLifecycle = settings.get("FILE_SYSTEM_LIFECYCLE");
                }
            }

            // Did the ISV configure the application for a database?
            Boolean enableDatabase = Boolean.FALSE;
            String dbInstanceClass = "";
            String dbEngine = "";
            String dbVersion = "";
            String dbFamily = "";
            String dbMasterUsername = "";
            String dbMasterPasswordRef = "";
            String dbPort = "";
            String dbDatabase = "";
            String dbBootstrap = "";
            if (settings.get("DB_ENGINE") != null && !settings.get("DB_ENGINE").isEmpty()) {
                enableDatabase = Boolean.TRUE;
                dbEngine = settings.get("DB_ENGINE");
                dbVersion = settings.get("DB_VERSION");
                dbFamily = settings.get("DB_PARAM_FAMILY");
                dbInstanceClass = settings.get("DB_INSTANCE_TYPE");
                dbMasterUsername = settings.get("DB_MASTER_USERNAME");
                dbPort = settings.get("DB_PORT");
                dbDatabase = settings.get("DB_NAME");
                dbBootstrap = settings.get("DB_BOOTSTRAP_FILE");

                // CloudFormation needs the Parameter Store reference key (version number) to properly
                // decode secure string parameters... So we need to call the private API to get it.
                ApiRequest paramStoreRef = ApiRequest.builder()
                        .resource("settings/DB_MASTER_PASSWORD/ref")
                        .method("GET")
                        .build();
                SdkHttpFullRequest paramStoreRefApiRequest = ApiGatewayHelper.getApiRequest(API_GATEWAY_HOST, API_GATEWAY_STAGE, paramStoreRef);
                try {
                    String paramStoreRefResponseBody = ApiGatewayHelper.signAndExecuteApiRequest(paramStoreRefApiRequest, API_TRUST_ROLE, context.getAwsRequestId());
                    Map<String, String> dbPasswordRef = Utils.fromJson(paramStoreRefResponseBody, HashMap.class);
                    if (null == dbPasswordRef) {
                        return new APIGatewayProxyResponseEvent()
                                .withStatusCode(400)
                                .withHeaders(CORS)
                                .withBody("{\"message\": \"Invalid response body.\"}");
                    }
                    dbMasterPasswordRef = dbPasswordRef.get("reference-key");
                } catch (Exception e) {
                    LOGGER.error("Error invoking API settings/DB_MASTER_PASSWORD/ref");
                    dal.updateStatus(onboardingId, OnboardingStatus.failed);
                    LOGGER.error(Utils.getFullStackTrace(e));
                    throw new RuntimeException(e);
                }
            }

            // If the tenant is being onboarded into a billing plan, we need to send
            // it through so we can configure it with the 3rd party when the stack completes
            String billingPlan = (String) tenant.get("planId");
            if (billingPlan == null) {
                billingPlan = "";
            }

            // CloudFormation needs the Parameter Store reference key (version number) to properly
            // decode secure string parameters... So we need to call the private API to get it.
            String sslCertArn= settings.get("SSL_CERT_ARN");
            String sslCertArnRef = "";
            if (null != sslCertArn && !"".equals(sslCertArn)) {
                ApiRequest paramStoreRef = ApiRequest.builder()
                        .resource("settings/SSL_CERT_ARN/ref")
                        .method("GET")
                        .build();
                SdkHttpFullRequest paramStoreRefApiRequest = ApiGatewayHelper.getApiRequest(API_GATEWAY_HOST, API_GATEWAY_STAGE, paramStoreRef);
                try {
                    String paramStoreRefResponseBody = ApiGatewayHelper.signAndExecuteApiRequest(paramStoreRefApiRequest, API_TRUST_ROLE, context.getAwsRequestId());
                    Map<String, String> certRef = Utils.fromJson(paramStoreRefResponseBody, HashMap.class);
                    if (null == certRef) {
                        return new APIGatewayProxyResponseEvent()
                                .withStatusCode(400)
                                .withHeaders(CORS)
                                .withBody("{\"message\": \"Invalid response body.\"}");
                    }
                    sslCertArnRef = certRef.get("reference-key");
                } catch (Exception e) {
                    LOGGER.error("Error invoking API settings/SSL_CERT_ARN/ref");
                    dal.updateStatus(onboardingId, OnboardingStatus.failed);
                    LOGGER.error(Utils.getFullStackTrace(e));
                    throw new RuntimeException(e);
                }
            }


        // CloudFormation won't let you use dashes or underscores in Mapping second level key names
            // And it won't let you use Fn::Join or Fn::Split in Fn::FindInMap... so we will mangle this
            // parameter before we send it in.
            String clusterOS = settings.getOrDefault("CLUSTER_OS", "").replace("_", "");

            List<Parameter> templateParameters = new ArrayList<>();
            templateParameters.add(Parameter.builder().parameterKey("TenantId").parameterValue(tenantId.toString()).build());
            templateParameters.add(Parameter.builder().parameterKey("TenantSubDomain").parameterValue(tenantSubdomain).build());
            templateParameters.add(Parameter.builder().parameterKey("Environment").parameterValue(SAAS_BOOST_ENV).build());
            templateParameters.add(Parameter.builder().parameterKey("SaaSBoostBucket").parameterValue(settings.get("SAAS_BOOST_BUCKET")).build());
            templateParameters.add(Parameter.builder().parameterKey("LambdaSourceFolder").parameterValue(settings.get("SAAS_BOOST_LAMBDAS_FOLDER")).build());
            templateParameters.add(Parameter.builder().parameterKey("DockerHostOS").parameterValue(clusterOS).build());
            templateParameters.add(Parameter.builder().parameterKey("DockerHostInstanceType").parameterValue(settings.get("CLUSTER_INSTANCE_TYPE")).build());
            templateParameters.add(Parameter.builder().parameterKey("TaskMemory").parameterValue(taskMemory).build());
            templateParameters.add(Parameter.builder().parameterKey("TaskCPU").parameterValue(taskCpu).build());
            templateParameters.add(Parameter.builder().parameterKey("TaskCount").parameterValue(taskCount).build());
            templateParameters.add(Parameter.builder().parameterKey("MaxTaskCount").parameterValue(maxTaskCount).build());
            templateParameters.add(Parameter.builder().parameterKey("ContainerRepository").parameterValue(settings.get("ECR_REPO")).build());
            templateParameters.add(Parameter.builder().parameterKey("ContainerPort").parameterValue(settings.get("CONTAINER_PORT")).build());
            templateParameters.add(Parameter.builder().parameterKey("ContainerHealthCheckPath").parameterValue(settings.get("HEALTH_CHECK")).build());
            templateParameters.add(Parameter.builder().parameterKey("CodePipelineRoleArn").parameterValue(settings.get("CODE_PIPELINE_ROLE")).build());
            templateParameters.add(Parameter.builder().parameterKey("ArtifactBucket").parameterValue(settings.get("CODE_PIPELINE_BUCKET")).build());
            templateParameters.add(Parameter.builder().parameterKey("TransitGateway").parameterValue(settings.get("TRANSIT_GATEWAY")).build());
            templateParameters.add(Parameter.builder().parameterKey("TenantTransitGatewayRouteTable").parameterValue(settings.get("TRANSIT_GATEWAY_ROUTE_TABLE")).build());
            templateParameters.add(Parameter.builder().parameterKey("EgressTransitGatewayRouteTable").parameterValue(settings.get("EGRESS_ROUTE_TABLE")).build());
            templateParameters.add(Parameter.builder().parameterKey("CidrPrefix").parameterValue(cidrPrefix).build());
            templateParameters.add(Parameter.builder().parameterKey("DomainName").parameterValue(settings.get("DOMAIN_NAME")).build());
            templateParameters.add(Parameter.builder().parameterKey("SSLCertArnParam").parameterValue(sslCertArnRef).build());
            templateParameters.add(Parameter.builder().parameterKey("HostedZoneId").parameterValue(settings.get("HOSTED_ZONE")).build());
            templateParameters.add(Parameter.builder().parameterKey("UseEFS").parameterValue(enableEfs.toString()).build());
            templateParameters.add(Parameter.builder().parameterKey("MountPoint").parameterValue(mountPoint).build());
            templateParameters.add(Parameter.builder().parameterKey("EncryptEFS").parameterValue(encryptFilesystem.toString()).build());
            templateParameters.add(Parameter.builder().parameterKey("EFSLifecyclePolicy").parameterValue(filesystemLifecycle).build());

            //--> for FSX for Windows
            templateParameters.add(Parameter.builder().parameterKey("UseFSx").parameterValue(enableFSx.toString()).build());
            templateParameters.add(Parameter.builder().parameterKey("FSxWindowsMountDrive").parameterValue(fsxWindowsMountDrive).build());
            templateParameters.add(Parameter.builder().parameterKey("FSxDailyBackupTime").parameterValue(fsxDailyBackupTime).build());
            templateParameters.add(Parameter.builder().parameterKey("FSxBackupRetention").parameterValue(fsxBackupRetentionDays).build());
            templateParameters.add(Parameter.builder().parameterKey("FSxThroughputCapacity").parameterValue(fsxThroughputMbs).build());
            templateParameters.add(Parameter.builder().parameterKey("FSxStorageCapacity").parameterValue(fsxStorageGb).build());
            templateParameters.add(Parameter.builder().parameterKey("FSxWeeklyMaintenanceTime").parameterValue(fsxWeeklyMaintenanceTime).build());
            // <<-
            templateParameters.add(Parameter.builder().parameterKey("UseRDS").parameterValue(enableDatabase.toString()).build());
            templateParameters.add(Parameter.builder().parameterKey("RDSInstanceClass").parameterValue(dbInstanceClass).build());
            templateParameters.add(Parameter.builder().parameterKey("RDSEngine").parameterValue(dbEngine).build());
            templateParameters.add(Parameter.builder().parameterKey("RDSEngineVersion").parameterValue(dbVersion).build());
            templateParameters.add(Parameter.builder().parameterKey("RDSParameterGroupFamily").parameterValue(dbFamily).build());
            templateParameters.add(Parameter.builder().parameterKey("RDSMasterUsername").parameterValue(dbMasterUsername).build());
            templateParameters.add(Parameter.builder().parameterKey("RDSMasterPasswordParam").parameterValue(dbMasterPasswordRef).build());
            templateParameters.add(Parameter.builder().parameterKey("RDSPort").parameterValue(dbPort).build());
            templateParameters.add(Parameter.builder().parameterKey("RDSDatabase").parameterValue(dbDatabase).build());
            templateParameters.add(Parameter.builder().parameterKey("RDSBootstrap").parameterValue(dbBootstrap).build());
            templateParameters.add(Parameter.builder().parameterKey("MetricsStream").parameterValue(settings.get("METRICS_STREAM") != null ? settings.get("METRICS_STREAM") : "").build());
            templateParameters.add(Parameter.builder().parameterKey("ALBAccessLogsBucket").parameterValue(settings.get("ALB_ACCESS_LOGS_BUCKET")).build());
            templateParameters.add(Parameter.builder().parameterKey("EventBus").parameterValue(settings.get("EVENT_BUS")).build());
            templateParameters.add(Parameter.builder().parameterKey("BillingPlan").parameterValue(billingPlan).build());
            for (Parameter p : templateParameters) {
                if (p.parameterValue() == null) {
                    LOGGER.error("OnboardingService::provisionTenant template parameter {} is NULL", p.parameterKey());
                    dal.updateStatus(onboardingId, OnboardingStatus.failed);
                    throw new RuntimeException("CloudFormation template parameter " + p.parameterKey() + " is NULL");
                }
            }

            String tenantShortId = tenantId.toString().substring(0, 8);
            String stackName = "Tenant-" + tenantShortId;

            // Now run the onboarding stack to provision the infrastructure for this tenant
            LOGGER.info("OnboardingService::provisionTenant create stack " + stackName);
            Onboarding onboarding = dal.getOnboarding(onboardingId);
            onboarding.setTenantId(tenantId);
            String stackId = null;
            try {
                CreateStackResponse cfnResponse = cfn.createStack(CreateStackRequest.builder()
                        .stackName(stackName)
                        .onFailure("DO_NOTHING") // This was set to DO_NOTHING to ease debugging of failed stacks. Maybe not appropriate for "production". If we change this we'll have to add a whole bunch of IAM delete permissions to the execution role.
                        //.timeoutInMinutes(60) // Some resources can take a really long time to light up. Do we want to specify this?
                        .capabilitiesWithStrings("CAPABILITY_NAMED_IAM")
                        .notificationARNs(settings.get("ONBOARDING_SNS"))
                        .templateURL("https://" + settings.get("SAAS_BOOST_BUCKET") + ".s3.amazonaws.com/" + settings.get("ONBOARDING_TEMPLATE"))
                        .parameters(templateParameters)
                        .build()
                );
                stackId = cfnResponse.stackId();
                onboarding.setStatus(OnboardingStatus.provisioning);
                onboarding.setStackId(stackId);
                onboarding = dal.updateOnboarding(onboarding);
                LOGGER.info("OnboardingService::provisionTenant stack id " + stackId);
            } catch (SdkServiceException cfnError) {
                LOGGER.error("cloudformation::createStack failed {}", cfnError.getMessage());
                LOGGER.error(Utils.getFullStackTrace(cfnError));
                dal.updateStatus(onboardingId, OnboardingStatus.failed);
                throw cfnError;
            }

            response = new APIGatewayProxyResponseEvent()
                    .withStatusCode(200)
                    .withHeaders(CORS)
                    .withBody(Utils.toJson(onboarding));
        }
        long totalTimeMillis = System.currentTimeMillis() - startTimeMillis;
        LOGGER.info("OnboardingService::provisionTenant exec " + totalTimeMillis);
        return response;
    }