Skip to content

Commit

Permalink
fix(AWS Deploy): Allow removal of stack with bucket missing
Browse files Browse the repository at this point in the history
  • Loading branch information
pgrzesik committed Dec 7, 2021
1 parent f12c7d8 commit 1a85a4a
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 6 deletions.
18 changes: 18 additions & 0 deletions lib/plugins/aws/lib/check-if-bucket-exists.js
@@ -0,0 +1,18 @@
'use strict';

module.exports = {
async checkIfBucketExists(bucketName) {
try {
await this.provider.request('S3', 'headBucket', {
Bucket: bucketName,
});
return true;
} catch (err) {
if (err.code === 'AWS_S3_HEAD_BUCKET_NOT_FOUND') {
return false;
}

throw err;
}
},
};
4 changes: 3 additions & 1 deletion lib/plugins/aws/remove/index.js
Expand Up @@ -2,6 +2,7 @@

const validate = require('../lib/validate');
const monitorStack = require('../lib/monitorStack');
const checkIfBucketExists = require('../lib/check-if-bucket-exists');
const emptyS3Bucket = require('./lib/bucket');
const removeStack = require('./lib/stack');
const removeEcrRepository = require('./lib/ecr');
Expand All @@ -23,7 +24,8 @@ class AwsRemove {
removeStack,
monitorStack,
removeEcrRepository,
checkIfEcrRepositoryExists
checkIfEcrRepositoryExists,
checkIfBucketExists
);

this.hooks = {
Expand Down
26 changes: 21 additions & 5 deletions lib/plugins/aws/remove/lib/bucket.js
@@ -1,11 +1,22 @@
'use strict';

const { legacy } = require('@serverless/utils/log');
const { legacy, log } = require('@serverless/utils/log');

module.exports = {
async setServerlessDeploymentBucketName() {
const bucketName = await this.provider.getServerlessDeploymentBucketName();
this.bucketName = bucketName;
try {
const bucketName = await this.provider.getServerlessDeploymentBucketName();
this.bucketName = bucketName;
} catch (err) {
// If there is a validation error with expected message, it means that logical resource for
// S3 bucket does not exist and we want to proceed with empty `bucketName`
if (
err.providerError.code !== 'ValidationError' ||
!err.message.includes('does not exist for stack')
) {
throw err;
}
}
},

async listObjectsV2() {
Expand Down Expand Up @@ -81,7 +92,12 @@ module.exports = {

async emptyS3Bucket() {
await this.setServerlessDeploymentBucketName();
await this.listObjects();
await this.deleteObjects();
if (this.bucketName && (await this.checkIfBucketExists(this.bucketName))) {
await this.listObjects();
await this.deleteObjects();
} else {
legacy.log('S3 bucket not found. Skipping S3 bucket objects removal');
log.info('S3 bucket not found. Skipping S3 bucket objects removal');
}
},
};
74 changes: 74 additions & 0 deletions test/unit/lib/plugins/aws/remove/index.test.js
Expand Up @@ -123,6 +123,80 @@ describe('test/unit/lib/plugins/aws/remove/index.test.js', () => {
expect(deleteObjectsStub).not.to.be.called;
});

it('executes expected operations related to files removal when S3 bucket is empty', async () => {
await runServerless({
fixture: 'function',
command: 'remove',
awsRequestStubMap,
});

expect(deleteObjectsStub).to.be.calledWithExactly({
Bucket: 'resource-id',
Delete: {
Objects: [{ Key: 'first' }, { Key: 'second' }],
},
});
});

it('skips attempts to remove S3 objects if S3 bucket not found', async () => {
const { awsNaming } = await runServerless({
fixture: 'function',
command: 'remove',
awsRequestStubMap: {
...awsRequestStubMap,
S3: {
deleteObjects: deleteObjectsStub,
listObjectsV2: { Contents: [{ Key: 'first' }, { Key: 'second' }] },
headBucket: () => {
const err = new Error('err');
err.code = 'AWS_S3_HEAD_BUCKET_NOT_FOUND';
throw err;
},
},
},
});

expect(deleteObjectsStub).not.to.be.called;
expect(deleteStackStub).to.be.calledWithExactly({ StackName: awsNaming.getStackName() });
expect(describeStackEventsStub).to.be.calledWithExactly({
StackName: awsNaming.getStackName(),
});
expect(describeStackEventsStub.calledAfter(deleteStackStub)).to.be.true;
});

it('skips attempts to remove S3 objects if S3 bucket resource missing from CloudFormation template', async () => {
const headBucketStub = sinon.stub();
const { awsNaming } = await runServerless({
fixture: 'function',
command: 'remove',
awsRequestStubMap: {
...awsRequestStubMap,
S3: {
...awsRequestStubMap.S3,
headBucket: headBucketStub,
},
CloudFormation: {
...awsRequestStubMap.CloudFormation,
describeStackResource: () => {
const err = new Error('does not exist for stack');
err.providerError = {
code: 'ValidationError',
};
throw err;
},
},
},
});

expect(headBucketStub).not.to.be.called;
expect(deleteObjectsStub).not.to.be.called;
expect(deleteStackStub).to.be.calledWithExactly({ StackName: awsNaming.getStackName() });
expect(describeStackEventsStub).to.be.calledWithExactly({
StackName: awsNaming.getStackName(),
});
expect(describeStackEventsStub.calledAfter(deleteStackStub)).to.be.true;
});

it('removes ECR repository if it exists', async () => {
describeRepositoriesStub.resolves();
const { awsNaming } = await runServerless({
Expand Down

0 comments on commit 1a85a4a

Please sign in to comment.