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