in services/onboarding-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingService.java [166:400]
public APIGatewayProxyResponseEvent startOnboarding(Map<String, Object> event, Context context) {
if (Utils.warmup(event)) {
//LOGGER.info("Warming up");
return new APIGatewayProxyResponseEvent().withHeaders(CORS).withStatusCode(200);
}
if (Utils.isBlank(ONBOARDING_WORKFLOW)) {
throw new IllegalStateException("Missing required environment variable ONBOARDING_WORKFLOW");
}
if (Utils.isBlank(SAAS_BOOST_BUCKET)) {
throw new IllegalStateException("Missing required environment variable SAAS_BOOST_BUCKET");
}
long startTimeMillis = System.currentTimeMillis();
LOGGER.info("OnboardingService::startOnboarding");
Utils.logRequestEvent(event);
// Check to see if there are any images in the ECR repo before allowing onboarding
try {
ListImagesResponse dockerImages = ecr.listImages(request -> request.repositoryName(ECR_REPO));
//ListImagesResponse::hasImageIds will return true if the imageIds object is not null
if (!dockerImages.hasImageIds() || dockerImages.imageIds().isEmpty()) {
return new APIGatewayProxyResponseEvent()
.withStatusCode(400)
.withHeaders(CORS)
.withBody("{\"message\": \"No workload image deployed to ECR.\"}");
}
} catch (SdkServiceException ecrError) {
LOGGER.error("ecr:ListImages error", ecrError.getMessage());
LOGGER.error(Utils.getFullStackTrace(ecrError));
throw ecrError;
}
// Parse the onboarding request
Map<String, Object> requestBody = Utils.fromJson((String) event.get("body"), HashMap.class);
if (null == requestBody) {
return new APIGatewayProxyResponseEvent()
.withStatusCode(400)
.withHeaders(CORS)
.withBody("{\"message\": \"Invalid Json in Request.\"}");
}
String tenantName = (String) requestBody.get("name");
String subdomain = (String) requestBody.get("subdomain");
String tshirt = (String) requestBody.get("computeSize");
ComputeSize computeSize = null;
Integer memory = (Integer) requestBody.get("memory");
Integer cpu = (Integer) requestBody.get("cpu");
Integer minCount = (Integer) requestBody.get("minCount");
Integer maxCount = (Integer) requestBody.get("maxCount");
String planId = (String) requestBody.get("planId");
// Make sure we're not trying to onboard a tenant to an existing subdomain
if (Utils.isNotBlank(subdomain)) {
Map<String, String> settings = null;
ApiRequest getSettingsRequest = ApiRequest.builder()
.resource("settings?setting=HOSTED_ZONE&setting=DOMAIN_NAME")
.method("GET")
.build();
SdkHttpFullRequest getSettingsApiRequest = ApiGatewayHelper.getApiRequest(API_GATEWAY_HOST, API_GATEWAY_STAGE, getSettingsRequest);
LOGGER.info("Fetching SaaS Boost hosted zone id from Settings Service");
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 response body.\"}");
}
settings = getSettingsResponse
.stream()
.collect(Collectors.toMap(
setting -> setting.get("name"), setting -> setting.get("value")
));
String hostedZoneId = settings.get("HOSTED_ZONE");
String domainName = settings.get("DOMAIN_NAME");
// Ask Route53 for all the records of this hosted zone
ListResourceRecordSetsResponse recordSets = route53.listResourceRecordSets(request -> request.hostedZoneId(hostedZoneId));
if (recordSets.hasResourceRecordSets()) {
for (ResourceRecordSet recordSet : recordSets.resourceRecordSets()) {
if (RRType.A == recordSet.type()) {
// Hosted Zone alias for the tenant subdomain
String recordSetName = recordSet.name();
String existingSubdomain = recordSetName.substring(0, recordSetName.indexOf(domainName) - 1);
LOGGER.info("Existing tenant subdomain " + existingSubdomain + " for record set " + recordSetName);
if (subdomain.equalsIgnoreCase(existingSubdomain)) {
return new APIGatewayProxyResponseEvent()
.withStatusCode(400)
.withHeaders(CORS)
.withBody("{\"message\": \"Tenant subdomain " + subdomain + " is already in use.\"}");
}
}
}
}
} catch (Exception e) {
LOGGER.error("Error invoking API settings?setting=HOSTED_ZONE&setting=DOMAIN_NAME");
LOGGER.error(Utils.getFullStackTrace(e));
return new APIGatewayProxyResponseEvent()
.withStatusCode(400)
.withHeaders(CORS)
.withBody("{\"message\":\"Error invoking settings API\"}");
}
}
// Create a new onboarding request record for a tenant
if (Utils.isBlank(tenantName)) {
return new APIGatewayProxyResponseEvent()
.withStatusCode(400)
.withHeaders(CORS)
.withBody("{\"message\": \"Tenant name is required.\"}");
}
if (Utils.isNotEmpty(tshirt)) {
try {
computeSize = ComputeSize.valueOf(tshirt);
} catch (IllegalArgumentException e) {
LOGGER.error("Invalid compute size {}", tshirt);
return new APIGatewayProxyResponseEvent()
.withStatusCode(400)
.withHeaders(CORS)
.withBody("{\"message\":\"Invalid compute size\"}");
}
}
Boolean overrideDefaults = (computeSize != null || memory != null || cpu != null || minCount != null || maxCount != null);
if (overrideDefaults) {
if (!validateTenantOverrides(computeSize, memory, cpu, minCount, maxCount)) {
LOGGER.error("Invalid default overrides. Both compute sizing and min and max counts must be set");
return new APIGatewayProxyResponseEvent()
.withStatusCode(400)
.withHeaders(CORS)
.withBody("{\"message\":\"Invalid default overrides. Both compute sizing and min and max counts must be set.\"}");
}
}
//check if Quotas will be exceeded.
try {
LOGGER.info("Check Service Quota Limits");
Map<String, Object> retMap = checkLimits();
Boolean passed = (Boolean) retMap.get("passed");
String message = (String) retMap.get("message");
if (!passed) {
LOGGER.error("Provisioning will exceed limits. {}", message);
return new APIGatewayProxyResponseEvent()
.withStatusCode(400)
.withHeaders(CORS)
.withBody("{\"message\":\"Provisioning will exceed limits. " + message + "\"}");
}
} catch (Exception e) {
LOGGER.error((Utils.getFullStackTrace(e)));
throw new RuntimeException("Error checking Service Quotas with Private API quotas/check");
}
//*TODO: We should add check for CIDRs here!
UUID onboardingId = UUID.randomUUID();
Onboarding onboarding = new Onboarding(onboardingId, OnboardingStatus.created);
onboarding.setTenantName((String) requestBody.get("name"));
onboarding = dal.insertOnboarding(onboarding);
// Collect up the input we need to send to the tenant service via our Step Functions workflow
Map<String, Object> tenant = new HashMap<>();
tenant.put("active", true);
tenant.put("onboardingStatus", onboarding.getStatus().toString());
tenant.put("name", tenantName);
if (Utils.isNotBlank(subdomain)) {
tenant.put("subdomain", subdomain);
}
tenant.put("overrideDefaults", overrideDefaults);
if (overrideDefaults) {
if (computeSize != null) {
tenant.put("computeSize", computeSize.name());
tenant.put("memory", computeSize.getMemory());
tenant.put("cpu", computeSize.getCpu());
} else {
tenant.put("memory", memory);
tenant.put("cpu", cpu);
}
tenant.put("maxCount", maxCount);
tenant.put("minCount", minCount);
}
if (Utils.isNotBlank(planId)) {
tenant.put("planId", planId);
}
//generate a pre-signed url to upload the zip file
String key = "temp/" + onboarding.getId().toString() + ".zip";
final Duration expires = Duration.ofMinutes(15); // UI times out in 10 min
// Generate the presigned URL
PresignedPutObjectRequest presignedObject = presigner.presignPutObject(request -> request
.signatureDuration(expires)
.putObjectRequest(PutObjectRequest.builder()
.bucket(SAAS_BOOST_BUCKET)
.key(key)
.build()
)
.build()
);
onboarding.setZipFileUrl(presignedObject.url().toString());
Map<String, Object> input = new HashMap<>();
input.put("onboardingId", onboarding.getId().toString());
input.put("tenant", tenant);
String executionName = onboarding.getId().toString();
String inputJson = Utils.toJson(input);
try {
LOGGER.info("OnboardingService::startOnboarding Starting Step Functions execution");
LOGGER.info(inputJson);
StartExecutionResponse response = snf.startExecution(StartExecutionRequest
.builder()
.name(executionName)
.input(inputJson)
.stateMachineArn(ONBOARDING_WORKFLOW)
.build()
);
LOGGER.info("OnboardingService::startOnboarding Step Functions responded with " + response.toString());
} catch (SdkServiceException snfError) {
LOGGER.error("OnboardingService::startOnboarding Step Functions error " + snfError.getMessage());
LOGGER.error(Utils.getFullStackTrace(snfError));
dal.updateStatus(onboardingId, OnboardingStatus.failed);
throw snfError;
}
long totalTimeMillis = System.currentTimeMillis() - startTimeMillis;
LOGGER.info("OnboardingService::startOnboarding exec " + totalTimeMillis);
return new APIGatewayProxyResponseEvent()
.withHeaders(CORS)
.withStatusCode(200)
.withBody(Utils.toJson(onboarding));
}