protected void installAnalyticsModule()

in installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostInstall.java [549:683]


    protected void installAnalyticsModule() {
        LOGGER.info("Installing Analytics module into existing AWS SaaS Boost installation.");
        outputMessage("Analytics will be deployed into the existing AWS SaaS Boost environment " + this.envName + ".");

        String metricsStackName = analyticsStackName();
        try {
            DescribeStacksResponse metricsStackResponse = cfn.describeStacks(request -> request.stackName(metricsStackName));
            if (metricsStackResponse.hasStacks()) {
                outputMessage("AWS SaaS Boost Analytics stack with name: " + metricsStackName + " is already deployed");
                System.exit(2);
            }
        } catch (CloudFormationException cfnError) {
            // Calling describe-stacks on a stack name that doesn't exist is an exception
            if (!cfnError.getMessage().contains("Stack with id " + metricsStackName + " does not exist")) {
                LOGGER.error("cloudformation:DescribeStacks error {}", cfnError.getMessage());
                LOGGER.error(getFullStackTrace(cfnError));
                throw cfnError;
            }
        }

        outputMessage("===========================================================");
        outputMessage("");
        outputMessage("Would you like to continue the Analytics module installation with the following options?");
        outputMessage("Existing AWS SaaS Boost environment : " + envName);
        if (useQuickSight) {
            outputMessage("Amazon QuickSight user for Analytics Module: " + quickSightUsername);
        } else {
            outputMessage("Amazon QuickSight user for Analytics Module: N/A");
        }

        System.out.print("Continue (y or n)? ");
        boolean continueInstall = Keyboard.readBoolean();
        if (!continueInstall) {
            outputMessage("Canceled installation of AWS SaaS Boost Analytics");
            cancel();
        }
        outputMessage("Continuing installation of AWS SaaS Boost Analytics");
        outputMessage("===========================================================");
        outputMessage("Installing AWS SaaS Boost Metrics and Analytics Module");
        outputMessage("===========================================================");

        // Generate a password for the RedShift database if we don't already have one
        String dbPassword = null;
        String dbPasswordParam = "/saas-boost/" + this.envName + "/REDSHIFT_MASTER_PASSWORD";
        try {
            GetParameterResponse existingDbPasswordResponse = ssm.getParameter(GetParameterRequest.builder()
                    .name(dbPasswordParam)
                    .withDecryption(true)
                    .build()
            );
            // We actually need the secret value because we need to give it to QuickSight
            dbPassword = existingDbPasswordResponse.parameter().value();
            // And, we'll add the parameter version to the end of the name just in case it's greater than 1
            // so that CloudFormation can properly fetch the secret value
            dbPasswordParam = dbPasswordParam + ":" + existingDbPasswordResponse.parameter().version();
            LOGGER.info("Reusing existing RedShift password for Analytics");
        } catch (SdkServiceException noSuchParameter) {
            LOGGER.info("Generating new random RedShift password for Analytics");
            // Save the database password as a secret
            dbPassword = generatePassword(16);
            try {
                LOGGER.info("Saving RedShift password secret to Parameter Store");
                PutParameterResponse dbPasswordResponse = ssm.putParameter(PutParameterRequest.builder()
                        .name(dbPasswordParam)
                        .type(ParameterType.SECURE_STRING)
                        .overwrite(true)
                        .value(dbPassword)
                        .build()
                );
            } catch (SdkServiceException ssmError) {
                LOGGER.error("ssm:PutParamter error {}", ssmError.getMessage());
                LOGGER.error(getFullStackTrace(ssmError));
                throw ssmError;
            }
            // CloudFormation ssm-secure resolution needs a version number, which is guaranteed to be 1
            // in this case where we just created it
            dbPasswordParam = dbPasswordParam + ":1";
        }
        outputMessage("Redshift Database User Password stored in secure SSM Parameter: " + dbPasswordParam);

        // Run CloudFormation
        outputMessage("Creating CloudFormation stack " + metricsStackName + " for Analytics Module");
        String databaseName = "sb_analytics_" + this.envName.replaceAll("-", "_");
        createMetricsStack(metricsStackName, dbPasswordParam, databaseName);

        // TODO Why doesn't the CloudFormation template own this?
        LOGGER.info("Update SSM param METRICS_ANALYTICS_DEPLOYED to true");
        try {
            PutParameterResponse response = ssm.putParameter(request -> request
                    .name("/saas-boost/" + this.envName + "/METRICS_ANALYTICS_DEPLOYED")
                    .type(ParameterType.STRING)
                    .overwrite(true)
                    .value("true")
            );
        } catch (SdkServiceException ssmError) {
            LOGGER.error("ssm:PutParameter error {}", ssmError.getMessage());
            LOGGER.error(getFullStackTrace(ssmError));
            throw ssmError;
        }

        // Upload the JSON path file for Redshift to the bucket provisioned by CloudFormation
        Map<String, String> outputs = getMetricStackOutputs(metricsStackName);
        String metricsBucket = outputs.get("MetricsBucket");
        Path jsonPathFile = workingDir.resolve(Path.of("metrics-analytics", "deploy", "artifacts", "metrics_redshift_jsonpath.json"));

        LOGGER.info("Copying json files for Metrics and Analytics from {} to {}", jsonPathFile.toString(), metricsBucket);
        try {
            PutObjectResponse s3Response = s3.putObject(PutObjectRequest.builder()
                    .bucket(metricsBucket)
                    .key("metrics_redshift_jsonpath.json")
                    .contentType("text/json")
                    .build(), RequestBody.fromFile(jsonPathFile)
            );
        } catch (SdkServiceException s3Error) {
            LOGGER.error("s3:PutObject error {}", s3Error.getMessage());
            LOGGER.error(getFullStackTrace(s3Error));
            outputMessage("Error copying " + jsonPathFile.toString() + " to " + metricsBucket);
            // TODO Why don't we bail here if that file is required?
            outputMessage("Continuing with installation so you will need to manually upload that file.");
        }

        // Setup the quicksight dataset
        if (useQuickSight) {
            outputMessage("Set up Amazon Quicksight for Analytics Module");
            try {
                // TODO does this fail if it's run more than once?
                setupQuickSight(metricsStackName, outputs, dbPassword);
            } catch (Exception e) {
                outputMessage("Error with setup of Quicksight datasource and dataset. Check log file.");
                outputMessage("Message: " + e.getMessage());
                LOGGER.error(getFullStackTrace(e));
                System.exit(2);
            }
        }
    }