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