5050import software .amazon .awssdk .services .s3 .model .*;
5151import software .amazon .awssdk .services .secretsmanager .SecretsManagerClient ;
5252import software .amazon .awssdk .services .secretsmanager .model .CreateSecretRequest ;
53+ import software .amazon .awssdk .services .secretsmanager .model .ResourceNotFoundException ;
5354import software .amazon .awssdk .services .secretsmanager .model .SecretsManagerException ;
5455import software .amazon .awssdk .services .ssm .SsmClient ;
5556import software .amazon .awssdk .services .ssm .model .*;
@@ -526,15 +527,20 @@ protected void deleteSaasBoostInstallation() {
526527 List <LinkedHashMap <String , Object >> tenants = getProvisionedTenants ();
527528 for (LinkedHashMap <String , Object > tenant : tenants ) {
528529 outputMessage ("Deleting AWS SaaS Boost tenant " + tenant .get ("id" ));
529- deleteProvisionedTenant (tenant );
530+ if ((Boolean ) tenant .get ("active" )) {
531+ LOGGER .debug ("Deleting active tenant" , tenant );
532+ deleteProvisionedTenant (tenant );
533+ } else {
534+ LOGGER .debug ("Not deleting inactive tenant: " , tenant );
535+ }
530536 }
531537
532538 // Clear all the images from ECR or CloudFormation won't be able to delete the repository
533539 try {
534- GetParameterResponse parameterResponse = ssm . getParameter ( request -> request . name ( "/saas-boost/" + this . envName + "/ECR_REPO" ));
535- String ecrRepo = parameterResponse . parameter (). value ( );
536- outputMessage ( "Deleting images from ECR repository " + ecrRepo );
537- deleteEcrImages ( ecrRepo );
540+ for ( String ecrRepo : getEcrRepositories ()) {
541+ outputMessage ( "Deleting images from ECR repository " + ecrRepo );
542+ deleteEcrImages ( ecrRepo );
543+ }
538544 } catch (SdkServiceException ssmError ) {
539545 LOGGER .error ("ssm:GetParameter error" , ssmError );
540546 LOGGER .error (getFullStackTrace (ssmError ));
@@ -555,6 +561,26 @@ protected void deleteSaasBoostInstallation() {
555561 outputMessage ("Deleting AWS SaaS Boost stack: " + this .stackName );
556562 deleteCloudFormationStack (this .stackName );
557563
564+ // Delete the ActiveDirectory Password in SSM if it exists
565+ try {
566+ ssm .deleteParameter (request -> request
567+ .name ("/saas-boost/" + envName + "/ACTIVE_DIRECTORY_PASSWORD" )
568+ .build ());
569+ outputMessage ("ActiveDirectory SSM secret deleted." );
570+ } catch (ParameterNotFoundException pnfe ) {
571+ // there is no ACTIVE_DIRECTORY_PASSWORD parameter, so there is nothing to delete
572+ }
573+ // Delete the ActiveDirectory password in SecretsManager if it exists
574+ try {
575+ secretsManager .deleteSecret (request -> request
576+ .forceDeleteWithoutRecovery (true )
577+ .secretId ("/saas-boost/" + envName + "/ACTIVE_DIRECTORY_PASSWORD" )
578+ .build ());
579+ outputMessage ("ActiveDirectory secretsManager secret deleted." );
580+ } catch (ResourceNotFoundException rnfe ) {
581+ // there is no ACTIVE_DIRECTORY_PASSWORD secret, so there is nothing to delete
582+ }
583+
558584 // Finally, remove the S3 artifacts bucket that this installer created outside of CloudFormation
559585 LOGGER .info ("Clean up s3 bucket: " + saasBoostArtifactsBucket );
560586 cleanUpS3 (saasBoostArtifactsBucket .getBucketName (), null );
@@ -578,6 +604,49 @@ protected void deleteSaasBoostInstallation() {
578604 outputMessage ("Delete of SaaS Boost environment " + this .envName + " complete." );
579605 }
580606
607+ private List <String > getEcrRepositories () {
608+ List <String > repos = new ArrayList <>();
609+ try {
610+ Map <String , Object > systemApiRequest = new HashMap <>();
611+ Map <String , Object > detail = new HashMap <>();
612+ detail .put ("resource" , "settings/config" );
613+ detail .put ("method" , "GET" );
614+ systemApiRequest .put ("detail" , detail );
615+ final ObjectMapper mapper = new ObjectMapper ();
616+ final byte [] payload = mapper .writeValueAsBytes (systemApiRequest );
617+ try {
618+ LOGGER .info ("Invoking getSettings API" );
619+ InvokeResponse response = lambda .invoke (request -> request
620+ .functionName ("sb-" + this .envName + "-private-api-client" )
621+ .invocationType (InvocationType .REQUEST_RESPONSE )
622+ .payload (SdkBytes .fromByteArray (payload ))
623+ );
624+ if (response .sdkHttpResponse ().isSuccessful ()) {
625+ LOGGER .error ("got response back: {}" , response );
626+ String configJson = response .payload ().asUtf8String ();
627+ HashMap <String , Object > config = mapper .readValue (configJson , HashMap .class );
628+ HashMap <String , Object > services = (HashMap <String , Object >) config .get ("services" );
629+ for (String serviceName : services .keySet ()) {
630+ HashMap <String , Object > service = (HashMap <String , Object >) services .get (serviceName );
631+ repos .add ((String ) service .get ("containerRepo" ));
632+ }
633+ } else {
634+ LOGGER .warn ("Private API client Lambda returned HTTP " + response .sdkHttpResponse ().statusCode ());
635+ throw new RuntimeException (response .sdkHttpResponse ().statusText ().get ());
636+ }
637+ } catch (SdkServiceException lambdaError ) {
638+ LOGGER .error ("lambda:Invoke error" , lambdaError );
639+ LOGGER .error (getFullStackTrace (lambdaError ));
640+ throw lambdaError ;
641+ }
642+ } catch (IOException jacksonError ) {
643+ LOGGER .error ("Error processing JSON" , jacksonError );
644+ LOGGER .error (getFullStackTrace (jacksonError ));
645+ throw new RuntimeException (jacksonError );
646+ }
647+ return repos ;
648+ }
649+
581650 protected void installAnalyticsModule () {
582651 LOGGER .info ("Installing Analytics module into existing AWS SaaS Boost installation." );
583652 outputMessage ("Analytics will be deployed into the existing AWS SaaS Boost environment " + this .envName + "." );
@@ -959,15 +1028,72 @@ protected void deleteApplicationConfig() {
9591028 }
9601029
9611030 protected void deleteProvisionedTenant (LinkedHashMap <String , Object > tenant ) {
962- // Arguably, the delete tenant API should be called here, but all it currently does is set the tenant status
963- // to disabled and then fire off an async event to delete the CloudFormation stack. We could do that, but we'd
964- // still need to either subscribe to the SNS topic for DELETE_COMPLETE notification or we'd need to cycle on
965- // describe stacks like we do elsewhere to wait for CloudFormation to finish. In either case, we need to know
966- // the stack name for the tenant.
967- // Also, this won't scale super well and could be parallelized...
968- String tenantStackName = "Tenant-" + ((String ) tenant .get ("id" )).split ("-" )[0 ];
969- outputMessage ("Deleteing tenant CloudFormation stack " + tenantStackName );
970- deleteCloudFormationStack (tenantStackName );
1031+ // TODO we can parallelize to improve performance with lots of tenants
1032+ String tenantStackId = (String ) ((HashMap <String , Object >)((HashMap <String , Object >) tenant .get ("resources" )).get ("CLOUDFORMATION" )).get ("arn" );
1033+ final ObjectMapper mapper = new ObjectMapper ();
1034+ try {
1035+ Map <String , Object > systemApiRequest = new HashMap <>();
1036+ Map <String , Object > detail = new HashMap <>();
1037+ detail .put ("resource" , "tenants/" + (String ) tenant .get ("id" ));
1038+ detail .put ("method" , "DELETE" );
1039+ Map <String , String > tenantIdOnly = new HashMap <>();
1040+ tenantIdOnly .put ("id" , (String ) tenant .get ("id" ));
1041+ detail .put ("body" , mapper .writeValueAsString (tenantIdOnly ));
1042+ systemApiRequest .put ("detail" , detail );
1043+ final byte [] payload = mapper .writeValueAsBytes (systemApiRequest );
1044+ try {
1045+ LOGGER .info ("Invoking delete tenant API" );
1046+ InvokeResponse response = lambda .invoke (request -> request
1047+ .functionName ("sb-" + this .envName + "-private-api-client" )
1048+ .invocationType (InvocationType .REQUEST_RESPONSE )
1049+ .payload (SdkBytes .fromByteArray (payload ))
1050+ );
1051+ if (response .sdkHttpResponse ().isSuccessful ()) {
1052+ LOGGER .error ("got response back: {}" , response );
1053+ // wait for tenant CloudFormation stack to reach deleted
1054+ boolean waiting = true ;
1055+ while (waiting ) {
1056+ DescribeStacksResponse stackStatusResponse = cfn .describeStacks (request -> request .stackName (tenantStackId ));
1057+ StackStatus stackStatus = stackStatusResponse .stacks ().get (0 ).stackStatus ();
1058+ switch (stackStatus ) {
1059+ case DELETE_COMPLETE :
1060+ case DELETE_FAILED : {
1061+ waiting = false ;
1062+ break ;
1063+ }
1064+ case DELETE_IN_PROGRESS : {
1065+ outputMessage ("Waiting 1 minute for " + tenantStackId + " to finish deleting." );
1066+ try {
1067+ Thread .sleep (60 * 1000 );
1068+ } catch (InterruptedException e ) {
1069+
1070+ }
1071+ }
1072+ default : {
1073+ outputMessage ("Unexpected stackStatus " + stackStatus + " while waiting for " + tenantStackId + " to finish deleting." );
1074+ outputMessage ("Waiting 1 minute for " + tenantStackId + " to finish deleting." );
1075+ try {
1076+ Thread .sleep (60 * 1000 );
1077+ } catch (InterruptedException e ) {
1078+
1079+ }
1080+ }
1081+ }
1082+ }
1083+ } else {
1084+ LOGGER .warn ("Private API client Lambda returned HTTP " + response .sdkHttpResponse ().statusCode ());
1085+ throw new RuntimeException (response .sdkHttpResponse ().statusText ().get ());
1086+ }
1087+ } catch (SdkServiceException lambdaError ) {
1088+ LOGGER .error ("lambda:Invoke error" , lambdaError );
1089+ LOGGER .error (getFullStackTrace (lambdaError ));
1090+ throw lambdaError ;
1091+ }
1092+ } catch (IOException jacksonError ) {
1093+ LOGGER .error ("Error processing JSON" , jacksonError );
1094+ LOGGER .error (getFullStackTrace (jacksonError ));
1095+ throw new RuntimeException (jacksonError );
1096+ }
9711097 }
9721098
9731099 protected void deleteEcrImages (String ecrRepo ) {
0 commit comments