Skip to content

Commit d429f92

Browse files
PoeppingTbrtrvn
authored andcommitted
Fix installer delete for v2
This commit adds the Tenant service delete function to the private api for use by the installer and improves the installer delete functionality for deleting ECR images from all service repositories, properly deleting tenants (and only active tenants), and deleting Active Directory resources when created.
1 parent 3814af4 commit d429f92

File tree

4 files changed

+202
-29
lines changed

4 files changed

+202
-29
lines changed

installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostInstall.java

Lines changed: 140 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
import software.amazon.awssdk.services.s3.model.*;
5151
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
5252
import software.amazon.awssdk.services.secretsmanager.model.CreateSecretRequest;
53+
import software.amazon.awssdk.services.secretsmanager.model.ResourceNotFoundException;
5354
import software.amazon.awssdk.services.secretsmanager.model.SecretsManagerException;
5455
import software.amazon.awssdk.services.ssm.SsmClient;
5556
import 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) {

layers/apigw-helper/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/ApiGatewayHelper.java

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,10 @@
3232
import software.amazon.awssdk.core.retry.backoff.BackoffStrategy;
3333
import software.amazon.awssdk.core.retry.conditions.RetryCondition;
3434
import software.amazon.awssdk.http.*;
35-
import software.amazon.awssdk.http.apache.ApacheHttpClient;
35+
import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient;
3636
import software.amazon.awssdk.regions.Region;
3737
import software.amazon.awssdk.services.sts.StsClient;
3838
import software.amazon.awssdk.services.sts.model.AssumeRoleResponse;
39-
import software.amazon.awssdk.services.sts.model.AssumedRoleUser;
4039
import software.amazon.awssdk.services.sts.model.Credentials;
4140
import software.amazon.awssdk.utils.StringInputStream;
4241

@@ -56,8 +55,7 @@ public class ApiGatewayHelper {
5655
private static final String AWS_REGION = System.getenv("AWS_REGION");
5756
private static final String SAAS_BOOST_ENV = System.getenv("SAAS_BOOST_ENV");
5857
private static final Aws4Signer SIG_V4 = Aws4Signer.create();
59-
private static SdkHttpClient HTTP_CLIENT = ApacheHttpClient.builder().build();
60-
//private static final StsClient sts = Utils.sdkClient(StsClient.builder(), StsClient.SERVICE_NAME);
58+
private static SdkHttpClient HTTP_CLIENT = UrlConnectionHttpClient.create();
6159
private static final StsClient sts = StsClient.builder()
6260
.httpClient(HTTP_CLIENT)
6361
.credentialsProvider(EnvironmentVariableCredentialsProvider.create())
@@ -97,17 +95,6 @@ private static String executeApiRequest(SdkHttpFullRequest apiRequest, SdkHttpFu
9795
HttpExecuteRequest.Builder requestBuilder = HttpExecuteRequest.builder().request(signedApiRequest != null ? signedApiRequest : apiRequest);
9896
apiRequest.contentStreamProvider().ifPresent(c -> requestBuilder.contentStreamProvider(c));
9997
HttpExecuteRequest apiExecuteRequest = requestBuilder.build();
100-
101-
// StringBuilder buffer = new StringBuilder();
102-
// for (Map.Entry<String, List<String>> header : apiExecuteRequest.httpRequest().headers().entrySet()) {
103-
// buffer.append(header.getKey());
104-
// buffer.append(": ");
105-
// buffer.append(header.getValue());
106-
// buffer.append("\n");
107-
// }
108-
// LOGGER.info(buffer.toString());
109-
110-
LOGGER.info("Calling REST API " + apiExecuteRequest.httpRequest().getUri());
11198
BufferedReader responseReader = null;
11299
String responseBody;
113100
try {

resources/saas-boost-private-api.yaml

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ Parameters:
4040
TenantServiceGetAll:
4141
Description: Tenant Service get all tenants Lambda ARN
4242
Type: String
43+
TenantServiceDelete:
44+
Description: Tenant Service delete tenant Lambda ARN
45+
Type: String
4346
SettingsServiceGetAll:
4447
Description: Settings Service get all settings Lambda ARN
4548
Type: String
@@ -201,6 +204,32 @@ Resources:
201204
Action: lambda:InvokeFunction
202205
FunctionName: !Ref TenantServiceGetAll
203206
SourceArn: !Sub arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${PrivateApi}/*/GET/tenants
207+
TenantServiceDeleteMethod:
208+
Type: AWS::ApiGateway::Method
209+
Properties:
210+
RestApiId: !Ref PrivateApi
211+
ResourceId: !Ref TenantServiceByIdResource
212+
HttpMethod: DELETE
213+
AuthorizationType: AWS_IAM
214+
RequestParameters: {method.request.path.id: true}
215+
Integration:
216+
Type: AWS_PROXY
217+
IntegrationHttpMethod: POST
218+
Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${TenantServiceDelete}/invocations
219+
PassthroughBehavior: WHEN_NO_MATCH
220+
RequestParameters: {integration.request.path.id: 'method.request.path.id'}
221+
MethodResponses:
222+
- StatusCode: '200'
223+
ResponseModels: {application/json: Empty}
224+
ResponseParameters:
225+
method.response.header.Access-Control-Allow-Origin: false
226+
TenantServiceDeleteLambdaPermission:
227+
Type: AWS::Lambda::Permission
228+
Properties:
229+
Principal: apigateway.amazonaws.com
230+
Action: lambda:InvokeFunction
231+
FunctionName: !Ref TenantServiceDelete
232+
SourceArn: !Sub arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${PrivateApi}/*/DELETE/tenants/{id}
204233
TenantServiceInsertMethod:
205234
Type: AWS::ApiGateway::Method
206235
Properties:
@@ -255,6 +284,36 @@ Resources:
255284
method.response.header.Access-Control-Allow-Origin: false
256285
method.response.header.Access-Control-Max-Age: false
257286
method.response.header.X-Requested-With: false
287+
TenantServiceByIdResourceCORS:
288+
Type: AWS::ApiGateway::Method
289+
Properties:
290+
RestApiId: !Ref PrivateApi
291+
ResourceId: !Ref TenantServiceByIdResource
292+
HttpMethod: OPTIONS
293+
AuthorizationType: NONE
294+
Integration:
295+
Type: MOCK
296+
PassthroughBehavior: WHEN_NO_MATCH
297+
IntegrationResponses:
298+
- StatusCode: '200'
299+
ResponseTemplates: {application/json: ''}
300+
ResponseParameters:
301+
method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'"
302+
method.response.header.Access-Control-Allow-Methods: "'GET,OPTIONS,DELETE'"
303+
method.response.header.Access-Control-Allow-Origin: "'*'"
304+
method.response.header.Access-Control-Max-Age: "'3600'"
305+
method.response.header.X-Requested-With: "'*'"
306+
RequestTemplates:
307+
application/json: '{"statusCode": 200}'
308+
MethodResponses:
309+
- StatusCode: '200'
310+
ResponseModels: {application/json: Empty}
311+
ResponseParameters:
312+
method.response.header.Access-Control-Allow-Headers: false
313+
method.response.header.Access-Control-Allow-Methods: false
314+
method.response.header.Access-Control-Allow-Origin: false
315+
method.response.header.Access-Control-Max-Age: false
316+
method.response.header.X-Requested-With: false
258317
SettingsServiceResource:
259318
Type: AWS::ApiGateway::Resource
260319
Properties:

resources/saas-boost.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -798,6 +798,7 @@ Resources:
798798
TenantServiceById: !GetAtt tenant.Outputs.TenantServiceByIdArn
799799
TenantServiceInsert: !GetAtt tenant.Outputs.TenantServiceInsertArn
800800
TenantServiceGetAll: !GetAtt tenant.Outputs.TenantServiceGetAllArn
801+
TenantServiceDelete: !GetAtt tenant.Outputs.TenantServiceDeleteArn
801802
SettingsServiceGetAll: !GetAtt settings.Outputs.SettingsServiceGetAllArn
802803
SettingsServiceGetSecret: !GetAtt settings.Outputs.SettingsServiceGetSecretArn
803804
SettingsServiceDeleteAppConfig: !GetAtt settings.Outputs.SettingsServiceDeleteAppConfigArn

0 commit comments

Comments
 (0)