Skip to content

Commit

Permalink
fix(AWS Deploy): Fix handling of inactive CloudFormation stack state
Browse files Browse the repository at this point in the history
(PR #11863)
  • Loading branch information
archepro84 committed Mar 29, 2023
1 parent 048ffad commit 0d850fc
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 7 deletions.
16 changes: 15 additions & 1 deletion lib/plugins/aws/deploy/lib/create-stack.js
@@ -1,10 +1,12 @@
'use strict';

const BbPromise = require('bluebird');
const ServerlessError = require('../../../../serverless-error');
const { progress, log } = require('@serverless/utils/log');
const ServerlessError = require('../../../../serverless-error');
const isChangeSetWithoutChanges = require('../../utils/is-change-set-without-changes');

const inactiveStateNames = new Set(['REVIEW_IN_PROGRESS']);

module.exports = {
async create() {
// Note: using three dots instead of ellipsis to support non uni-code consoles.
Expand Down Expand Up @@ -88,6 +90,18 @@ module.exports = {
this.provider.disableTransferAccelerationForCurrentDeploy();
}
}

const stackStatus = data.Stacks[0].StackStatus;
if (inactiveStateNames.has(stackStatus)) {
const errorMessage = [
'Service cannot be deployed as the CloudFormation stack ',
`is in the '${stackStatus}' state. `,
'This may signal either that stack is currently deployed by a different entity, ',
'or that the previous deployment failed and was left in an abnormal state, ',
"in which case you can mitigate the issue by running 'sls remove' command",
].join('');
throw new ServerlessError(errorMessage, 'AWS_CLOUDFORMATION_INACTIVE_STACK');
}
return BbPromise.resolve('alreadyCreated');
})
)
Expand Down
57 changes: 57 additions & 0 deletions test/unit/lib/plugins/aws/deploy/index.test.js
Expand Up @@ -651,6 +651,63 @@ describe('test/unit/lib/plugins/aws/deploy/index.test.js', () => {
expect(deleteObjectsStub).not.to.be.called;
});

it('with nonexistent stack - should output an appropriate error message for an abnormal stack state', async () => {
const describeStacksStub = sinon
.stub()
.onFirstCall()
.resolves({
Stacks: [
{
StackStatus: 'REVIEW_IN_PROGRESS',
},
],
});
const createChangeSetStub = sinon.stub().resolves({});
const executeChangeSetStub = sinon.stub().resolves({});
const s3UploadStub = sinon.stub().resolves();
const deleteObjectsStub = sinon.stub().resolves({});
const awsRequestStubMap = {
...baseAwsRequestStubMap,
ECR: {
describeRepositories: sinon.stub().throws({
providerError: { code: 'RepositoryNotFoundException' },
}),
},
S3: {
deleteObjects: deleteObjectsStub,
listObjectsV2: { Contents: [] },
upload: s3UploadStub,
headBucket: {},
},
CloudFormation: {
describeStacks: describeStacksStub,
createChangeSet: createChangeSetStub,
executeChangeSet: executeChangeSetStub,
deleteChangeSet: {},
describeChangeSet: {
ChangeSetName: 'new-service-dev-change-set',
ChangeSetId: 'some-change-set-id',
StackName: 'new-service-dev',
Status: 'CREATE_COMPLETE',
},
describeStackEvents: {},
describeStackResource: {
StackResourceDetail: { PhysicalResourceId: 's3-bucket-resource' },
},
validateTemplate: {},
listStackResources: {},
},
};

await expect(
runServerless({
fixture: 'function',
command: 'deploy',
awsRequestStubMap,
})
).to.have.been.eventually.rejected.with.property('code', 'AWS_CLOUDFORMATION_INACTIVE_STACK');
});

it('with existing stack - subsequent deploy', async () => {
const s3BucketPrefix = 'serverless/test-aws-deploy-with-existing-stack/dev';
const s3UploadStub = sinon.stub().resolves();
Expand Down
Expand Up @@ -571,7 +571,7 @@ describe('checkForChanges #2', () => {
lastLifecycleHookName: 'aws:deploy:deploy:checkForChanges',
awsRequestStubMap: {
CloudFormation: {
describeStacks: {},
describeStacks: { Stacks: [{}] },
describeStackResource: {
StackResourceDetail: { PhysicalResourceId: 'deployment-bucket' },
},
Expand Down Expand Up @@ -617,7 +617,7 @@ describe('checkForChanges #2', () => {

const commonAwsSdkMock = {
CloudFormation: {
describeStacks: {},
describeStacks: { Stacks: [{}] },
describeStackResource: {
StackResourceDetail: { PhysicalResourceId: 'deployment-bucket' },
},
Expand Down
7 changes: 5 additions & 2 deletions test/unit/lib/plugins/aws/deploy/lib/upload-artifacts.test.js
Expand Up @@ -307,7 +307,10 @@ describe('uploadArtifacts', () => {
// There were observed race conditions (mostly in Node.js v6) where this temporary home
// folder was cleaned before stream initialized fully, hence throwing uncaught
// ENOENT exception into the air.
sinon.stub(fs, 'createReadStream').returns({ path: customResourcesFilePath, on: () => {} });
sinon.stub(fs, 'createReadStream').returns({
path: customResourcesFilePath,
on: () => {},
});
serverless.serviceDir = serviceDirPath;
});

Expand Down Expand Up @@ -351,7 +354,7 @@ describe('test/unit/lib/plugins/aws/deploy/lib/upload-artifacts.test.js', () =>
lastLifecycleHookName: 'aws:deploy:deploy:uploadArtifacts',
awsRequestStubMap: {
CloudFormation: {
describeStacks: {},
describeStacks: { Stacks: [{}] },
describeStackResource: {
StackResourceDetail: { PhysicalResourceId: 'deployment-bucket' },
},
Expand Down
Expand Up @@ -771,7 +771,7 @@ describe('test/unit/lib/plugins/aws/package/compile/events/apiGateway/lib/hack/u
lastLifecycleHookName: 'aws:deploy:deploy:checkForChanges',
awsRequestStubMap: {
CloudFormation: {
describeStacks: {},
describeStacks: { Stacks: [{}] },
describeStackResource: {
StackResourceDetail: { PhysicalResourceId: 'deployment-bucket' },
},
Expand Down
2 changes: 1 addition & 1 deletion test/unit/lib/plugins/package/lib/package-service.test.js
Expand Up @@ -289,7 +289,7 @@ describe('test/unit/lib/plugins/package/lib/packageService.test.js', () => {
headBucket: {},
},
CloudFormation: {
describeStacks: {},
describeStacks: { Stacks: [{}] },
describeStackResource: { StackResourceDetail: { PhysicalResourceId: 'resource-id' } },
},
STS: {
Expand Down

0 comments on commit 0d850fc

Please sign in to comment.