public APIGatewayProxyResponseEvent startOnboarding()

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