Skip to content
This repository has been archived by the owner on Dec 6, 2024. It is now read-only.

Commit

Permalink
fix: Don't delete ALB if another instance is starting (#797)
Browse files Browse the repository at this point in the history
  • Loading branch information
nguyen102 authored Nov 12, 2021
1 parent 07d03e3 commit 079e11c
Show file tree
Hide file tree
Showing 3 changed files with 224 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,14 @@ describe('TerminateLaunchDependencyStep', () => {
});

describe('checkAndTerminateAlb', () => {
let origCheckPendingEnvWithSSLCertFn;
beforeAll(() => {
origCheckPendingEnvWithSSLCertFn = step.checkPendingEnvWithSSLCert;
step.checkPendingEnvWithSSLCert = jest.fn(() => Promise.resolve(true));
});
afterAll(() => {
step.checkPendingEnvWithSSLCert = origCheckPendingEnvWithSSLCertFn;
});
it('should throw error when project is not valid', async () => {
albService.albDependentWorkspacesCount.mockImplementationOnce(() => {
throw new Error('project with id "test-project" does not exist');
Expand All @@ -408,6 +416,7 @@ describe('TerminateLaunchDependencyStep', () => {
await step.checkAndTerminateAlb('test-project-id', 'test-external-id');
// CHECK
expect(step.terminateStack).not.toHaveBeenCalled();
jest.clearAllMocks();
});

it('should skip alb termination when alb does not exist', async () => {
Expand All @@ -423,6 +432,25 @@ describe('TerminateLaunchDependencyStep', () => {
expect(step.terminateStack).not.toHaveBeenCalled();
});

it('should not call alb termination when there are pending env with SSL Cert', async () => {
// BUILD
albService.albDependentWorkspacesCount.mockImplementationOnce(() => {
return 0;
});
albService.checkAlbExists.mockImplementationOnce(() => {
return true;
});
step.checkPendingEnvWithSSLCert = jest.fn(() => Promise.resolve(true));

jest.spyOn(step, 'terminateStack').mockImplementationOnce(() => {});

// OPERATE
await step.checkAndTerminateAlb('test-project-id', 'test-external-id');

// CHECK
expect(step.terminateStack).not.toHaveBeenCalled();
});

it('should call alb termination when count <= 0', async () => {
albService.albDependentWorkspacesCount.mockImplementationOnce(() => {
return 0;
Expand Down Expand Up @@ -585,4 +613,146 @@ describe('TerminateLaunchDependencyStep', () => {
expect(pluginRegistryService.visitPlugins).toHaveBeenCalled();
});
});

describe('checkPendingEnvWithSSLCert', () => {
it('should return false since there are no pending environment', async () => {
// BUILD
const envScService = {};
envScService.list = jest.fn(() => Promise.resolve([]));

const envTypeService = {};
envTypeService.mustFind = jest.fn(() => Promise.resolve({}));

// OPERATE, CHECK
await expect(step.checkPendingEnvWithSSLCert(envScService, envTypeService, requestContext)).resolves.toEqual(
false,
);
});

it('should return false since there are NO pending environment with SSL cert', async () => {
// BUILD
const envScService = {};
envScService.list = jest.fn(() =>
Promise.resolve([
{
id: 'abc',
rev: 0,
projectId: 'proj-123',
inWorkflow: 'true',
status: 'PENDING',
createdAt: '2021-11-11T05:47:39.178Z',
cidr: '0.0.0.0/0',
updatedBy: 'u-zBpBkLuXjdDbdUAHalfY7',
createdBy: 'u-zBpBkLuXjdDbdUAHalfY7',
name: 'rstudio-6',
studyIds: [],
updatedAt: '2021-11-11T05:47:39.178Z',
indexId: 'index-123',
description: 'RStudio service workspace',
envTypeConfigId: 'config-1',
envTypeId: 'prod-xyz',
},
]),
);

const envTypeService = {};
envTypeService.mustFind = jest.fn(() =>
Promise.resolve({
id: 'prod-xyz',
product: {
productId: 'prod-n52qqfqv6bmya',
},
rev: 1,
status: 'approved',
createdAt: '2021-11-09T18:06:56.944Z',
updatedBy: 'u-zBpBkLuXjdDbdUAHalfY7',
createdBy: 'u-zBpBkLuXjdDbdUAHalfY7',
name: 'Sample Workspace Type',
desc: '',
provisioningArtifact: {
id: 'pa-7udayuv3syfo6',
},
params: [
{
IsNoEcho: false,
ParameterConstraints: {
AllowedValues: [],
},
ParameterType: 'String',
Description: 'The ARN of the KMS encryption Key used to encrypt data in the instance',
ParameterKey: 'EncryptionKeyArn',
},
],
}),
);

// OPERATE, CHECK
await expect(step.checkPendingEnvWithSSLCert(envScService, envTypeService, requestContext)).resolves.toEqual(
false,
);
});

it('should return true since there are pending environment with SSL cert', async () => {
// BUILD
const envScService = {};
envScService.list = jest.fn(() =>
Promise.resolve([
{
id: 'abc',
rev: 0,
projectId: 'proj-123',
inWorkflow: 'true',
status: 'PENDING',
createdAt: '2021-11-11T05:47:39.178Z',
cidr: '0.0.0.0/32',
updatedBy: 'u-zBpBkLuXjdDbdUAHalfY7',
createdBy: 'u-zBpBkLuXjdDbdUAHalfY7',
name: 'rstudio-6',
studyIds: [],
updatedAt: '2021-11-11T05:47:39.178Z',
indexId: 'index-123',
description: 'RStudio service workspace',
envTypeConfigId: 'config-1',
envTypeId: 'prod-xyz',
},
]),
);

const envTypeService = {};
envTypeService.mustFind = jest.fn(() =>
Promise.resolve({
id: 'prod-xyz',
product: {
productId: 'prod-n52qqfqv6bmya',
},
rev: 1,
status: 'approved',
createdAt: '2021-11-09T18:06:56.944Z',
updatedBy: 'u-zBpBkLuXjdDbdUAHalfY7',
createdBy: 'u-zBpBkLuXjdDbdUAHalfY7',
name: 'Sample Workspace Type',
desc: '',
provisioningArtifact: {
id: 'pa-7udayuv3syfo6',
},
params: [
{
IsNoEcho: false,
ParameterConstraints: {
AllowedValues: [],
},
ParameterType: 'String',
Description: 'The ARN of the AWS Certificate Manager SSL Certificate to associate with the Load Balancer',
ParameterKey: 'ACMSSLCertARN',
},
],
}),
);

// OPERATE, CHECK
await expect(step.checkPendingEnvWithSSLCert(envScService, envTypeService, requestContext)).resolves.toEqual(
true,
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -187,10 +187,19 @@ class TerminateLaunchDependency extends StepBase {
* @returns {Promise<>}
*/
async checkAndTerminateAlb(requestContext, projectId, externalId) {
const [albService] = await this.mustFindServices(['albService']);
const [albService, environmentScService, envTypeService] = await this.mustFindServices([
'albService',
'environmentScService',
'envTypeService',
]);
const count = await albService.albDependentWorkspacesCount(requestContext, projectId);
const albExists = await albService.checkAlbExists(requestContext, projectId);
if (count === 0 && albExists) {
const pendingEnvWithSSLCert = await this.checkPendingEnvWithSSLCert(
environmentScService,
envTypeService,
requestContext,
);
if (count === 0 && albExists && !pendingEnvWithSSLCert) {
this.print({
msg: 'Last ALB Dependent workspace is being terminated. Terminating ALB',
});
Expand All @@ -207,6 +216,33 @@ class TerminateLaunchDependency extends StepBase {
return null;
}

async checkPendingEnvWithSSLCert(environmentScService, envTypeService, requestContext) {
const envs = await environmentScService.list(requestContext);
const pendingEnvTypeIds = envs
.filter(env => {
return env.status === environmentStatusEnum.PENDING;
})
.map(env => {
return env.envTypeId;
});
const envTypeOfPendingEnvs = await Promise.all(
pendingEnvTypeIds.map(envTypeId => {
return envTypeService.mustFind(requestContext, { id: envTypeId });
}),
);
const response = envTypeOfPendingEnvs.some(envType => {
if (envType.params) {
return (
envType.params.find(param => {
return param.ParameterKey === 'ACMSSLCertARN';
}) !== undefined
);
}
return false;
});
return response;
}

/**
* Method to terminate a cfn stack
*
Expand Down
20 changes: 16 additions & 4 deletions main/solution/infrastructure/config/infra/cloudformation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ Conditions:
IsDev: !Equals ['${self:custom.settings.envType}', 'dev']
UseCustomDomain: !Not
- !Equals ['${self:custom.settings.domainName}', '']
UseHostedZoneId: !Not
- !Equals ['${self:custom.settings.hostedZoneId}', '']
CustomDomainWithoutHostedZoneId: !And
- !Not
- !Equals ['${self:custom.settings.domainName}', '']
- !Equals ['${self:custom.settings.hostedZoneId}', '']

Description: (SO0144) Service Workbench on AWS Solution

Expand Down Expand Up @@ -168,7 +174,7 @@ Resources:

HostedZone:
Type: AWS::Route53::HostedZone
Condition: UseCustomDomain
Condition: CustomDomainWithoutHostedZoneId
DeletionPolicy: Retain
Properties:
Name: ${self:custom.settings.domainName}
Expand All @@ -177,7 +183,12 @@ Resources:
Type: AWS::Route53::RecordSetGroup
Condition: UseCustomDomain
Properties:
HostedZoneId: !Ref HostedZone
# Blank hosted zone ID in stage file creates a new one
# This is to ensure backwards compatibility
HostedZoneId: !If
- UseHostedZoneId
- ${self:custom.settings.hostedZoneId}
- !Ref HostedZone
RecordSets:
- Name: ${self:custom.settings.domainName}
Type: A
Expand Down Expand Up @@ -206,8 +217,9 @@ Outputs:
Value: !Ref WebsiteCloudFront

HostedZoneId:
Condition: UseCustomDomain
Description: Id of the hosted zone created when a custom domain is used
Value: !If
- UseCustomDomain
- UseHostedZoneId
- ${self:custom.settings.hostedZoneId}
- !Ref HostedZone
- 'NotSetAsCustomDomainDisabled'

0 comments on commit 079e11c

Please sign in to comment.