From ebe713c966c7632285b93415b8e2bbde2fb14956 Mon Sep 17 00:00:00 2001 From: Thomas Poepping Date: Thu, 15 Sep 2022 10:11:32 -0700 Subject: [PATCH] Installer update enhancements (#288) * Rename and fix all update scripts. * Add caching to AwsClientBuilderFactory * Fix build script indexing bug for sample app * Update enhancements: intelligent updating, improved architecture, more tests. * Address PR comments Co-authored-by: PoeppingT --- .../{update_service.sh => update.sh} | 0 .../{update_service.sh => update.sh} | 0 .../{update_service.sh => update.sh} | 0 .../{update_service.sh => update.sh} | 0 .../{update_service.sh => update.sh} | 0 .../{update_service.sh => update.sh} | 0 .../{update_service.sh => update.sh} | 0 .../{update_service.sh => update.sh} | 0 installer/pom.xml | 2 +- .../saasfactory/saasboost/Constants.java | 26 + .../saasboost/SaaSBoostInstall.java | 416 ++------------ .../clients/AwsClientBuilderFactory.java | 88 ++- .../saasboost/model/Environment.java | 187 +++++++ .../model/EnvironmentLoadException.java | 27 + .../model/ExistingEnvironmentFactory.java | 196 +++++++ .../saasboost/workflow/AbstractWorkflow.java | 30 + .../saasboost/workflow/UpdateAction.java | 85 +++ .../saasboost/workflow/UpdateWorkflow.java | 522 ++++++++++++++++++ .../saasboost/workflow/Workflow.java | 21 + .../saasboost/SaaSBoostInstallTest.java | 28 - .../clients/AwsClientBuilderFactoryTest.java | 22 + .../clients/MockAwsClientBuilderFactory.java | 99 ++++ .../workflow/UpdateWorkflowTest.java | 156 ++++++ .../{update_layer.sh => update.sh} | 0 .../{update_layer.sh => update.sh} | 0 .../partners/saasfactory/saasboost/Utils.java | 2 +- layers/utils/{update_layer.sh => update.sh} | 6 +- .../lambdas/{update_service.sh => update.sh} | 0 .../app-services-ecr-macro/update.sh | 52 ++ .../custom-resources/cidr-dynamodb/update.sh | 52 ++ .../clear-s3-bucket/update.sh | 52 ++ .../{update_service.sh => update.sh} | 29 +- .../custom-resources/rds-bootstrap/update.sh | 58 ++ .../update.sh} | 34 +- .../custom-resources/redshift-table/update.sh | 58 ++ .../set-instance-protection/update.sh | 52 ++ samples/java/build.sh | 2 +- .../{update_service.sh => update.sh} | 0 .../{update_service.sh => update.sh} | 0 .../{update_service.sh => update.sh} | 0 .../{update_service.sh => update.sh} | 0 .../{update_service.sh => update.sh} | 0 .../{update_service.sh => update.sh} | 0 .../{update_service.sh => update.sh} | 0 44 files changed, 1848 insertions(+), 454 deletions(-) rename functions/core-stack-listener/{update_service.sh => update.sh} (100%) rename functions/ecs-service-update/{update_service.sh => update.sh} (100%) mode change 100644 => 100755 rename functions/ecs-shutdown-services/{update_service.sh => update.sh} (100%) rename functions/ecs-startup-services/{update_service.sh => update.sh} (100%) rename functions/onboarding-app-stack-listener/{update_service.sh => update.sh} (100%) rename functions/onboarding-stack-listener/{update_service.sh => update.sh} (100%) rename functions/system-rest-api-client/{update_service.sh => update.sh} (100%) rename functions/workload-deploy/{update_service.sh => update.sh} (100%) create mode 100644 installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Constants.java create mode 100644 installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/model/Environment.java create mode 100644 installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/model/EnvironmentLoadException.java create mode 100644 installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/model/ExistingEnvironmentFactory.java create mode 100644 installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/workflow/AbstractWorkflow.java create mode 100644 installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/workflow/UpdateAction.java create mode 100644 installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/workflow/UpdateWorkflow.java create mode 100644 installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/workflow/Workflow.java create mode 100644 installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/clients/MockAwsClientBuilderFactory.java create mode 100644 installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/workflow/UpdateWorkflowTest.java rename layers/apigw-helper/{update_layer.sh => update.sh} (100%) rename layers/cloudformation-utils/{update_layer.sh => update.sh} (100%) rename layers/utils/{update_layer.sh => update.sh} (86%) rename metering-billing/lambdas/{update_service.sh => update.sh} (100%) create mode 100755 resources/custom-resources/app-services-ecr-macro/update.sh create mode 100755 resources/custom-resources/cidr-dynamodb/update.sh create mode 100755 resources/custom-resources/clear-s3-bucket/update.sh rename resources/custom-resources/fsx-dns-name/{update_service.sh => update.sh} (71%) create mode 100755 resources/custom-resources/rds-bootstrap/update.sh rename resources/custom-resources/{redshift-table/update_service.sh => rds-options/update.sh} (59%) mode change 100644 => 100755 create mode 100755 resources/custom-resources/redshift-table/update.sh create mode 100755 resources/custom-resources/set-instance-protection/update.sh rename services/metrics-service/{update_service.sh => update.sh} (100%) rename services/onboarding-service/{update_service.sh => update.sh} (100%) rename services/quotas-service/{update_service.sh => update.sh} (100%) rename services/settings-service/{update_service.sh => update.sh} (100%) rename services/tenant-service/{update_service.sh => update.sh} (100%) rename services/tier-service/{update_service.sh => update.sh} (100%) rename services/user-service/{update_service.sh => update.sh} (100%) diff --git a/functions/core-stack-listener/update_service.sh b/functions/core-stack-listener/update.sh similarity index 100% rename from functions/core-stack-listener/update_service.sh rename to functions/core-stack-listener/update.sh diff --git a/functions/ecs-service-update/update_service.sh b/functions/ecs-service-update/update.sh old mode 100644 new mode 100755 similarity index 100% rename from functions/ecs-service-update/update_service.sh rename to functions/ecs-service-update/update.sh diff --git a/functions/ecs-shutdown-services/update_service.sh b/functions/ecs-shutdown-services/update.sh similarity index 100% rename from functions/ecs-shutdown-services/update_service.sh rename to functions/ecs-shutdown-services/update.sh diff --git a/functions/ecs-startup-services/update_service.sh b/functions/ecs-startup-services/update.sh similarity index 100% rename from functions/ecs-startup-services/update_service.sh rename to functions/ecs-startup-services/update.sh diff --git a/functions/onboarding-app-stack-listener/update_service.sh b/functions/onboarding-app-stack-listener/update.sh similarity index 100% rename from functions/onboarding-app-stack-listener/update_service.sh rename to functions/onboarding-app-stack-listener/update.sh diff --git a/functions/onboarding-stack-listener/update_service.sh b/functions/onboarding-stack-listener/update.sh similarity index 100% rename from functions/onboarding-stack-listener/update_service.sh rename to functions/onboarding-stack-listener/update.sh diff --git a/functions/system-rest-api-client/update_service.sh b/functions/system-rest-api-client/update.sh similarity index 100% rename from functions/system-rest-api-client/update_service.sh rename to functions/system-rest-api-client/update.sh diff --git a/functions/workload-deploy/update_service.sh b/functions/workload-deploy/update.sh similarity index 100% rename from functions/workload-deploy/update_service.sh rename to functions/workload-deploy/update.sh diff --git a/installer/pom.xml b/installer/pom.xml index b334ac6a..d0a00b67 100644 --- a/installer/pom.xml +++ b/installer/pom.xml @@ -34,7 +34,7 @@ limitations under the License. - 102 + 82 diff --git a/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Constants.java b/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Constants.java new file mode 100644 index 00000000..e0aceecc --- /dev/null +++ b/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Constants.java @@ -0,0 +1,26 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost; + +import software.amazon.awssdk.core.SdkSystemSetting; +import software.amazon.awssdk.regions.Region; + +public final class Constants { + public static final Region AWS_REGION = Region.of(System.getenv(SdkSystemSetting.AWS_REGION.environmentVariable())); + public static final String OS = System.getProperty("os.name").toLowerCase(); + public static final String VERSION = Utils.version(Constants.class); +} diff --git a/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostInstall.java b/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostInstall.java index 8cc82ded..445af2ab 100644 --- a/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostInstall.java +++ b/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostInstall.java @@ -17,6 +17,11 @@ package com.amazon.aws.partners.saasfactory.saasboost; import com.amazon.aws.partners.saasfactory.saasboost.clients.AwsClientBuilderFactory; +import com.amazon.aws.partners.saasfactory.saasboost.model.Environment; +import com.amazon.aws.partners.saasfactory.saasboost.model.EnvironmentLoadException; +import com.amazon.aws.partners.saasfactory.saasboost.model.ExistingEnvironmentFactory; +import com.amazon.aws.partners.saasfactory.saasboost.workflow.UpdateWorkflow; +import com.amazon.aws.partners.saasfactory.saasboost.workflow.Workflow; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import org.slf4j.Logger; @@ -67,6 +72,9 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import static com.amazon.aws.partners.saasfactory.saasboost.Constants.AWS_REGION; +import static com.amazon.aws.partners.saasfactory.saasboost.Constants.OS; +import static com.amazon.aws.partners.saasfactory.saasboost.Constants.VERSION; import static com.amazon.aws.partners.saasfactory.saasboost.Utils.isBlank; import static com.amazon.aws.partners.saasfactory.saasboost.Utils.isEmpty; import static com.amazon.aws.partners.saasfactory.saasboost.Utils.isNotBlank; @@ -75,9 +83,6 @@ public class SaaSBoostInstall { private static final Logger LOGGER = LoggerFactory.getLogger(SaaSBoostInstall.class); - private static final Region AWS_REGION = Region.of(System.getenv(SdkSystemSetting.AWS_REGION.environmentVariable())); - private static final String OS = System.getProperty("os.name").toLowerCase(); - private static final String VERSION = getVersionInfo(); private final AwsClientBuilderFactory awsClientBuilderFactory; private final ApiGatewayClient apigw; @@ -92,6 +97,7 @@ public class SaaSBoostInstall { private final SecretsManagerClient secretsManager; private final String accountId; + private Environment environment; private String envName; private Path workingDir; private SaaSBoostArtifactsBucket saasBoostArtifactsBucket; @@ -215,12 +221,14 @@ public void start(String existingBucket) { this.workingDir = getWorkingDirectory(); } + Workflow workflow = null; + switch (installOption) { case INSTALL: installSaaSBoost(existingBucket); break; case UPDATE: - updateSaaSBoost(); + workflow = new UpdateWorkflow(this.workingDir, this.environment, this.awsClientBuilderFactory); break; case UPDATE_WEB_APP: updateWebApp(); @@ -244,6 +252,11 @@ public void start(String existingBucket) { // debug(); // break; } + + if (workflow != null) { + workflow.run(); + System.exit(workflow.getExitCode()); + } } protected void installSaaSBoost(String existingBucket) { @@ -399,13 +412,15 @@ protected void installSaaSBoost(String existingBucket) { // project and have CloudFormation own building/copying the web files to S3. // Wait for completion and then build web app outputMessage("Build website and upload to S3"); - final String webUrl = buildAndCopyWebApp(); + final String webUrl = buildAndCopyWebApp(this.workingDir, cfn, s3, this.envName, this.accountId); if (useAnalyticsModule) { LOGGER.info("Install metrics and analytics module"); // The analytics module stack reads baseStackDetails for its CloudFormation template parameters // because we're not yet creating the analytics resources as a nested child stack of the main stack - this.baseStackDetails = getExistingSaaSBoostStackDetails(); + this.environment = ExistingEnvironmentFactory.findExistingEnvironment( + ssm, cfn, this.envName, this.accountId); + this.baseStackDetails = environment.getBaseCloudFormationStackInfo(); installAnalyticsModule(); } @@ -414,100 +429,6 @@ protected void installSaaSBoost(String existingBucket) { outputMessage("AWS SaaS Boost Console URL is: " + webUrl); } - protected void updateSaaSBoost() { - LOGGER.info("Perform Update of AWS SaaS Boost deployment"); - outputMessage("******* W A R N I N G *******"); - outputMessage("Updating AWS SaaS Boost environment is an IRREVERSIBLE operation. You should test an " - + "updated install in a non-production environment\n" - + "before updating a production environment. By continuing you understand and ACCEPT the RISKS!"); - System.out.print("Enter y to continue with UPDATE of " + stackName + " or n to CANCEL: "); - boolean continueUpgrade = Keyboard.readBoolean(); - if (!continueUpgrade) { - outputMessage("Canceled UPDATE of AWS SaaS Boost environment"); - System.exit(2); - } else { - outputMessage("Continuing UPDATE of AWS SaaS Boost stack " + stackName); - } - - // First, upload the (potentially) modified CloudFormation templates up to S3 - outputMessage("Copy CloudFormation template files to S3 artifacts bucket " + saasBoostArtifactsBucket); - copyTemplateFilesToS3(); - - // Grab the current Lambda folder. We are going to upload the (potentially) modified Lambda functions to a - // different S3 folder as a way to force CloudFormation to update the function resources. After we copy the - // function code up to S3 in the new folder, we can delete the existing one to save space/money on S3. - final String existingLambdaSourceFolder = this.lambdaSourceFolder; - - // Now create a new S3 folder for the Lambda functions so that CloudFormation sees a change that will - // trigger an update function call to the Lambda service. - this.lambdaSourceFolder = "lambdas-" + DateTimeFormatter.ISO_LOCAL_DATE.format(LocalDate.now()); - outputMessage("Updating Lambda folder to " + this.lambdaSourceFolder); - outputMessage("Compiling Lambda functions and uploading to S3 artifacts bucket. This will take some time..."); - processLambdas(); - - // Update the analytics stack if needed - if (useAnalyticsModule) { - outputMessage("Update the Metrics and Analytics stack..."); - updateMetricsStack(); - } - - // Get values for all the CloudFormation parameters including possibly new ones in the template file on disk - Path cloudFormationTemplate = workingDir.resolve(Path.of("resources", "saas-boost.yaml")); - if (!Files.exists(cloudFormationTemplate)) { - outputMessage("Unable to find file " + cloudFormationTemplate.toString()); - System.exit(2); - } - Map cloudFormationParamMap = getCloudFormationParameterMap(cloudFormationTemplate, this.baseStackDetails); - - // Update the Lambda source folder to deploy updated code - outputMessage("Updating LambdaSourceFolder parameter to " + this.lambdaSourceFolder); - cloudFormationParamMap.put("LambdaSourceFolder", this.lambdaSourceFolder); - - // Update the version number - outputMessage("Updating Version parameter to " + VERSION); - cloudFormationParamMap.put("Version", VERSION); - - // Now call update stack - outputMessage("Executing CloudFormation update stack on: " + this.stackName); - updateCloudFormationStack(this.stackName, cloudFormationParamMap, "saas-boost.yaml"); - - // CloudFormation will not redeploy an API Gateway stage on update - outputMessage("Updating API Gateway deployment for stages"); - try { - String publicApiName = "sb-public-api-" + this.envName; - String privateApiName = "sb-private-api-" + this.envName; - GetRestApisResponse response = apigw.getRestApis(); - if (response.hasItems()) { - for (RestApi api : response.items()) { - String apiName = api.name(); - boolean isPublicApi = publicApiName.equals(apiName); - boolean isPrivateApi = privateApiName.equals(apiName); - if (isPublicApi || isPrivateApi) { - String stage = isPublicApi ? cloudFormationParamMap.get("PublicApiStage") : cloudFormationParamMap.get("PrivateApiStage"); - outputMessage("Updating API Gateway deployment for " + apiName + " to stage: " + stage); - apigw.createDeployment(request -> request - .restApiId(api.id()) - .stageName(stage) - ); - } - } - } - } catch (SdkServiceException apigwError) { - LOGGER.error("apigateway error", apigwError); - LOGGER.error(getFullStackTrace(apigwError)); - throw apigwError; - } - - //build and copy the web site - buildAndCopyWebApp(); - - // Delete the old lambdas zip files - outputMessage("Delete files from previous Lambda folder: " + existingLambdaSourceFolder); - cleanUpS3(saasBoostArtifactsBucket.getBucketName(), existingLambdaSourceFolder); - - outputMessage("Update of SaaS Boost environment " + this.envName + " complete."); - } - protected void deleteSaasBoostInstallation() { // Confirm delete outputMessage("****** W A R N I N G"); @@ -577,7 +498,7 @@ protected void deleteSaasBoostInstallation() { // Finally, remove the S3 artifacts bucket that this installer created outside of CloudFormation LOGGER.info("Clean up s3 bucket: " + saasBoostArtifactsBucket); - cleanUpS3(saasBoostArtifactsBucket.getBucketName(), null); + cleanUpS3(s3, saasBoostArtifactsBucket.getBucketName(), null); s3.deleteBucket(r -> r.bucket(saasBoostArtifactsBucket.getBucketName())); // This installer also creates some Parameter Store entries outside of CloudFormation which are @@ -783,7 +704,7 @@ protected void installAnalyticsModule() { protected void updateWebApp() { LOGGER.info("Perform Update of the Web Application for AWS SaaS Boost"); outputMessage("Build web app and copy files to S3 web bucket"); - String webUrl = buildAndCopyWebApp(); + String webUrl = buildAndCopyWebApp(this.workingDir, cfn, s3, this.envName, this.accountId); outputMessage("AWS SaaS Boost Console URL is: " + webUrl); } @@ -869,89 +790,6 @@ protected void getQuickSightUsername() { } } - protected void updateMetricsStack() { - String analyticsStackName = analyticsStackName(); - // Load up the existing parameters from CloudFormation - Map stackParamsMap = new LinkedHashMap<>(); - try { - DescribeStacksResponse response = cfn.describeStacks(request -> request.stackName(analyticsStackName)); - if (response.hasStacks() && !response.stacks().isEmpty()) { - Stack stack = response.stacks().get(0); - stackParamsMap = stack.parameters().stream() - .collect(Collectors.toMap(Parameter::parameterKey, Parameter::parameterValue)); - } - } catch (SdkServiceException cfnError) { - if (cfnError.getMessage().contains("does not exist")) { - outputMessage("Analytics module CloudFormation stack " + analyticsStackName + " not found."); - System.exit(2); - } - LOGGER.error("cloudformation:DescribeStacks error", cfnError); - LOGGER.error(getFullStackTrace(cfnError)); - throw cfnError; - } - // Update the parameter values if necessary to match the template file on disk - Path cloudFormationTemplate = workingDir.resolve(Path.of("resources", "saas-boost-metrics-analytics.yaml")); - if (!Files.exists(cloudFormationTemplate)) { - outputMessage("Unable to find file " + cloudFormationTemplate.toString()); - System.exit(2); - } - Map cloudFormationParamMap = getCloudFormationParameterMap(cloudFormationTemplate, stackParamsMap); - updateCloudFormationStack(stackName, cloudFormationParamMap, "saas-boost-metrics-analytics.yaml"); - } - - protected static Map getCloudFormationParameterMap(Path cloudFormationTemplateFile, Map stackParamsMap) { - // Open CFN template yaml file and prompt for values of params that are not in the existing stack - LOGGER.info("Building map of parameters for template " + cloudFormationTemplateFile); - Map cloudFormationParamMap = new LinkedHashMap<>(); - - ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); - try (InputStream cloudFormationTemplate = Files.newInputStream(cloudFormationTemplateFile)) { - LinkedHashMap template = mapper.readValue(cloudFormationTemplate, LinkedHashMap.class); - LinkedHashMap> parameters = (LinkedHashMap>) template.get("Parameters"); - for (Map.Entry> parameter : parameters.entrySet()) { - String parameterKey = parameter.getKey(); - LinkedHashMap parameterProperties = (LinkedHashMap) parameter.getValue(); - - // For each parameter in the template file, set the value to any existing value - // otherwise prompt the user to set the value. - Object existingParameter = stackParamsMap.get(parameterKey); - if (existingParameter != null) { - // We're running an update. Start with reusing the current value for this parameter. - // The calling code can override this parameter's value before executing update stack. - LOGGER.info("Reuse existing value for parameter {} => {}", parameterKey, existingParameter); - cloudFormationParamMap.put(parameterKey, stackParamsMap.get(parameterKey)); - } else { - // This is a new parameter added to the template file on disk. Prompt the user for a value. - Object defaultValue = parameterProperties.get("Default"); - String parameterType = (String) parameterProperties.get("Type"); - System.out.print("Enter a " + parameterType + " value for parameter " + parameterKey); - if (defaultValue != null) { - // No default value for this property - System.out.print(". (Press Enter for '" + defaultValue + "'): "); - } else { - System.out.print(": "); - } - String enteredValue = Keyboard.readString(); - if (isEmpty(enteredValue) && defaultValue != null) { - cloudFormationParamMap.put(parameterKey, String.valueOf(defaultValue)); - LOGGER.info("Using default value for parameter {} => {}", parameterKey, cloudFormationParamMap.get(parameterKey)); - } else if (isEmpty(enteredValue) && defaultValue == null) { - cloudFormationParamMap.put(parameterKey, ""); - LOGGER.info("Using entered value for parameter {} => {}", parameterKey, cloudFormationParamMap.get(parameterKey)); - } else { - cloudFormationParamMap.put(parameterKey, enteredValue); - LOGGER.info("Using entered value for parameter {} => {}", parameterKey, cloudFormationParamMap.get(parameterKey)); - } - } - } - } catch (IOException ioe) { - LOGGER.error("Error parsing YAML file from path", ioe); - LOGGER.error(getFullStackTrace(ioe)); - throw new RuntimeException(ioe); - } - return cloudFormationParamMap; - } - protected List> getProvisionedTenants() { List> provisionedTenants = new ArrayList<>(); Map systemApiRequest = new HashMap<>(); @@ -1341,7 +1179,7 @@ protected static void printResults(Process process) { } } - protected static void outputMessage(String msg) { + public static void outputMessage(String msg) { LOGGER.info(msg); System.out.println(msg); } @@ -1378,11 +1216,18 @@ protected static void checkEnvironment() { protected void loadExistingSaaSBoostEnvironment() { this.envName = getExistingSaaSBoostEnvironment(); - this.saasBoostArtifactsBucket = getExistingSaaSBoostArtifactBucket(); - this.lambdaSourceFolder = getExistingSaaSBoostLambdasFolder(); - this.stackName = getExistingSaaSBoostStackName(); - this.baseStackDetails = getExistingSaaSBoostStackDetails(); - this.useAnalyticsModule = getExistingSaaSBoostAnalyticsDeployed(); + try { + this.environment = ExistingEnvironmentFactory.findExistingEnvironment(ssm, cfn, envName, accountId); + } catch (EnvironmentLoadException ele) { + outputMessage("Failed to load existing SaaS Boost Environment: " + ele.getMessage()); + LOGGER.error(Utils.getFullStackTrace(ele)); + System.exit(2); + } + this.saasBoostArtifactsBucket = environment.getArtifactsBucket(); + this.lambdaSourceFolder = environment.getLambdasFolderName(); + this.stackName = environment.getBaseCloudFormationStackName(); + this.baseStackDetails = environment.getBaseCloudFormationStackInfo(); + this.useAnalyticsModule = environment.isMetricsAnalyticsDeployed(); } protected String getExistingSaaSBoostEnvironment() { @@ -1423,129 +1268,6 @@ protected static boolean validateEnvironmentName(String envName) { return valid; } - protected SaaSBoostArtifactsBucket getExistingSaaSBoostArtifactBucket() { - LOGGER.info("Getting existing SaaS Boost artifact bucket name from Parameter Store"); - String artifactsBucket = null; - if (isBlank(this.envName)) { - this.envName = getExistingSaaSBoostEnvironment(); - } - try { - GetParameterResponse response = ssm.getParameter(request -> request - .name("/saas-boost/" + this.envName + "/SAAS_BOOST_BUCKET") - ); - artifactsBucket = response.parameter().value(); - } catch (SdkServiceException ssmError) { - LOGGER.error("ssm:GetParameter error {}", ssmError.getMessage()); - LOGGER.error(getFullStackTrace(ssmError)); - throw ssmError; - } - LOGGER.info("Loaded artifacts bucket {}", artifactsBucket); - return new SaaSBoostArtifactsBucket(artifactsBucket, AWS_REGION); - } - - protected String getExistingSaaSBoostStackName() { - LOGGER.info("Getting existing SaaS Boost CloudFormation stack name from Parameter Store"); - String stackName = null; - if (isBlank(this.envName)) { - this.envName = getExistingSaaSBoostEnvironment(); - } - try { - GetParameterResponse response = ssm.getParameter(request -> request - .name("/saas-boost/" + this.envName + "/SAAS_BOOST_STACK") - ); - stackName = response.parameter().value(); - } catch (ParameterNotFoundException paramStoreError) { - LOGGER.warn("Parameter /saas-boost/" + this.envName + "/SAAS_BOOST_STACK not found setting to default 'sb-" + this.envName + "'"); - stackName = "sb-" + this.envName; - } catch (SdkServiceException ssmError) { - LOGGER.error("ssm:GetParameter error {}", ssmError.getMessage()); - LOGGER.error(getFullStackTrace(ssmError)); - throw ssmError; - } - LOGGER.info("Loaded stack name {}", stackName); - return stackName; - } - - protected String getExistingSaaSBoostLambdasFolder() { - LOGGER.info("Getting existing SaaS Boost Lambdas folder from Parameter Store"); - String lambdasFolder = null; - if (isBlank(this.envName)) { - this.envName = getExistingSaaSBoostEnvironment(); - } - try { - GetParameterResponse response = ssm.getParameter(request -> request - .name("/saas-boost/" + this.envName + "/SAAS_BOOST_LAMBDAS_FOLDER") - ); - lambdasFolder = response.parameter().value(); - } catch (ParameterNotFoundException paramStoreError) { - LOGGER.warn("Parameter /saas-boost/" + this.envName + "/SAAS_BOOST_LAMBDAS_FOLDER not found setting to default 'lambdas'"); - lambdasFolder = "lambdas"; - } catch (SdkServiceException ssmError) { - LOGGER.error("ssm:GetParameter error {}", ssmError.getMessage()); - LOGGER.error(getFullStackTrace(ssmError)); - throw ssmError; - } - LOGGER.info("Loaded Lambdas folder {}", lambdasFolder); - return lambdasFolder; - } - - protected boolean getExistingSaaSBoostAnalyticsDeployed() { - LOGGER.info("Getting existing SaaS Boost Analytics module deployed from Parameter Store"); - boolean analyticsDeployed = false; - if (isBlank(this.envName)) { - this.envName = getExistingSaaSBoostEnvironment(); - } - try { - GetParameterResponse response = ssm.getParameter(request -> request - .name("/saas-boost/" + this.envName + "/METRICS_ANALYTICS_DEPLOYED") - ); - analyticsDeployed = Boolean.parseBoolean(response.parameter().value()); - } catch (SdkServiceException ssmError) { - // TODO CloudFormation should own this parameter, not the installer... it's possible the parameter doesn't exist - // parameter not found is an exception - if (!ssmError.getMessage().contains("not found")) { - LOGGER.error("ssm:GetParameter error {}", ssmError.getMessage()); - LOGGER.error(getFullStackTrace(ssmError)); - throw ssmError; - } - } - LOGGER.info("Loaded analytics deployed {}", analyticsDeployed); - return analyticsDeployed; - } - - protected Map getExistingSaaSBoostStackDetails() { - LOGGER.info("Getting CloudFormation stack details for SaaS Boost stack {}", this.stackName); - Map details = new HashMap<>(); - Collection requiredOutputs = List.of("PublicSubnet1", "PublicSubnet2", "PrivateSubnet1", "PrivateSubnet2", "EgressVpc", "LoggingBucket"); - try { - DescribeStacksResponse response = cfn.describeStacks(request -> request.stackName(this.stackName)); - if (response.hasStacks() && !response.stacks().isEmpty()) { - Stack stack = response.stacks().get(0); - Map outputs = stack.outputs().stream() - .filter(output -> requiredOutputs.contains(output.outputKey())) // TODO Should we just capture them all? - .collect(Collectors.toMap(Output::outputKey, Output::outputValue)); - for (String requiredOutput : requiredOutputs) { - if (!outputs.containsKey(requiredOutput)) { - outputMessage("Missing required CloudFormation stack output " + requiredOutput + " from stack " + this.stackName); - System.exit(2); - } else { - LOGGER.info("Loaded required stack output {} -> {}", requiredOutput, outputs.get(requiredOutput)); - } - } - Map parameters = stack.parameters().stream() - .collect(Collectors.toMap(Parameter::parameterKey, Parameter::parameterValue)); - - details.putAll(parameters); - details.putAll(outputs); - } - } catch (SdkServiceException cfnError) { - LOGGER.error("cloudformation:DescribeStacks error", cfnError); - LOGGER.error(getFullStackTrace(cfnError)); - throw cfnError; - } - return details; - } - protected void processLambdas() { try { List sourceDirectories = new ArrayList<>(); @@ -1661,55 +1383,6 @@ protected void createSaaSBoostStack(final String stackName, String adminEmail, B } } - protected void updateCloudFormationStack(final String stackName, final Map paramsMap, String yamlFile) { - List templateParameters = paramsMap.entrySet().stream() - .map(entry -> Parameter.builder().parameterKey(entry.getKey()).parameterValue(entry.getValue()).build()) - .collect(Collectors.toList()); - - LOGGER.info("Executing CloudFormation update stack for " + stackName); - try { - UpdateStackResponse updateStackResponse = cfn.updateStack(UpdateStackRequest.builder() - .stackName(stackName) - .capabilitiesWithStrings("CAPABILITY_NAMED_IAM", "CAPABILITY_AUTO_EXPAND") - .templateURL(saasBoostArtifactsBucket.getBucketUrl() + yamlFile) - .parameters(templateParameters) - .build() - ); - String stackId = updateStackResponse.stackId(); - LOGGER.info("Waiting for update stack to complete for " + stackId); - long sleepTime = 3L; - while (true) { - DescribeStacksResponse response = cfn.describeStacks(request -> request.stackName(stackId)); - Stack stack = response.stacks().get(0); - String stackStatus = stack.stackStatusAsString(); - if ("UPDATE_COMPLETE".equalsIgnoreCase(stackStatus)) { - outputMessage("CloudFormation stack: " + stackName + " updated successfully."); - break; - } else if ("UPDATE_ROLLBACK_COMPLETE".equalsIgnoreCase(stackStatus)) { - outputMessage("CloudFormation stack: " + stackName + " update failed."); - throw new RuntimeException("Error with CloudFormation stack " + stackName + ". Check the events in the AWS CloudFormation Console"); - } else { - // TODO should we set an upper bound on this loop? - outputMessage("Awaiting Update of CloudFormation Stack " + stackName + " to complete. Sleep " + sleepTime + " minute(s)..."); - try { - Thread.sleep(sleepTime * 60 * 1000); - } catch (Exception e) { - LOGGER.error("Error pausing thread", e); - } - sleepTime = 1L; //set to 1 minute after kick off of 5 minute - } - } - } catch (SdkServiceException cfnError) { - if (cfnError.getMessage().contains("No updates are to be performed")) { - outputMessage("No Updates to be performed for Stack: " + stackName); - } else { - LOGGER.error("updateCloudFormationStack::update stack failed {}", cfnError.getMessage()); - LOGGER.error(getFullStackTrace(cfnError)); - throw cfnError; - } - } - } - protected void createMetricsStack(final String stackName, final String dbPasswordSsmParameter, final String databaseName) { LOGGER.info("Creating CloudFormation stack {} with database name {}", stackName, databaseName); List templateParameters = new ArrayList<>(); @@ -1847,7 +1520,12 @@ protected boolean checkCloudFormationStack(final String stackName) { return exists; } - protected String buildAndCopyWebApp() { + public static String buildAndCopyWebApp( + Path workingDir, + CloudFormationClient cfn, + S3Client s3, + String envName, + String accountId) { Path webDir = workingDir.resolve(Path.of("client", "web")); if (!Files.isDirectory(webDir)) { outputMessage("Error, can't find client/web directory at " + webDir.toAbsolutePath().toString()); @@ -1943,7 +1621,7 @@ protected String buildAndCopyWebApp() { // Sync files to the web bucket outputMessage("Synchronizing AWS SaaS Boost web application files to s3 web bucket"); // First, clear out any files that are currently in the web bucket - cleanUpS3(webBucket, ""); + cleanUpS3(s3, webBucket, ""); String cacheControl = null; Path yarnBuildDir = webDir.resolve(Path.of("build")); List filesToUpload; @@ -1982,7 +1660,7 @@ protected String buildAndCopyWebApp() { return webUrl; } - protected static void executeCommand(String command, String[] environment, File dir) { + public static void executeCommand(String command, String[] environment, File dir) { LOGGER.info("Executing Commands: " + command); if (null != dir) { LOGGER.info("Directory: " + dir.getPath()); @@ -2052,7 +1730,7 @@ protected String analyticsStackName() { return this.stackName + "-analytics"; } - protected void cleanUpS3(String bucket, String prefix) { + protected static void cleanUpS3(S3Client s3, String bucket, String prefix) { // The list of objects in the bucket to delete List toDelete = new ArrayList<>(); if (isNotEmpty(prefix) && !prefix.endsWith("/")) { @@ -2198,10 +1876,6 @@ public static String getFullStackTrace(Exception e) { return sw.getBuffer().toString(); } - public static String getVersionInfo() { - return Utils.version(SaaSBoostInstall.class); - } - public static boolean isWindows() { return (OS.contains("win")); } diff --git a/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/clients/AwsClientBuilderFactory.java b/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/clients/AwsClientBuilderFactory.java index 38d913f6..dde33542 100644 --- a/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/clients/AwsClientBuilderFactory.java +++ b/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/clients/AwsClientBuilderFactory.java @@ -54,6 +54,23 @@ public class AwsClientBuilderFactory { private final Region awsRegion; private final AwsCredentialsProvider credentialsProvider; + private ApiGatewayClientBuilder cachedApiGatewayBuilder; + private CloudFormationClientBuilder cachedCloudFormationBuilder; + private EcrClientBuilder cachedEcrBuilder; + private IamClientBuilder cachedIamBuilder; + private LambdaClientBuilder cachedLambdaBuilder; + private QuickSightClientBuilder cachedQuickSightBuilder; + private S3ClientBuilder cachedS3Builder; + private SsmClientBuilder cachedSsmBuilder; + private StsClientBuilder cachedStsBuilder; + private SecretsManagerClientBuilder cachedSecretsManagerClientBuilder; + + AwsClientBuilderFactory() { + // for testing + this.awsRegion = null; + this.credentialsProvider = null; + } + private AwsClientBuilderFactory(Builder builder) { // passing no region or a null region to any of the AWS Client Builders // leads to the default region from the configured profile being used @@ -71,54 +88,85 @@ > B decorateBuilderWithDef } public ApiGatewayClientBuilder apiGatewayBuilder() { - // override throttling policy to wait 5 seconds if we're throttled on CreateDeployment - // https://docs.aws.amazon.com/apigateway/latest/developerguide/limits.html - return decorateBuilderWithDefaults(ApiGatewayClient.builder()) + if (cachedApiGatewayBuilder == null) { + // override throttling policy to wait 5 seconds if we're throttled on CreateDeployment + // https://docs.aws.amazon.com/apigateway/latest/developerguide/limits.html + cachedApiGatewayBuilder = decorateBuilderWithDefaults(ApiGatewayClient.builder()) .overrideConfiguration(config -> config.retryPolicy(AwsRetryPolicy.addRetryConditions( - RetryPolicy.builder().throttlingBackoffStrategy(retryPolicyContext -> { - if (retryPolicyContext.originalRequest() instanceof CreateDeploymentRequest) { - return Duration.ofSeconds(5); - } - return null; - }).build()))); + RetryPolicy.builder().throttlingBackoffStrategy(retryPolicyContext -> { + if (retryPolicyContext.originalRequest() instanceof CreateDeploymentRequest) { + return Duration.ofSeconds(5); + } + return null; + }).build()))); + } + + return cachedApiGatewayBuilder; } public CloudFormationClientBuilder cloudFormationBuilder() { - return decorateBuilderWithDefaults(CloudFormationClient.builder()); + if (cachedCloudFormationBuilder == null) { + cachedCloudFormationBuilder = decorateBuilderWithDefaults(CloudFormationClient.builder()); + } + return cachedCloudFormationBuilder; } public EcrClientBuilder ecrBuilder() { - return decorateBuilderWithDefaults(EcrClient.builder()); + if (cachedEcrBuilder == null) { + cachedEcrBuilder = decorateBuilderWithDefaults(EcrClient.builder()); + } + return cachedEcrBuilder; } public IamClientBuilder iamBuilder() { - // IAM is not regionalized: all endpoints except us-gov and aws-cn use the AWS_GLOBAL region - // ref: https://docs.aws.amazon.com/general/latest/gr/iam-service.html - return decorateBuilderWithDefaults(IamClient.builder()).region(Region.AWS_GLOBAL); + if (cachedIamBuilder == null) { + // IAM is not regionalized: all endpoints except us-gov and aws-cn use the AWS_GLOBAL region + // ref: https://docs.aws.amazon.com/general/latest/gr/iam-service.html + cachedIamBuilder = decorateBuilderWithDefaults(IamClient.builder()).region(Region.AWS_GLOBAL); + } + return cachedIamBuilder; } public LambdaClientBuilder lambdaBuilder() { - return decorateBuilderWithDefaults(LambdaClient.builder()); + if (cachedLambdaBuilder == null) { + cachedLambdaBuilder = decorateBuilderWithDefaults(LambdaClient.builder()); + } + return cachedLambdaBuilder; } public QuickSightClientBuilder quickSightBuilder() { - return decorateBuilderWithDefaults(QuickSightClient.builder()); + if (cachedQuickSightBuilder == null) { + cachedQuickSightBuilder = decorateBuilderWithDefaults(QuickSightClient.builder()); + } + return cachedQuickSightBuilder; } public S3ClientBuilder s3Builder() { - return decorateBuilderWithDefaults(S3Client.builder()); + if (cachedS3Builder == null) { + cachedS3Builder = decorateBuilderWithDefaults(S3Client.builder()); + } + return cachedS3Builder; } public SsmClientBuilder ssmBuilder() { - return decorateBuilderWithDefaults(SsmClient.builder()); + if (cachedSsmBuilder == null) { + cachedSsmBuilder = decorateBuilderWithDefaults(SsmClient.builder()); + } + return cachedSsmBuilder; } public StsClientBuilder stsBuilder() { - return decorateBuilderWithDefaults(StsClient.builder()); + if (cachedStsBuilder == null) { + cachedStsBuilder = decorateBuilderWithDefaults(StsClient.builder()); + } + return cachedStsBuilder; } public SecretsManagerClientBuilder secretsManagerBuilder() { - return decorateBuilderWithDefaults(SecretsManagerClient.builder()); + if (cachedSecretsManagerClientBuilder == null) { + cachedSecretsManagerClientBuilder = decorateBuilderWithDefaults(SecretsManagerClient.builder()); + } + return cachedSecretsManagerClientBuilder; } public static Builder builder() { diff --git a/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/model/Environment.java b/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/model/Environment.java new file mode 100644 index 00000000..a8c81db1 --- /dev/null +++ b/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/model/Environment.java @@ -0,0 +1,187 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost.model; + +import com.amazon.aws.partners.saasfactory.saasboost.SaaSBoostArtifactsBucket; + +import java.util.Map; + +/** + * The Environment class represents all gathered information about a SaaS Boost environment. + * + * Objects for existing environments should be created using {@link ExistingEnvironmentFactory#create}. + * + * New Environments can be created using {@link Environment.Builder#build}. Instances of {@link Environment.Builder} + * can be instantiated via the {@link Environment#builder()} static function. + */ +public final class Environment { + private String name; + private String accountId; + private SaaSBoostArtifactsBucket artifactsBucket; + private String lambdasFolderName; + private String baseCloudFormationStackName; + private Map baseCloudFormationStackInfo; + private boolean metricsAnalyticsDeployed; + + private Environment(Builder b) { + this.name = b.name; + this.accountId = b.accountId; + this.artifactsBucket = b.artifactsBucket; + this.lambdasFolderName = b.lambdasFolderName; + this.baseCloudFormationStackName = b.baseCloudFormationStackName; + this.baseCloudFormationStackInfo = b.baseCloudFormationStackInfo; + this.metricsAnalyticsDeployed = b.metricsAnalyticsDeployed; + } + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public String getAccountId() { + return this.accountId; + } + + public void setAccountId(String accountId) { + this.accountId = accountId; + } + + public SaaSBoostArtifactsBucket getArtifactsBucket() { + return this.artifactsBucket; + } + + public void setArtifactsBucket(SaaSBoostArtifactsBucket artifactsBucket) { + this.artifactsBucket = artifactsBucket; + } + + public String getLambdasFolderName() { + return this.lambdasFolderName; + } + + public void setLambdasFolderName(String lambdasFolderName) { + this.lambdasFolderName = lambdasFolderName; + } + + public String getBaseCloudFormationStackName() { + return this.baseCloudFormationStackName; + } + + public void setBaseCloudFormationStackName(String baseCloudFormationStackName) { + this.baseCloudFormationStackName = baseCloudFormationStackName; + } + + public Map getBaseCloudFormationStackInfo() { + return this.baseCloudFormationStackInfo; + } + + public void setBaseCloudFormationStackInfo(Map baseCloudFormationStackInfo) { + this.baseCloudFormationStackInfo = baseCloudFormationStackInfo; + } + + public boolean isMetricsAnalyticsDeployed() { + return this.metricsAnalyticsDeployed; + } + + public void setMetricsAnalyticsDeployed(boolean metricsAnalyticsDeployed) { + this.metricsAnalyticsDeployed = metricsAnalyticsDeployed; + } + + /** + * Retrieves a new empty instance of the {@link Environment.Builder}. + * + * @return an empty {@link Environment.Builder}. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Constructs a {@link Environment.Builder} using the provided {@link Environment} as a baseline. + * + * It will always be true that calling build on the {@link Environment.Builder} returned + * by this function will create an {@link Environment} equal to the provided {@link Environment}. In other words: + * + * myEnvironment.equals(Environment.builder(myEnvironment).build()) // <== always true + * + * + * @param baseEnvironment the {@link Environment} to base this {@link Environment.Builder} on. + * @return the prebuilt {@link Environment.Builder} + */ + public static Builder builder(Environment baseEnvironment) { + return new Builder(); + } + + /** + * A convenient `Builder` class for {@link Environment}. + */ + public static final class Builder { + + private String name; + private String accountId; + private SaaSBoostArtifactsBucket artifactsBucket; + private String lambdasFolderName; + private String baseCloudFormationStackName; + private Map baseCloudFormationStackInfo; + private boolean metricsAnalyticsDeployed; + + private Builder() { + + } + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder accountId(String accountId) { + this.accountId = accountId; + return this; + } + + public Builder artifactsBucket(SaaSBoostArtifactsBucket artifactsBucket) { + this.artifactsBucket = artifactsBucket; + return this; + } + + public Builder lambdasFolderName(String lambdasFolderName) { + this.lambdasFolderName = lambdasFolderName; + return this; + } + + public Builder baseCloudFormationStackName(String baseCloudFormationStackName) { + this.baseCloudFormationStackName = baseCloudFormationStackName; + return this; + } + + public Builder baseCloudFormationStackInfo(Map baseCloudFormationStackInfo) { + this.baseCloudFormationStackInfo = baseCloudFormationStackInfo; + return this; + } + + public Builder metricsAnalyticsDeployed(boolean metricsAnalyticsDeployed) { + this.metricsAnalyticsDeployed = metricsAnalyticsDeployed; + return this; + } + + public Environment build() { + return new Environment(this); + } + } +} diff --git a/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/model/EnvironmentLoadException.java b/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/model/EnvironmentLoadException.java new file mode 100644 index 00000000..101b26c7 --- /dev/null +++ b/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/model/EnvironmentLoadException.java @@ -0,0 +1,27 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost.model; + +/** + * A {@link RuntimeException} indicating that the specified {@link Environment} was unable + * to be loaded. + */ +public final class EnvironmentLoadException extends RuntimeException { + public EnvironmentLoadException(String message) { + super(message); + } +} diff --git a/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/model/ExistingEnvironmentFactory.java b/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/model/ExistingEnvironmentFactory.java new file mode 100644 index 00000000..e312a8d2 --- /dev/null +++ b/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/model/ExistingEnvironmentFactory.java @@ -0,0 +1,196 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost.model; + +import com.amazon.aws.partners.saasfactory.saasboost.Constants; +import com.amazon.aws.partners.saasfactory.saasboost.SaaSBoostArtifactsBucket; +import com.amazon.aws.partners.saasfactory.saasboost.Utils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.awssdk.core.exception.SdkServiceException; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.cloudformation.CloudFormationClient; +import software.amazon.awssdk.services.cloudformation.model.DescribeStacksResponse; +import software.amazon.awssdk.services.cloudformation.model.Output; +import software.amazon.awssdk.services.cloudformation.model.Parameter; +import software.amazon.awssdk.services.cloudformation.model.Stack; +import software.amazon.awssdk.services.ssm.SsmClient; +import software.amazon.awssdk.services.ssm.model.GetParameterResponse; +import software.amazon.awssdk.services.ssm.model.ParameterNotFoundException; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public final class ExistingEnvironmentFactory { + private static final Logger LOGGER = LoggerFactory.getLogger(ExistingEnvironmentFactory.class); + + public static Environment findExistingEnvironment( + SsmClient ssm, + CloudFormationClient cfn, + String environmentName, + String accountId) { + if (Utils.isBlank(environmentName)) { + throw new EnvironmentLoadException("EnvironmentName cannot be blank."); + } + + String baseCloudFormationStackName = getExistingSaaSBoostStackName(ssm, environmentName); + + return Environment.builder() + .artifactsBucket(getExistingSaaSBoostArtifactBucket(ssm, environmentName, Constants.AWS_REGION)) + .baseCloudFormationStackName(baseCloudFormationStackName) + .baseCloudFormationStackInfo(getExistingSaaSBoostStackDetails(cfn, baseCloudFormationStackName)) + .lambdasFolderName(getExistingSaaSBoostLambdasFolder(ssm, environmentName)) + .metricsAnalyticsDeployed(getExistingSaaSBoostAnalyticsDeployed(ssm, environmentName)) + .name(environmentName) + .accountId(accountId) + .build(); + } + + // VisibleForTesting + static SaaSBoostArtifactsBucket getExistingSaaSBoostArtifactBucket( + SsmClient ssm, + String environmentName, + Region region) { + LOGGER.debug("Getting existing SaaS Boost artifact bucket name from Parameter Store"); + String artifactsBucket = null; + try { + // note: this currently assumes Settings service implementation details and should eventually be + // replaced with a call to getSettings + GetParameterResponse response = ssm.getParameter(request -> request + .name("/saas-boost/" + environmentName + "/SAAS_BOOST_BUCKET") + ); + artifactsBucket = response.parameter().value(); + } catch (ParameterNotFoundException paramStoreError) { + LOGGER.error("Parameter /saas-boost/" + environmentName + "/SAAS_BOOST_BUCKET not found"); + LOGGER.error(Utils.getFullStackTrace(paramStoreError)); + throw paramStoreError; + } catch (SdkServiceException ssmError) { + LOGGER.error("ssm:GetParameter error {}", ssmError.getMessage()); + LOGGER.error(Utils.getFullStackTrace(ssmError)); + throw ssmError; + } + LOGGER.info("Loaded artifacts bucket {}", artifactsBucket); + return new SaaSBoostArtifactsBucket(artifactsBucket, region); + } + + // VisibleForTesting + static String getExistingSaaSBoostStackName(SsmClient ssm, String environmentName) { + LOGGER.debug("Getting existing SaaS Boost CloudFormation stack name from Parameter Store"); + String stackName = null; + try { + GetParameterResponse response = ssm.getParameter(request -> request + .name("/saas-boost/" + environmentName + "/SAAS_BOOST_STACK") + ); + stackName = response.parameter().value(); + } catch (ParameterNotFoundException paramStoreError) { + LOGGER.warn("Parameter /saas-boost/" + environmentName + + "/SAAS_BOOST_STACK not found setting to default 'sb-" + environmentName + "'"); + stackName = "sb-" + environmentName; + } catch (SdkServiceException ssmError) { + LOGGER.error("ssm:GetParameter error {}", ssmError.getMessage()); + LOGGER.error(Utils.getFullStackTrace(ssmError)); + throw ssmError; + } + LOGGER.info("Loaded stack name {}", stackName); + return stackName; + } + + // VisibleForTesting + static Map getExistingSaaSBoostStackDetails( + CloudFormationClient cfn, + String baseCloudFormationStackName) { + LOGGER.debug("Getting CloudFormation stack details for SaaS Boost stack {}", baseCloudFormationStackName); + Map details = new HashMap<>(); + List requiredOutputs = List.of("PublicSubnet1", "PublicSubnet2", "PrivateSubnet1", + "PrivateSubnet2", "EgressVpc", "LoggingBucket"); + try { + DescribeStacksResponse response = cfn.describeStacks( + request -> request.stackName(baseCloudFormationStackName)); + if (response.hasStacks() && !response.stacks().isEmpty()) { + Stack stack = response.stacks().get(0); + // TODO Should we just capture them all? + Map outputs = stack.outputs().stream() + .filter(output -> requiredOutputs.contains(output.outputKey())) + .collect(Collectors.toMap(Output::outputKey, Output::outputValue)); + for (String requiredOutput : requiredOutputs) { + if (!outputs.containsKey(requiredOutput)) { + throw new EnvironmentLoadException("Missing required CloudFormation stack output " + + requiredOutput + " from stack " + baseCloudFormationStackName); + } else { + LOGGER.debug("Loaded required stack output {} -> {}", + requiredOutput, outputs.get(requiredOutput)); + } + } + Map parameters = stack.parameters().stream() + .collect(Collectors.toMap(Parameter::parameterKey, Parameter::parameterValue)); + + details.putAll(parameters); + details.putAll(outputs); + } + } catch (SdkServiceException cfnError) { + LOGGER.error("cloudformation:DescribeStacks error", cfnError); + LOGGER.error(Utils.getFullStackTrace(cfnError)); + throw cfnError; + } + return details; + } + + // VisibleForTesting + static String getExistingSaaSBoostLambdasFolder(SsmClient ssm, String environmentName) { + LOGGER.debug("Getting existing SaaS Boost Lambdas folder from Parameter Store"); + String lambdasFolder = null; + try { + GetParameterResponse response = ssm.getParameter(request -> request + .name("/saas-boost/" + environmentName + "/SAAS_BOOST_LAMBDAS_FOLDER") + ); + lambdasFolder = response.parameter().value(); + } catch (ParameterNotFoundException paramStoreError) { + LOGGER.warn("Parameter /saas-boost/" + environmentName + + "/SAAS_BOOST_LAMBDAS_FOLDER not found setting to default 'lambdas'"); + lambdasFolder = "lambdas"; + } catch (SdkServiceException ssmError) { + LOGGER.error("ssm:GetParameter error {}", ssmError.getMessage()); + LOGGER.error(Utils.getFullStackTrace(ssmError)); + throw ssmError; + } + LOGGER.info("Loaded Lambdas folder {}", lambdasFolder); + return lambdasFolder; + } + + // VisibleForTesting + static boolean getExistingSaaSBoostAnalyticsDeployed(SsmClient ssm, String environmentName) { + LOGGER.debug("Getting existing SaaS Boost Analytics module deployed from Parameter Store"); + boolean analyticsDeployed = false; + try { + GetParameterResponse response = ssm.getParameter(request -> request + .name("/saas-boost/" + environmentName + "/METRICS_ANALYTICS_DEPLOYED") + ); + analyticsDeployed = Boolean.parseBoolean(response.parameter().value()); + } catch (ParameterNotFoundException paramStoreError) { + // this means the parameter doesn't exist, so ignore + } catch (SdkServiceException ssmError) { + // TODO CloudFormation should own this parameter, not the installer... + LOGGER.error("ssm:GetParameter error {}", ssmError.getMessage()); + LOGGER.error(Utils.getFullStackTrace(ssmError)); + throw ssmError; + } + LOGGER.info("Loaded analytics deployed {}", analyticsDeployed); + return analyticsDeployed; + } +} diff --git a/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/workflow/AbstractWorkflow.java b/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/workflow/AbstractWorkflow.java new file mode 100644 index 00000000..a6ee9a8a --- /dev/null +++ b/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/workflow/AbstractWorkflow.java @@ -0,0 +1,30 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost.workflow; + +public abstract class AbstractWorkflow implements Workflow { + private int exitCode; + + void setExitCode(int exitCode) { + this.exitCode = exitCode; + } + + @Override + public int getExitCode() { + return this.exitCode; + } +} diff --git a/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/workflow/UpdateAction.java b/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/workflow/UpdateAction.java new file mode 100644 index 00000000..b7c177f4 --- /dev/null +++ b/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/workflow/UpdateAction.java @@ -0,0 +1,85 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost.workflow; + +import java.nio.file.Path; +import java.util.HashSet; +import java.util.Set; + +/** + * This enumerates all component types the installer may need to update during + * the course of a SaaS Boost update. + */ +public enum UpdateAction { + CLIENT, + CUSTOM_RESOURCES, + FUNCTIONS, + LAYERS, + METERING_BILLING, + RESOURCES, + SERVICES; + + private Set targets = new HashSet(); + + /** + * Adds a new target to this UpdateAction. + * + * e.g. if this is a SERVICE, the list of targets may be + * [ "onboarding", "tenant" ]. + * + * @param target the target to add + */ + public void addTarget(String target) { + targets.add(target); + } + + public Set getTargets() { + return targets; + } + + public void resetTargets() { + targets.clear(); + } + + public String getDirectoryName() { + if (this != CUSTOM_RESOURCES) { + return nameToDirectory(this.name()); + } + return Path.of(RESOURCES.getDirectoryName(), nameToDirectory(this.name())).toString(); + } + + private static String nameToDirectory(String name) { + return name.toLowerCase().replace('_', '-'); + } + + private static String directoryToName(String dir) { + return dir.toUpperCase().replace('-', '_'); + } + + public static UpdateAction fromDirectoryName(String directoryName) { + try { + return UpdateAction.valueOf(directoryToName(directoryName)); + } catch (IllegalArgumentException iae) { + return null; + } + } + + @Override + public String toString() { + return this.name() + " | " + this.getTargets(); + } +} diff --git a/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/workflow/UpdateWorkflow.java b/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/workflow/UpdateWorkflow.java new file mode 100644 index 00000000..a27550d0 --- /dev/null +++ b/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/workflow/UpdateWorkflow.java @@ -0,0 +1,522 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost.workflow; + +import com.amazon.aws.partners.saasfactory.saasboost.Constants; +import com.amazon.aws.partners.saasfactory.saasboost.Keyboard; +import com.amazon.aws.partners.saasfactory.saasboost.SaaSBoostInstall; +import com.amazon.aws.partners.saasfactory.saasboost.Utils; +import com.amazon.aws.partners.saasfactory.saasboost.clients.AwsClientBuilderFactory; +import com.amazon.aws.partners.saasfactory.saasboost.model.Environment; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.awssdk.core.exception.SdkServiceException; +import software.amazon.awssdk.services.apigateway.ApiGatewayClient; +import software.amazon.awssdk.services.apigateway.model.GetRestApisResponse; +import software.amazon.awssdk.services.apigateway.model.RestApi; +import software.amazon.awssdk.services.cloudformation.CloudFormationClient; +import software.amazon.awssdk.services.cloudformation.model.DescribeStacksResponse; +import software.amazon.awssdk.services.cloudformation.model.Parameter; +import software.amazon.awssdk.services.cloudformation.model.Stack; +import software.amazon.awssdk.services.cloudformation.model.StackStatus; +import software.amazon.awssdk.services.cloudformation.model.UpdateStackRequest; +import software.amazon.awssdk.services.cloudformation.model.UpdateStackResponse; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static com.amazon.aws.partners.saasfactory.saasboost.SaaSBoostInstall.outputMessage; + +public class UpdateWorkflow extends AbstractWorkflow { + + private static final Logger LOGGER = LoggerFactory.getLogger(UpdateWorkflow.class); + + private final Environment environment; + private final Path workingDir; + private final AwsClientBuilderFactory clientBuilderFactory; + + public UpdateWorkflow(Path workingDir, Environment environment, AwsClientBuilderFactory clientBuilderFactory) { + this.environment = environment; + this.workingDir = workingDir; + this.clientBuilderFactory = clientBuilderFactory; + } + + private boolean confirm() { + outputMessage("******* W A R N I N G *******"); + outputMessage("Updating AWS SaaS Boost environment is an IRREVERSIBLE operation. You should test an " + + "updated install in a non-production environment\n" + + "before updating a production environment. By continuing you understand and ACCEPT the RISKS!"); + System.out.print("Enter y to continue with UPDATE of " + environment.getBaseCloudFormationStackName() + + " or n to CANCEL: "); + boolean continueUpgrade = Keyboard.readBoolean(); + if (!continueUpgrade) { + outputMessage("Canceled UPDATE of AWS SaaS Boost environment"); + } else { + outputMessage("Continuing UPDATE of AWS SaaS Boost stack " + environment.getBaseCloudFormationStackName()); + } + return continueUpgrade; + } + + public void run() { + LOGGER.info("Perform Update of AWS SaaS Boost deployment"); + if (!confirm()) { + setExitCode(2); + return; + } + + // Get values for all the CloudFormation parameters including possibly new ones in the template file on disk + Map cloudFormationParamMap = getCloudFormationParameterMap( + workingDir.resolve(Path.of("resources", "saas-boost.yaml")), + environment.getBaseCloudFormationStackInfo()); + + // find all changed files from git, and execute specific update actions for each + List changedPaths = findChangedPaths(cloudFormationParamMap); + LOGGER.debug("Found changedPaths: {}", changedPaths); + for (UpdateAction action : getUpdateActionsFromPaths(changedPaths)) { + LOGGER.debug("executing UpdateAction: {}", action); + switch (action) { + case CLIENT: { + outputMessage("Updating Admin UI web application.."); + SaaSBoostInstall.buildAndCopyWebApp( + workingDir, + clientBuilderFactory.cloudFormationBuilder().build(), + clientBuilderFactory.s3Builder().build(), + environment.getName(), + environment.getAccountId()); + break; + } + case CUSTOM_RESOURCES: + case FUNCTIONS: + case LAYERS: + case METERING_BILLING: + case SERVICES: { + // for each target, run the update script in the target's directory + for (String target : action.getTargets()) { + // TODO update this logic for windows + File updatedDirectory = new File(action.getDirectoryName(), target); + outputMessage("Updating " + updatedDirectory + " using " + + new File(updatedDirectory, "update.sh")); + // if this fails because update.sh does not exist, does not have the proper + // permissions or any other reason, a runtimeException will be thrown, exiting + // the run() execution + SaaSBoostInstall.executeCommand( + "./update.sh " + environment.getName(), // command to execute + null, // environment to use + updatedDirectory.getAbsoluteFile()); // directory to execute from + } + break; + } + case RESOURCES: { + // upload the template to the Boost Artifacts bucket + for (String target : action.getTargets()) { + outputMessage("Updating CloudFormation template: " + target); + environment.getArtifactsBucket().putFile( + clientBuilderFactory.s3Builder().build(), // s3 client + Path.of(action.getDirectoryName(), target), // local path + Path.of(target)); // remote path + if (target.equals("saas-boost-metrics-analytics.yaml") + && environment.isMetricsAnalyticsDeployed()) { + // the metrics-analytics stack is not a child stack of the base stack, + // so just updating the base stack won't update. update it manually. + String analyticsStackName = environment.getBaseCloudFormationStackName() + "-analytics"; + // Load up the existing parameters from CloudFormation + Map stackParamsMap = new LinkedHashMap<>(); + try { + DescribeStacksResponse response = clientBuilderFactory.cloudFormationBuilder().build() + .describeStacks(request -> request.stackName(analyticsStackName)); + if (response.hasStacks() && !response.stacks().isEmpty()) { + Stack stack = response.stacks().get(0); + stackParamsMap = stack.parameters().stream() + .collect(Collectors.toMap( + Parameter::parameterKey, Parameter::parameterValue)); + } + } catch (SdkServiceException cfnError) { + if (cfnError.getMessage().contains("does not exist")) { + outputMessage("Analytics module CloudFormation stack " + + analyticsStackName + " not found."); + System.exit(2); + } + LOGGER.error("cloudformation:DescribeStacks error", cfnError); + LOGGER.error(Utils.getFullStackTrace(cfnError)); + throw cfnError; + } + Map paramsMap = getCloudFormationParameterMap( + workingDir.resolve(Path.of("resources", "saas-boost-metrics-analytics.yaml")), + stackParamsMap); + updateCloudFormationStack(analyticsStackName, paramsMap, target); + } + } + break; + } + default: { + // unrecognized case above means either not implemented or something was missed + LOGGER.error("Ignoring parsed UpdateAction: " + action + + " since no update case is implemented for it."); + } + } + } + + // Update the version number + outputMessage("Updating Version parameter to " + Constants.VERSION); + cloudFormationParamMap.put("Version", Constants.VERSION); + + // Always call update stack + outputMessage("Executing CloudFormation update stack on: " + environment.getBaseCloudFormationStackName()); + updateCloudFormationStack( + environment.getBaseCloudFormationStackName(), + cloudFormationParamMap, + "saas-boost.yaml"); + + runApiGatewayDeployment(cloudFormationParamMap); + + setExitCode(0); + outputMessage("Update of SaaS Boost environment " + environment.getName() + " complete."); + } + + protected static Map getCloudFormationParameterMap( + Path cloudFormationTemplateFile, + Map stackParamsMap) { + + if (!Files.exists(cloudFormationTemplateFile)) { + outputMessage("Unable to find file " + cloudFormationTemplateFile.toString()); + throw new RuntimeException("Could not find base CloudFormation stack: " + cloudFormationTemplateFile); + } + // Open CFN template yaml file and prompt for values of params that are not in the existing stack + LOGGER.info("Building map of parameters for template " + cloudFormationTemplateFile); + Map cloudFormationParamMap = new LinkedHashMap<>(); + + ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + try (InputStream cloudFormationTemplate = Files.newInputStream(cloudFormationTemplateFile)) { + LinkedHashMap template = mapper.readValue(cloudFormationTemplate, LinkedHashMap.class); + LinkedHashMap> parameters = + (LinkedHashMap>) template.get("Parameters"); + for (Map.Entry> parameter : parameters.entrySet()) { + String parameterKey = parameter.getKey(); + LinkedHashMap parameterProperties = + (LinkedHashMap) parameter.getValue(); + + // For each parameter in the template file, set the value to any existing value + // otherwise prompt the user to set the value. + Object existingParameter = stackParamsMap.get(parameterKey); + if (existingParameter != null) { + // We're running an update. Start with reusing the current value for this parameter. + // The calling code can override this parameter's value before executing update stack. + LOGGER.info("Reuse existing value for parameter {} => {}", parameterKey, existingParameter); + cloudFormationParamMap.put(parameterKey, stackParamsMap.get(parameterKey)); + } else { + // This is a new parameter added to the template file on disk. Prompt the user for a value. + Object defaultValue = parameterProperties.get("Default"); + String parameterType = (String) parameterProperties.get("Type"); + System.out.print("Enter a " + parameterType + " value for parameter " + parameterKey); + if (defaultValue != null) { + // No default value for this property + System.out.print(". (Press Enter for '" + defaultValue + "'): "); + } else { + System.out.print(": "); + } + String enteredValue = Keyboard.readString(); + if (Utils.isEmpty(enteredValue) && defaultValue != null) { + cloudFormationParamMap.put(parameterKey, String.valueOf(defaultValue)); + LOGGER.info("Using default value for parameter {} => {}", + parameterKey, cloudFormationParamMap.get(parameterKey)); + } else if (Utils.isEmpty(enteredValue) && defaultValue == null) { + cloudFormationParamMap.put(parameterKey, ""); + LOGGER.info("Using entered value for parameter {} => {}", + parameterKey, cloudFormationParamMap.get(parameterKey)); + } else { + cloudFormationParamMap.put(parameterKey, enteredValue); + LOGGER.info("Using entered value for parameter {} => {}", + parameterKey, cloudFormationParamMap.get(parameterKey)); + } + } + } + } catch (IOException ioe) { + LOGGER.error("Error parsing YAML file from path", ioe); + LOGGER.error(Utils.getFullStackTrace(ioe)); + throw new RuntimeException(ioe); + } + return cloudFormationParamMap; + } + + // TODO git functionality should be extracted to a "gitToolbox" object for easier mock/testing + protected List findChangedPaths(Map cloudFormationParamMap) { + // list all staged and committed changes against the last updated commit + String versionParameter = cloudFormationParamMap.get("Version"); + LOGGER.debug("Found existing version: {}", versionParameter); + // if Version was created with "Commit time", we need to remove that to get commit hash + if (versionParameter.contains(",")) { + versionParameter = versionParameter.split(",")[0]; + } + // if last update or install was created with uncommitted code, assume we're working from + // the last information we have: the commit on top of which the uncommitted code was written + if (versionParameter.contains("-dirty")) { + versionParameter = versionParameter.split("-")[0]; + } + LOGGER.debug("Parsed version to: {}", versionParameter); + List changedPaths = new ArrayList<>(); + // -b : ignore whitespace-only changes + // --name-only : only output the filename (for easy parsing) + // $(version)..HEAD : output changes since $(version) + String gitDiffCommand = "git diff -b --name-only " + versionParameter; + changedPaths.addAll(listPathsFromGitCommand(gitDiffCommand)); + + // list all untracked changes (i.e. net new un-added files) + String gitListUntrackedFilesCommand = "git ls-files --others --exclude-standard"; + if (SaaSBoostInstall.isWindows()) { + gitListUntrackedFilesCommand = "cmd /c " + gitListUntrackedFilesCommand; + } + changedPaths.addAll(listPathsFromGitCommand(gitListUntrackedFilesCommand)); + + return changedPaths; + } + + private List listPathsFromGitCommand(String command) { + List paths = new ArrayList<>(); + Process process = null; + try { + if (SaaSBoostInstall.isWindows()) { + command = "cmd /c " + command; + } + + LOGGER.debug("Executing `" + command + "`"); + + process = Runtime.getRuntime().exec(command); + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + String line = ""; + while ((line = reader.readLine()) != null) { + LOGGER.debug(line); + paths.add(Path.of(line)); + } + } catch (IOException ioe) { + LOGGER.error("Error reading from runtime exec process", ioe); + LOGGER.error(Utils.getFullStackTrace(ioe)); + throw new RuntimeException(ioe); + } + + process.waitFor(); + int exitValue = process.exitValue(); + if (exitValue != 0) { + throw new RuntimeException("Error running command `" + command + "`: exit code " + exitValue); + } + } catch (IOException | InterruptedException e) { + LOGGER.error(Utils.getFullStackTrace(e)); + } finally { + if (process != null) { + process.destroy(); + } + } + return paths; + } + + protected Collection getUpdateActionsFromPaths(List changedPaths) { + Set actions = EnumSet.noneOf(UpdateAction.class); + + /* + * Take for example the following list of changed paths: + * client/web/src/App.js + * functions/core-stack-listener/src/... + * services/onboarding-service/src/... + * services/tenant-service/src/... + * services/tenant-service/src/... + * resources/saas-boost.yaml + * resources/saas-boost-svc-tenant.yaml + * resources/custom-resources/app-services-ecr-macro/src/... + * + * The intention of this algorithm is to pull out the high level SaaS Boost components from the Path, as + * represented by the UpdateAction Enum. e.g. CLIENT, FUNCTIONS, SERVICES, CUSTOM_RESOURCES, RESOURCES for + * the above example, following these steps + * - for each path + * - traverse through each path component, up to a maximum depth of 2 (optimization, since no component + * pathname is at a depth deeper than two) + * - if we find the resources/ path component and the next component is custom-resources, continue + * - otherwise match the path component against an UpdateAction. if we find a valid one, add it to our + * list taking into account not only the UpdateAction itself (e.g. FUNCTIONS) but also the "target" + * of the UpdateAction (e.g. FUNCTIONS -> core-stack-listener) + * + * So the expected set of UpdateActions resulting from the above example is: + * CLIENT + * FUNCTIONS -> core-stack-listener + * SERVICES -> onboarding-service, tenant-service + * RESOURCES -> saas-boost.yaml, saas-boost-svc-tenant.yaml + * CUSTOM_RESOURCES -> app-services-ecr-macro + */ + for (Path changedPath : changedPaths) { + LOGGER.debug("processing {}", changedPath); + if (!Utils.isBlank(workingDir.toString())) { + // TODO support alternate workingDir for update + // relativize the path before continuing + } + final int maximumTraversalDepth = 2; + for (int i = 0; i < Math.min(changedPath.getNameCount(), maximumTraversalDepth); i++) { + UpdateAction pathAction = UpdateAction.fromDirectoryName(changedPath.getName(i).toString()); + if (pathAction != null) { + // edge case: if this is a resources/custom-resources/.. path, we might be pinging on resources/ + // when we should on custom-resources. so skip if it is + LOGGER.debug("found action {} from path {}", pathAction, changedPath); + if (pathAction == UpdateAction.RESOURCES + && UpdateAction.fromDirectoryName(changedPath.getName(i + 1).toString()) != null) { + LOGGER.debug("Skipping RESOURCES for CUSTOM_RESOURCES in {}", changedPath); + continue; + } + // now add targets if necessary + switch (pathAction) { + case CUSTOM_RESOURCES: + case FUNCTIONS: + case LAYERS: + case METERING_BILLING: + case RESOURCES: + case SERVICES: { + try { + String target = changedPath.getName(i + 1).toString(); + LOGGER.debug("Adding new target {} to UpdateAction {}", target, pathAction); + pathAction.addTarget(changedPath.getName(i + 1).toString()); + } catch (IllegalArgumentException iae) { + LOGGER.error("Error parsing changed paths during update: {} is an unparsable path", + changedPath); + LOGGER.error(Utils.getFullStackTrace(iae)); + throw new RuntimeException(iae); + } + break; + } + default: { + // do nothing + } + } + if (!actions.contains(pathAction)) { + LOGGER.debug("Adding new action {} from path {}", pathAction, changedPath); + actions.add(pathAction); + } + break; + } + } + } + + return actions; + } + + // TODO all CloudFormation activities (reading params, updating stacks) + // should be extracted to a class for easier mocking/testing + private void updateCloudFormationStack(String stackName, Map paramsMap, String yamlFile) { + List templateParameters = paramsMap.entrySet().stream() + .map(entry -> Parameter.builder().parameterKey(entry.getKey()).parameterValue(entry.getValue()).build()) + .collect(Collectors.toList()); + + CloudFormationClient cfn = clientBuilderFactory.cloudFormationBuilder().build(); + LOGGER.info("Executing CloudFormation update stack for " + stackName); + try { + UpdateStackResponse updateStackResponse = cfn.updateStack(UpdateStackRequest.builder() + .stackName(stackName) + .capabilitiesWithStrings("CAPABILITY_NAMED_IAM", "CAPABILITY_AUTO_EXPAND") + .templateURL(environment.getArtifactsBucket().getBucketUrl() + yamlFile) + .parameters(templateParameters) + .build() + ); + String stackId = updateStackResponse.stackId(); + LOGGER.info("Waiting for update stack to complete for " + stackId); + long sleepTime = 1L; + final long timeoutMinutes = 60L; + final long timeout = (timeoutMinutes * 60 * 1000) + System.currentTimeMillis(); + while (true) { + if (System.currentTimeMillis() > timeout) { + outputMessage("CloudFormation update of stack: " + stackName + " timed out. " + + "Check the events in the AWS CloudFormation console."); + } + DescribeStacksResponse response = cfn.describeStacks(request -> request.stackName(stackId)); + Stack stack = response.stacks().get(0); + StackStatus stackStatus = stack.stackStatus(); + EnumSet failureStatuses = EnumSet.of( + StackStatus.UPDATE_ROLLBACK_COMPLETE, + StackStatus.UPDATE_FAILED, + StackStatus.UPDATE_ROLLBACK_FAILED); + if (stackStatus == StackStatus.UPDATE_COMPLETE) { + outputMessage("CloudFormation stack: " + stackName + " updated successfully."); + break; + } else if (failureStatuses.contains(stackStatus)) { + outputMessage("CloudFormation stack: " + stackName + " update failed."); + throw new RuntimeException("Error with CloudFormation stack " + stackName + + ". Check the events in the AWS CloudFormation Console"); + } else { + // TODO should we set an upper bound on this loop? + outputMessage("Awaiting Update of CloudFormation Stack " + stackName + + " to complete. Sleep " + sleepTime + " minute(s)..."); + try { + Thread.sleep(sleepTime * 60 * 1000); + } catch (Exception e) { + LOGGER.error("Error pausing thread", e); + } + sleepTime = 1L; //set to 1 minute after kick off of 5 minute + } + } + } catch (SdkServiceException cfnError) { + if (cfnError.getMessage().contains("No updates are to be performed")) { + outputMessage("No Updates to be performed for Stack: " + stackName); + } else { + LOGGER.error("updateCloudFormationStack::update stack failed {}", cfnError.getMessage()); + LOGGER.error(Utils.getFullStackTrace(cfnError)); + throw cfnError; + } + } + } + + // VisibleForTesting + protected void runApiGatewayDeployment(Map cloudFormationParamMap) { + // CloudFormation will not redeploy an API Gateway stage on update + outputMessage("Updating API Gateway deployment for stages"); + try { + String publicApiName = "sb-" + environment.getName() + "-public-api"; + String privateApiName = "sb-" + environment.getName() + "-private-api"; + ApiGatewayClient apigw = clientBuilderFactory.apiGatewayBuilder().build(); + GetRestApisResponse response = apigw.getRestApis(); + if (response.hasItems()) { + for (RestApi api : response.items()) { + String apiName = api.name(); + boolean isPublicApi = publicApiName.equals(apiName); + boolean isPrivateApi = privateApiName.equals(apiName); + if (isPublicApi || isPrivateApi) { + String stage = isPublicApi ? cloudFormationParamMap.get("PublicApiStage") + : cloudFormationParamMap.get("PrivateApiStage"); + outputMessage("Updating API Gateway deployment for " + apiName + " to stage: " + stage); + apigw.createDeployment(request -> request + .restApiId(api.id()) + .stageName(stage) + ); + } + } + } + } catch (SdkServiceException apigwError) { + LOGGER.error("apigateway error", apigwError); + LOGGER.error(Utils.getFullStackTrace(apigwError)); + throw apigwError; + } + } +} diff --git a/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/workflow/Workflow.java b/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/workflow/Workflow.java new file mode 100644 index 00000000..8cf9c19c --- /dev/null +++ b/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/workflow/Workflow.java @@ -0,0 +1,21 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost.workflow; + +public interface Workflow extends Runnable { + public int getExitCode(); +} diff --git a/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostInstallTest.java b/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostInstallTest.java index e406c7e6..0a922bf8 100644 --- a/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostInstallTest.java +++ b/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostInstallTest.java @@ -80,32 +80,4 @@ public static void initTemplate() { public void resetStdIn() { System.setIn(System.in); } - - @Test - public void testGetCloudFormationParameterMap() throws Exception { - // The input map represents the existing CloudFormation parameter values. - // These will either be the template defaults, or they will be the parameter - // values read from a created stack with the describeStacks call. - // We'll pretend that the RequiredStringParameter parameter is newly added - // to the template on disk so the user should be prompted for a value - Map input = new LinkedHashMap<>(); - input.put("DefaultStringParameter", "foobar"); - input.put("NumericParameter", "1"); // Let's pretend that we overwrote the default the first time around - - // Fill up standard input with a response for the Keyboard class - System.setIn(new ByteArrayInputStream(("keyboard input" + System.lineSeparator()).getBytes(StandardCharsets.UTF_8))); - - Path cloudFormationTemplate = Path.of(this.getClass().getClassLoader().getResource("template.yaml").toURI()); - Map actual = SaaSBoostInstall.getCloudFormationParameterMap(cloudFormationTemplate, input); - - Map expected = new LinkedHashMap<>(); - expected.put("RequiredStringParameter", "keyboard input"); - expected.put("DefaultStringParameter", "foobar"); - expected.put("NumericParameter", "1"); - - assertEquals("Template has 3 parameters", expected.size(), actual.size()); - for (Map.Entry entry : expected.entrySet()) { - assertEquals(entry.getKey() + " equals " + entry.getValue(), entry.getValue(), actual.get(entry.getKey())); - } - } } diff --git a/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/clients/AwsClientBuilderFactoryTest.java b/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/clients/AwsClientBuilderFactoryTest.java index c96093fc..d9f30dbe 100644 --- a/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/clients/AwsClientBuilderFactoryTest.java +++ b/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/clients/AwsClientBuilderFactoryTest.java @@ -22,9 +22,13 @@ import org.mockito.ArgumentCaptor; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; +import software.amazon.awssdk.awscore.client.builder.AwsSyncClientBuilder; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.quicksight.QuickSightClientBuilder; +import java.lang.reflect.Method; +import java.util.List; + import static org.junit.Assert.*; import static org.mockito.Mockito.*; @@ -79,6 +83,24 @@ public void verifyBuilderCredentialProviderOverridden() { DEFAULT_EXPECTED_REGION, expectedCredentialsProviderClass); } + @Test + public void verifyFactoryCachingForAllBuilders() { + AwsClientBuilderFactory factory = AwsClientBuilderFactory.builder().build(); + // for each method that returns a builder.. + for (Method m : factory.getClass().getMethods()) { + // checking if the return type implements AwsSyncClientBuilder + if (List.of(m.getReturnType().getInterfaces()).contains(AwsSyncClientBuilder.class)) { + try { + AwsSyncClientBuilder b = (AwsSyncClientBuilder) m.invoke(factory); + // invoking the builder function again should not create a new builder + assertEquals(b, (AwsSyncClientBuilder) m.invoke(factory)); + } catch (Exception e) { + throw new RuntimeException("test failed", e); + } + } + } + } + private void runBoostAwsClientBuilderFactoryTest( AwsClientBuilderFactory factory, Region expectedRegion, diff --git a/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/clients/MockAwsClientBuilderFactory.java b/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/clients/MockAwsClientBuilderFactory.java new file mode 100644 index 00000000..a585618d --- /dev/null +++ b/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/clients/MockAwsClientBuilderFactory.java @@ -0,0 +1,99 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + package com.amazon.aws.partners.saasfactory.saasboost.clients; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.net.URI; + +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.http.SdkHttpClient; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.cloudformation.CloudFormationClient; +import software.amazon.awssdk.services.cloudformation.CloudFormationClientBuilder; + +public class MockAwsClientBuilderFactory extends AwsClientBuilderFactory { + private final AwsClientBuilderFactory factory = mock(AwsClientBuilderFactory.class); + + public MockAwsClientBuilderFactory() { + + } + + public void mockCfn(CloudFormationClient cfn) { + when(factory.cloudFormationBuilder()).thenReturn(new CloudFormationClientBuilder() { + + @Override + public CloudFormationClientBuilder httpClient(SdkHttpClient httpClient) { + // TODO Auto-generated method stub + return null; + } + + @Override + public CloudFormationClientBuilder httpClientBuilder( + software.amazon.awssdk.http.SdkHttpClient.Builder httpClientBuilder) { + // TODO Auto-generated method stub + return null; + } + + @Override + public CloudFormationClientBuilder credentialsProvider(AwsCredentialsProvider credentialsProvider) { + // TODO Auto-generated method stub + return null; + } + + @Override + public CloudFormationClientBuilder region(Region region) { + // TODO Auto-generated method stub + return null; + } + + @Override + public CloudFormationClientBuilder dualstackEnabled(Boolean dualstackEndpointEnabled) { + // TODO Auto-generated method stub + return null; + } + + @Override + public CloudFormationClientBuilder fipsEnabled(Boolean fipsEndpointEnabled) { + // TODO Auto-generated method stub + return null; + } + + @Override + public CloudFormationClientBuilder overrideConfiguration( + ClientOverrideConfiguration overrideConfiguration) { + // TODO Auto-generated method stub + return null; + } + + @Override + public CloudFormationClientBuilder endpointOverride(URI endpointOverride) { + // TODO Auto-generated method stub + return null; + } + + @Override + public CloudFormationClient build() { + // TODO Auto-generated method stub + return cfn; + } + + }); + } +} diff --git a/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/workflow/UpdateWorkflowTest.java b/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/workflow/UpdateWorkflowTest.java new file mode 100644 index 00000000..625440f5 --- /dev/null +++ b/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/workflow/UpdateWorkflowTest.java @@ -0,0 +1,156 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.aws.partners.saasfactory.saasboost.workflow; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collection; +import java.util.EnumSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import com.amazon.aws.partners.saasfactory.saasboost.clients.AwsClientBuilderFactory; +import com.amazon.aws.partners.saasfactory.saasboost.clients.MockAwsClientBuilderFactory; +import com.amazon.aws.partners.saasfactory.saasboost.model.Environment; + +public class UpdateWorkflowTest { + + private static final Environment testEnvironment = Environment.builder() + .name("ENV") + .accountId("123456789012") + .build(); + private static final Path workingDir = Paths.get(""); + + private UpdateWorkflow updateWorkflow; + private AwsClientBuilderFactory clientBuilderFactory; + + @Before + public void setup() { + clientBuilderFactory = new MockAwsClientBuilderFactory(); + updateWorkflow = new UpdateWorkflow(workingDir, testEnvironment, clientBuilderFactory); + } + + @After + public void cleanup() { + for (UpdateAction action : UpdateAction.values()) { + action.resetTargets(); + } + } + + @Test + public void testGetCloudFormationParameterMap() throws Exception { + // The input map represents the existing CloudFormation parameter values. + // These will either be the template defaults, or they will be the parameter + // values read from a created stack with the describeStacks call. + // We'll pretend that the RequiredStringParameter parameter is newly added + // to the template on disk so the user should be prompted for a value + Map input = new LinkedHashMap<>(); + input.put("DefaultStringParameter", "foobar"); + input.put("NumericParameter", "1"); // Let's pretend that we overwrote the default the first time around + + // Fill up standard input with a response for the Keyboard class + System.setIn(new ByteArrayInputStream(("keyboard input" + System.lineSeparator()).getBytes(StandardCharsets.UTF_8))); + + Path cloudFormationTemplate = Path.of(this.getClass().getClassLoader().getResource("template.yaml").toURI()); + Map actual = UpdateWorkflow.getCloudFormationParameterMap(cloudFormationTemplate, input); + + Map expected = new LinkedHashMap<>(); + expected.put("RequiredStringParameter", "keyboard input"); + expected.put("DefaultStringParameter", "foobar"); + expected.put("NumericParameter", "1"); + + assertEquals("Template has 3 parameters", expected.size(), actual.size()); + for (Map.Entry entry : expected.entrySet()) { + assertEquals(entry.getKey() + " equals " + entry.getValue(), entry.getValue(), actual.get(entry.getKey())); + } + } + + @Test + public void testUpdateActionsFromPaths_basic() { + Set expectedActions = EnumSet.of(UpdateAction.CLIENT, UpdateAction.FUNCTIONS); + List changedPaths = List.of( + Path.of("client/src/App.js"), + Path.of("functions/onboarding-app-stack-listener/pom.xml")); + Collection actualActions = updateWorkflow.getUpdateActionsFromPaths(changedPaths); + assertEquals(expectedActions, actualActions); + actualActions.forEach(action -> { + if (action == UpdateAction.FUNCTIONS) { + assertEquals(1, action.getTargets().size()); + assertEquals(1, UpdateAction.FUNCTIONS.getTargets().size()); + assertTrue(action.getTargets().contains("onboarding-app-stack-listener")); + } + }); + } + + @Test + public void testUpdateActionsFromPaths_unrecognizedPath() { + Set expectedActions = EnumSet.of(UpdateAction.FUNCTIONS, UpdateAction.SERVICES); + List unrecognizedPaths = List.of( + Path.of("abc/unrecognized/path.java"), + Path.of("services/new-service/src/main/java/MyService.java"), + Path.of("services/really-new-service/src/main/java/MyService.java"), + Path.of("functions/new-function/pom.xml")); + Collection actualActions = updateWorkflow.getUpdateActionsFromPaths(unrecognizedPaths); + assertEquals(expectedActions, actualActions); + actualActions.forEach(action -> { + if (action == UpdateAction.FUNCTIONS) { + assertEquals(1, action.getTargets().size()); + assertTrue(action.getTargets().contains("new-function")); + } + if (action == UpdateAction.SERVICES) { + assertEquals(2, action.getTargets().size()); + assertTrue(action.getTargets().contains("new-service")); + assertTrue(action.getTargets().contains("really-new-service")); + } + }); + } + + @Test + public void testUpdateActionsFromPaths_customResourcesPath() { + Set expectedActions = EnumSet.of(UpdateAction.CUSTOM_RESOURCES, UpdateAction.RESOURCES); + List changedPaths = List.of( + Path.of("resources/saas-boost.yaml"), + Path.of("resources/new-cfn-template.yaml"), + Path.of("resources/custom-resources/app-services-ecr-macro/pom.xml"), + Path.of("resources/custom-resources/new-resource/pom.xml")); + Collection actualActions = updateWorkflow.getUpdateActionsFromPaths(changedPaths); + assertEquals(expectedActions, actualActions); + actualActions.forEach(action -> { + if (action == UpdateAction.RESOURCES) { + assertEquals(2, action.getTargets().size()); + assertTrue(action.getTargets().contains("saas-boost.yaml")); + assertTrue(action.getTargets().contains("new-cfn-template.yaml")); + } + if (action == UpdateAction.CUSTOM_RESOURCES) { + assertEquals(2, action.getTargets().size()); + assertTrue(action.getTargets().contains("app-services-ecr-macro")); + assertTrue(action.getTargets().contains("new-resource")); + } + }); + } +} diff --git a/layers/apigw-helper/update_layer.sh b/layers/apigw-helper/update.sh similarity index 100% rename from layers/apigw-helper/update_layer.sh rename to layers/apigw-helper/update.sh diff --git a/layers/cloudformation-utils/update_layer.sh b/layers/cloudformation-utils/update.sh similarity index 100% rename from layers/cloudformation-utils/update_layer.sh rename to layers/cloudformation-utils/update.sh diff --git a/layers/utils/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Utils.java b/layers/utils/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Utils.java index cef8972a..946e9b8f 100644 --- a/layers/utils/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Utils.java +++ b/layers/utils/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Utils.java @@ -379,7 +379,7 @@ public static String version(Class clazz) { try (InputStream propertiesFile = clazz.getClassLoader().getResourceAsStream(GIT_PROPERTIES_FILENAME)) { Properties versionProperties = new Properties(); versionProperties.load(propertiesFile); - version = versionProperties.getProperty("git.commit.id.describe") + ", Commit time: " + versionProperties.getProperty("git.commit.time"); + version = versionProperties.getProperty("git.commit.id.describe"); } catch (Exception e) { LOGGER.error("Error loading version info from {} for {}", GIT_PROPERTIES_FILENAME, clazz.getName()); LOGGER.error(Utils.getFullStackTrace(e)); diff --git a/layers/utils/update_layer.sh b/layers/utils/update.sh similarity index 86% rename from layers/utils/update_layer.sh rename to layers/utils/update.sh index 084f18b1..f60cceeb 100755 --- a/layers/utils/update_layer.sh +++ b/layers/utils/update.sh @@ -58,7 +58,11 @@ echo "Published new layer = $LAYER_VERSION_ARN" # Find all the functions for this SaaS Boost environment that have layers eval FUNCTIONS=\$\("aws --region $MY_AWS_REGION lambda list-functions --query 'Functions[?starts_with(FunctionName, \`sb-${ENVIRONMENT}-\`)] | [?Layers != null] | [].FunctionName' --output text"\) -FUNCTIONS=($FUNCTIONS) +# Because the saas-boost-app-services-macro relies on the Utils package, we need to make sure that also gets updated +# In case we have multiple environments in the same account/region, this could potentially override the Utils implementation +# when one environment is updated from underneath another. This shouldn't be an issue unless the Utils upgrade includes a +# change to the isBlank, isEmpty, logRequestEvent, or Utils.toJson functions. +FUNCTIONS=($FUNCTIONS "saas-boost-app-services-macro") #echo "Updating ${#FUNCTIONS[@]} functions with new layer version" for FX in ${FUNCTIONS[@]}; do diff --git a/metering-billing/lambdas/update_service.sh b/metering-billing/lambdas/update.sh similarity index 100% rename from metering-billing/lambdas/update_service.sh rename to metering-billing/lambdas/update.sh diff --git a/resources/custom-resources/app-services-ecr-macro/update.sh b/resources/custom-resources/app-services-ecr-macro/update.sh new file mode 100755 index 00000000..25db0da1 --- /dev/null +++ b/resources/custom-resources/app-services-ecr-macro/update.sh @@ -0,0 +1,52 @@ +#!/bin/bash +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +if [ -z $1 ]; then + echo "Usage: $0 [Lambda Folder]" + exit 2 +fi + +MY_AWS_REGION=$(aws configure list | grep region | awk '{print $2}') +echo "AWS Region = $MY_AWS_REGION" + +ENVIRONMENT=$1 +LAMBDA_STAGE_FOLDER=$2 +if [ -z $LAMBDA_STAGE_FOLDER ]; then + LAMBDA_STAGE_FOLDER="lambdas" +fi +LAMBDA_CODE=ApplicationServicesEcrMacro-lambda.zip + +#set this for V2 AWS CLI to disable paging +export AWS_PAGER="" + +SAAS_BOOST_BUCKET=$(aws --region $MY_AWS_REGION ssm get-parameter --name "/saas-boost/${ENVIRONMENT}/SAAS_BOOST_BUCKET" --query 'Parameter.Value' --output text) +echo "SaaS Boost Bucket = $SAAS_BOOST_BUCKET" +if [ -z $SAAS_BOOST_BUCKET ]; then + echo "Can't find SAAS_BOOST_BUCKET in Parameter Store" + exit 1 +fi + +# Do a fresh build of the project +mvn +if [ $? -ne 0 ]; then + echo "Error building project" + exit 1 +fi + +# And copy it up to S3 +aws s3 cp target/$LAMBDA_CODE s3://$SAAS_BOOST_BUCKET/$LAMBDA_STAGE_FOLDER/ + +printf "Updating function code for saas-boost-app-services-macro\n" +aws lambda --region "$MY_AWS_REGION" update-function-code --function-name "saas-boost-app-services-macro" --s3-bucket "$SAAS_BOOST_BUCKET" --s3-key $LAMBDA_STAGE_FOLDER/$LAMBDA_CODE diff --git a/resources/custom-resources/cidr-dynamodb/update.sh b/resources/custom-resources/cidr-dynamodb/update.sh new file mode 100755 index 00000000..2bc2d025 --- /dev/null +++ b/resources/custom-resources/cidr-dynamodb/update.sh @@ -0,0 +1,52 @@ +#!/bin/bash +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +if [ -z $1 ]; then + echo "Usage: $0 [Lambda Folder]" + exit 2 +fi + +MY_AWS_REGION=$(aws configure list | grep region | awk '{print $2}') +echo "AWS Region = $MY_AWS_REGION" + +ENVIRONMENT=$1 +LAMBDA_STAGE_FOLDER=$2 +if [ -z $LAMBDA_STAGE_FOLDER ]; then + LAMBDA_STAGE_FOLDER="lambdas" +fi +LAMBDA_CODE=CidrDynamoDB-lambda.zip + +#set this for V2 AWS CLI to disable paging +export AWS_PAGER="" + +SAAS_BOOST_BUCKET=$(aws --region $MY_AWS_REGION ssm get-parameter --name "/saas-boost/${ENVIRONMENT}/SAAS_BOOST_BUCKET" --query 'Parameter.Value' --output text) +echo "SaaS Boost Bucket = $SAAS_BOOST_BUCKET" +if [ -z $SAAS_BOOST_BUCKET ]; then + echo "Can't find SAAS_BOOST_BUCKET in Parameter Store" + exit 1 +fi + +# Do a fresh build of the project +mvn +if [ $? -ne 0 ]; then + echo "Error building project" + exit 1 +fi + +# And copy it up to S3 +aws s3 cp target/$LAMBDA_CODE s3://$SAAS_BOOST_BUCKET/$LAMBDA_STAGE_FOLDER/ + +printf "Updating function code for sb-${ENVIRONMENT}-populate-ddb\n" +aws lambda --region "$MY_AWS_REGION" update-function-code --function-name "sb-${ENVIRONMENT}-populate-ddb" --s3-bucket "$SAAS_BOOST_BUCKET" --s3-key $LAMBDA_STAGE_FOLDER/$LAMBDA_CODE \ No newline at end of file diff --git a/resources/custom-resources/clear-s3-bucket/update.sh b/resources/custom-resources/clear-s3-bucket/update.sh new file mode 100755 index 00000000..2b199df4 --- /dev/null +++ b/resources/custom-resources/clear-s3-bucket/update.sh @@ -0,0 +1,52 @@ +#!/bin/bash +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +if [ -z $1 ]; then + echo "Usage: $0 [Lambda Folder]" + exit 2 +fi + +MY_AWS_REGION=$(aws configure list | grep region | awk '{print $2}') +echo "AWS Region = $MY_AWS_REGION" + +ENVIRONMENT=$1 +LAMBDA_STAGE_FOLDER=$2 +if [ -z $LAMBDA_STAGE_FOLDER ]; then + LAMBDA_STAGE_FOLDER="lambdas" +fi +LAMBDA_CODE=ClearS3Bucket-lambda.zip + +#set this for V2 AWS CLI to disable paging +export AWS_PAGER="" + +SAAS_BOOST_BUCKET=$(aws --region $MY_AWS_REGION ssm get-parameter --name "/saas-boost/${ENVIRONMENT}/SAAS_BOOST_BUCKET" --query 'Parameter.Value' --output text) +echo "SaaS Boost Bucket = $SAAS_BOOST_BUCKET" +if [ -z $SAAS_BOOST_BUCKET ]; then + echo "Can't find SAAS_BOOST_BUCKET in Parameter Store" + exit 1 +fi + +# Do a fresh build of the project +mvn +if [ $? -ne 0 ]; then + echo "Error building project" + exit 1 +fi + +# And copy it up to S3 +aws s3 cp target/$LAMBDA_CODE s3://$SAAS_BOOST_BUCKET/$LAMBDA_STAGE_FOLDER/ + +printf "Updating function code for sb-${ENVIRONMENT}-clear-bucket\n" +aws lambda --region "$MY_AWS_REGION" update-function-code --function-name "sb-${ENVIRONMENT}-clear-bucket" --s3-bucket "$SAAS_BOOST_BUCKET" --s3-key $LAMBDA_STAGE_FOLDER/$LAMBDA_CODE \ No newline at end of file diff --git a/resources/custom-resources/fsx-dns-name/update_service.sh b/resources/custom-resources/fsx-dns-name/update.sh similarity index 71% rename from resources/custom-resources/fsx-dns-name/update_service.sh rename to resources/custom-resources/fsx-dns-name/update.sh index 100504b1..b944d37d 100755 --- a/resources/custom-resources/fsx-dns-name/update_service.sh +++ b/resources/custom-resources/fsx-dns-name/update.sh @@ -13,45 +13,46 @@ # See the License for the specific language governing permissions and # limitations under the License. +if [ -z $1 ]; then + echo "Usage: $0 [Lambda Folder]" + exit 2 +fi + MY_AWS_REGION=$(aws configure list | grep region | awk '{print $2}') echo "AWS Region = $MY_AWS_REGION" -if [ "X$1" = "X" ]; then - echo "usage: $0 " - exit 2 -fi ENVIRONMENT=$1 LAMBDA_STAGE_FOLDER=$2 -if [ "X$LAMBDA_STAGE_FOLDER" = "X" ]; then +if [ -z $LAMBDA_STAGE_FOLDER ]; then LAMBDA_STAGE_FOLDER="lambdas" fi - LAMBDA_CODE=FsxDnsName-lambda.zip #set this for V2 AWS CLI to disable paging export AWS_PAGER="" -SAAS_BOOST_BUCKET=`aws ssm get-parameter --name "/saas-boost/${ENVIRONMENT}/SAAS_BOOST_BUCKET" --query "Parameter.Value" --output text` +SAAS_BOOST_BUCKET=$(aws --region $MY_AWS_REGION ssm get-parameter --name "/saas-boost/${ENVIRONMENT}/SAAS_BOOST_BUCKET" --query 'Parameter.Value' --output text) echo "SaaS Boost Bucket = $SAAS_BOOST_BUCKET" -if [ "X$SAAS_BOOST_BUCKET" = "X" ]; then - echo "/saas-boost/${ENVIRONMENT}/SAAS_BOOST_BUCKET SSM parameter not read from AWS env" +if [ -z $SAAS_BOOST_BUCKET ]; then + echo "Can't find SAAS_BOOST_BUCKET in Parameter Store" exit 1 fi - - +# Do a fresh build of the project mvn if [ $? -ne 0 ]; then echo "Error building project" exit 1 fi +# And copy it up to S3 aws s3 cp target/$LAMBDA_CODE s3://$SAAS_BOOST_BUCKET/$LAMBDA_STAGE_FOLDER/ -# Find all the functions provisioned for tenants -eval FUNCTIONS=\$\("aws --region $MY_AWS_REGION lambda list-functions --query 'Functions[?starts_with(FunctionName, \`sb-${ENVIRONMENT}-fsx-dns-tenant-\`)] | [].FunctionName' --output text"\) +# Find all the functions for this microservice +# We must list in the fsx-dns-name case since functions are created with a tenant ID suffix +eval FUNCTIONS=\$\("aws --region $MY_AWS_REGION lambda list-functions --query 'Functions[?starts_with(FunctionName, \`sb-${ENVIRONMENT}-fsx-dns-\`)] | [].FunctionName' --output text"\) FUNCTIONS=($FUNCTIONS) for FX in "${FUNCTIONS[@]}"; do printf "Updating function code for %s\n" $FX aws lambda --region "$MY_AWS_REGION" update-function-code --function-name "$FX" --s3-bucket "$SAAS_BOOST_BUCKET" --s3-key $LAMBDA_STAGE_FOLDER/$LAMBDA_CODE -done +done \ No newline at end of file diff --git a/resources/custom-resources/rds-bootstrap/update.sh b/resources/custom-resources/rds-bootstrap/update.sh new file mode 100755 index 00000000..77674b7e --- /dev/null +++ b/resources/custom-resources/rds-bootstrap/update.sh @@ -0,0 +1,58 @@ +#!/bin/bash +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +if [ -z $1 ]; then + echo "Usage: $0 [Lambda Folder]" + exit 2 +fi + +MY_AWS_REGION=$(aws configure list | grep region | awk '{print $2}') +echo "AWS Region = $MY_AWS_REGION" + +ENVIRONMENT=$1 +LAMBDA_STAGE_FOLDER=$2 +if [ -z $LAMBDA_STAGE_FOLDER ]; then + LAMBDA_STAGE_FOLDER="lambdas" +fi +LAMBDA_CODE=RdsBootstrap-lambda.zip + +#set this for V2 AWS CLI to disable paging +export AWS_PAGER="" + +SAAS_BOOST_BUCKET=$(aws --region $MY_AWS_REGION ssm get-parameter --name "/saas-boost/${ENVIRONMENT}/SAAS_BOOST_BUCKET" --query 'Parameter.Value' --output text) +echo "SaaS Boost Bucket = $SAAS_BOOST_BUCKET" +if [ -z $SAAS_BOOST_BUCKET ]; then + echo "Can't find SAAS_BOOST_BUCKET in Parameter Store" + exit 1 +fi + +# Do a fresh build of the project +mvn +if [ $? -ne 0 ]; then + echo "Error building project" + exit 1 +fi + +# And copy it up to S3 +aws s3 cp target/$LAMBDA_CODE s3://$SAAS_BOOST_BUCKET/$LAMBDA_STAGE_FOLDER/ + +# Find all the functions for this microservice +# We must list in the rds-bootstrap case since functions are created with a tenant ID suffix +eval FUNCTIONS=\$\("aws --region $MY_AWS_REGION lambda list-functions --query 'Functions[?starts_with(FunctionName, \`sb-${ENVIRONMENT}-rds-bootstrap-\`)] | [].FunctionName' --output text"\) +FUNCTIONS=($FUNCTIONS) +for FX in "${FUNCTIONS[@]}"; do + printf "Updating function code for %s\n" $FX + aws lambda --region "$MY_AWS_REGION" update-function-code --function-name "$FX" --s3-bucket "$SAAS_BOOST_BUCKET" --s3-key $LAMBDA_STAGE_FOLDER/$LAMBDA_CODE +done diff --git a/resources/custom-resources/redshift-table/update_service.sh b/resources/custom-resources/rds-options/update.sh old mode 100644 new mode 100755 similarity index 59% rename from resources/custom-resources/redshift-table/update_service.sh rename to resources/custom-resources/rds-options/update.sh index c69f89b8..e141d194 --- a/resources/custom-resources/redshift-table/update_service.sh +++ b/resources/custom-resources/rds-options/update.sh @@ -13,40 +13,40 @@ # See the License for the specific language governing permissions and # limitations under the License. -MY_AWS_REGION=$(aws configure list | grep region | awk '{print $2}') -echo "AWS Region = $MY_AWS_REGION" - -if [ "X$1" = "X" ]; then - echo "usage: $0 " +if [ -z $1 ]; then + echo "Usage: $0 [Lambda Folder]" exit 2 fi +MY_AWS_REGION=$(aws configure list | grep region | awk '{print $2}') +echo "AWS Region = $MY_AWS_REGION" + ENVIRONMENT=$1 -LAMBDA_CODE=RedshiftTable-lambda.zip -LAMBDA_STAGE_FOLDER=lambdas +LAMBDA_STAGE_FOLDER=$2 +if [ -z $LAMBDA_STAGE_FOLDER ]; then + LAMBDA_STAGE_FOLDER="lambdas" +fi +LAMBDA_CODE=RdsOptions-lambda.zip #set this for V2 AWS CLI to disable paging export AWS_PAGER="" -SAAS_BOOST_BUCKET=$(aws ssm get-parameter --name "/saas-boost/${ENVIRONMENT}/SAAS_BOOST_BUCKET" --query "Parameter.Value" --output text) +SAAS_BOOST_BUCKET=$(aws --region $MY_AWS_REGION ssm get-parameter --name "/saas-boost/${ENVIRONMENT}/SAAS_BOOST_BUCKET" --query 'Parameter.Value' --output text) echo "SaaS Boost Bucket = $SAAS_BOOST_BUCKET" -if [ "X$SAAS_BOOST_BUCKET" = "X" ]; then - echo "SaaS Boost Bucket export not read from AWS env" +if [ -z $SAAS_BOOST_BUCKET ]; then + echo "Can't find SAAS_BOOST_BUCKET in Parameter Store" exit 1 fi -# Do a fresh build of the micro service +# Do a fresh build of the project mvn if [ $? -ne 0 ]; then echo "Error building project" exit 1 fi + # And copy it up to S3 aws s3 cp target/$LAMBDA_CODE s3://$SAAS_BOOST_BUCKET/$LAMBDA_STAGE_FOLDER/ -FUNCTIONS=("sb-${ENVIRONMENT}-redshift-table-create-${MY_AWS_REGION}" -) - -for FUNCTION in ${FUNCTIONS[@]}; do - aws lambda --region $MY_AWS_REGION update-function-code --function-name $FUNCTION --s3-bucket $SAAS_BOOST_BUCKET --s3-key $LAMBDA_STAGE_FOLDER/$LAMBDA_CODE -done +printf "Updating function code for sb-${ENVIRONMENT}-rds-options\n" +aws lambda --region "$MY_AWS_REGION" update-function-code --function-name "sb-${ENVIRONMENT}-rds-options" --s3-bucket "$SAAS_BOOST_BUCKET" --s3-key $LAMBDA_STAGE_FOLDER/$LAMBDA_CODE \ No newline at end of file diff --git a/resources/custom-resources/redshift-table/update.sh b/resources/custom-resources/redshift-table/update.sh new file mode 100755 index 00000000..ed8ea5f7 --- /dev/null +++ b/resources/custom-resources/redshift-table/update.sh @@ -0,0 +1,58 @@ +#!/bin/bash +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +if [ -z $1 ]; then + echo "Usage: $0 [Lambda Folder]" + exit 2 +fi + +MY_AWS_REGION=$(aws configure list | grep region | awk '{print $2}') +echo "AWS Region = $MY_AWS_REGION" + +ENVIRONMENT=$1 +LAMBDA_STAGE_FOLDER=$2 +if [ -z $LAMBDA_STAGE_FOLDER ]; then + LAMBDA_STAGE_FOLDER="lambdas" +fi +LAMBDA_CODE=RedshiftTable-lambda.zip + +#set this for V2 AWS CLI to disable paging +export AWS_PAGER="" + +SAAS_BOOST_BUCKET=$(aws --region $MY_AWS_REGION ssm get-parameter --name "/saas-boost/${ENVIRONMENT}/SAAS_BOOST_BUCKET" --query 'Parameter.Value' --output text) +echo "SaaS Boost Bucket = $SAAS_BOOST_BUCKET" +if [ -z $SAAS_BOOST_BUCKET ]; then + echo "Can't find SAAS_BOOST_BUCKET in Parameter Store" + exit 1 +fi + +# Do a fresh build of the project +mvn +if [ $? -ne 0 ]; then + echo "Error building project" + exit 1 +fi + +# And copy it up to S3 +aws s3 cp target/$LAMBDA_CODE s3://$SAAS_BOOST_BUCKET/$LAMBDA_STAGE_FOLDER/ + +# Find all the functions for this microservice +# We must list in the redshift-table case since functions are created with a tenant ID suffix +eval FUNCTIONS=\$\("aws --region $MY_AWS_REGION lambda list-functions --query 'Functions[?starts_with(FunctionName, \`sb-${ENVIRONMENT}-redshift-table-\`)] | [].FunctionName' --output text"\) +FUNCTIONS=($FUNCTIONS) +for FX in "${FUNCTIONS[@]}"; do + printf "Updating function code for %s\n" $FX + aws lambda --region "$MY_AWS_REGION" update-function-code --function-name "$FX" --s3-bucket "$SAAS_BOOST_BUCKET" --s3-key $LAMBDA_STAGE_FOLDER/$LAMBDA_CODE +done diff --git a/resources/custom-resources/set-instance-protection/update.sh b/resources/custom-resources/set-instance-protection/update.sh new file mode 100755 index 00000000..b949d6c1 --- /dev/null +++ b/resources/custom-resources/set-instance-protection/update.sh @@ -0,0 +1,52 @@ +#!/bin/bash +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +if [ -z $1 ]; then + echo "Usage: $0 [Lambda Folder]" + exit 2 +fi + +MY_AWS_REGION=$(aws configure list | grep region | awk '{print $2}') +echo "AWS Region = $MY_AWS_REGION" + +ENVIRONMENT=$1 +LAMBDA_STAGE_FOLDER=$2 +if [ -z $LAMBDA_STAGE_FOLDER ]; then + LAMBDA_STAGE_FOLDER="lambdas" +fi +LAMBDA_CODE=SetInstanceProtection-lambda.zip + +#set this for V2 AWS CLI to disable paging +export AWS_PAGER="" + +SAAS_BOOST_BUCKET=$(aws --region $MY_AWS_REGION ssm get-parameter --name "/saas-boost/${ENVIRONMENT}/SAAS_BOOST_BUCKET" --query 'Parameter.Value' --output text) +echo "SaaS Boost Bucket = $SAAS_BOOST_BUCKET" +if [ -z $SAAS_BOOST_BUCKET ]; then + echo "Can't find SAAS_BOOST_BUCKET in Parameter Store" + exit 1 +fi + +# Do a fresh build of the project +mvn +if [ $? -ne 0 ]; then + echo "Error building project" + exit 1 +fi + +# And copy it up to S3 +aws s3 cp target/$LAMBDA_CODE s3://$SAAS_BOOST_BUCKET/$LAMBDA_STAGE_FOLDER/ + +printf "Updating function code for sb-${ENVIRONMENT}-set-instance-protection\n" +aws lambda --region "$MY_AWS_REGION" update-function-code --function-name "sb-${ENVIRONMENT}-set-instance-protection" --s3-bucket "$SAAS_BOOST_BUCKET" --s3-key $LAMBDA_STAGE_FOLDER/$LAMBDA_CODE \ No newline at end of file diff --git a/samples/java/build.sh b/samples/java/build.sh index f6b69527..a14baf98 100755 --- a/samples/java/build.sh +++ b/samples/java/build.sh @@ -35,7 +35,7 @@ i=0 echo "${SAAS_BOOST_ENV} contains ${#SERVICE_NAMES[@]} services:" for SERVICE in "${SERVICE_NAMES[@]}"; do echo "| ${i}: ${SERVICE}" - i=$((i++)) + i=$((i + 1)) done read -p "Please enter the number of the service to upload to: " CHOSEN_SERVICE_INDEX CHOSEN_SERVICE="${SERVICE_NAMES[CHOSEN_SERVICE_INDEX]}" diff --git a/services/metrics-service/update_service.sh b/services/metrics-service/update.sh similarity index 100% rename from services/metrics-service/update_service.sh rename to services/metrics-service/update.sh diff --git a/services/onboarding-service/update_service.sh b/services/onboarding-service/update.sh similarity index 100% rename from services/onboarding-service/update_service.sh rename to services/onboarding-service/update.sh diff --git a/services/quotas-service/update_service.sh b/services/quotas-service/update.sh similarity index 100% rename from services/quotas-service/update_service.sh rename to services/quotas-service/update.sh diff --git a/services/settings-service/update_service.sh b/services/settings-service/update.sh similarity index 100% rename from services/settings-service/update_service.sh rename to services/settings-service/update.sh diff --git a/services/tenant-service/update_service.sh b/services/tenant-service/update.sh similarity index 100% rename from services/tenant-service/update_service.sh rename to services/tenant-service/update.sh diff --git a/services/tier-service/update_service.sh b/services/tier-service/update.sh similarity index 100% rename from services/tier-service/update_service.sh rename to services/tier-service/update.sh diff --git a/services/user-service/update_service.sh b/services/user-service/update.sh similarity index 100% rename from services/user-service/update_service.sh rename to services/user-service/update.sh