1345 lines
44 KiB
JavaScript

'use strict';
const chai = require('chai');
const sinon = require('sinon');
const runServerless = require('../../../../../utils/run-serverless');
chai.use(require('chai-as-promised'));
chai.use(require('sinon-chai'));
const expect = require('chai').expect;
describe('test/unit/lib/plugins/aws/deploy/index.test.js', () => {
const baseAwsRequestStubMap = {
STS: {
getCallerIdentity: {
ResponseMetadata: { RequestId: 'ffffffff-ffff-ffff-ffff-ffffffffffff' },
UserId: 'XXXXXXXXXXXXXXXXXXXXX',
Account: '999999999999',
Arn: 'arn:aws:iam::999999999999:user/test',
},
},
};
describe('with direct create/update calls', () => {
it('with nonexistent stack - first deploy', async () => {
const describeStacksStub = sinon
.stub()
.onFirstCall()
.throws('error', 'stack does not exist')
.onSecondCall()
.resolves({ Stacks: [{}] });
const createStackStub = sinon.stub().resolves({});
const updateStackStub = 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,
createStack: createStackStub,
updateStack: updateStackStub,
describeStackEvents: {
StackEvents: [
{
EventId: '1e2f3g4h',
StackName: 'new-service-dev',
LogicalResourceId: 'new-service-dev',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'CREATE_COMPLETE',
},
],
},
describeStackResource: {
StackResourceDetail: { PhysicalResourceId: 's3-bucket-resource' },
},
validateTemplate: {},
listStackResources: {},
},
};
await runServerless({
fixture: 'function',
command: 'deploy',
awsRequestStubMap,
configExt: {
provider: {
deploymentMethod: 'direct',
},
},
});
expect(createStackStub).to.be.calledOnce;
expect(updateStackStub).to.be.calledOnce;
const wasCloudFormationTemplateUploadInitiated = s3UploadStub.args.some((call) =>
call[0].Key.endsWith('compiled-cloudformation-template.json')
);
expect(wasCloudFormationTemplateUploadInitiated).to.be.true;
expect(deleteObjectsStub).not.to.be.called;
});
it('with nonexistent stack - first deploy with custom deployment bucket', async () => {
const describeStacksStub = sinon
.stub()
.onFirstCall()
.throws('error', 'stack does not exist')
.onSecondCall()
.resolves({ Stacks: [{}] });
const createStackStub = sinon.stub().resolves({});
const updateStackStub = 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: {},
getBucketLocation: () => {
return {
LocationConstraint: 'us-east-1',
};
},
},
CloudFormation: {
describeStacks: describeStacksStub,
createStack: createStackStub,
updateStack: updateStackStub,
describeStackEvents: {
StackEvents: [
{
EventId: '1e2f3g4h',
StackName: 'new-service-dev',
LogicalResourceId: 'new-service-dev',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'CREATE_COMPLETE',
},
],
},
validateTemplate: {},
listStackResources: {},
},
};
await runServerless({
fixture: 'function',
command: 'deploy',
awsRequestStubMap,
configExt: {
provider: {
deploymentBucket: 'existing-s3-bucket',
deploymentMethod: 'direct',
},
},
});
expect(createStackStub).to.be.calledOnce;
expect(updateStackStub).not.to.be.called;
const wasCloudFormationTemplateUploadInitiated = s3UploadStub.args.some((call) =>
call[0].Key.endsWith('compiled-cloudformation-template.json')
);
expect(wasCloudFormationTemplateUploadInitiated).to.be.true;
expect(deleteObjectsStub).not.to.be.called;
});
it('with existing stack - subsequent deploy', async () => {
const s3BucketPrefix = 'serverless/test-aws-deploy-with-existing-stack/dev';
const s3UploadStub = sinon.stub().resolves();
const createStackStub = sinon.stub().resolves({});
const updateStackStub = sinon.stub().resolves({});
const listObjectsV2Stub = sinon
.stub()
.onFirstCall()
.resolves({ Contents: [] })
.onSecondCall()
.resolves({
Contents: [
{
Key: `${s3BucketPrefix}/1589988704351-2020-05-20T15:31:44.359Z/compiled-cloudformation-template.json`,
},
{
Key: `${s3BucketPrefix}/1589988704351-2020-05-20T15:31:44.359Z/artifact.zip`,
},
{
Key: `${s3BucketPrefix}/1589988704352-2020-05-20T15:31:44.359Z/compiled-cloudformation-template.json`,
},
{
Key: `${s3BucketPrefix}/1589988704352-2020-05-20T15:31:44.359Z/artifact.zip`,
},
],
});
const deleteObjectsStub = sinon.stub().resolves();
const awsRequestStubMap = {
...baseAwsRequestStubMap,
ECR: {
describeRepositories: sinon.stub().throws({
providerError: { code: 'RepositoryNotFoundException' },
}),
},
S3: {
deleteObjects: deleteObjectsStub,
listObjectsV2: listObjectsV2Stub,
upload: s3UploadStub,
headBucket: {},
},
CloudFormation: {
describeStacks: { Stacks: [{}] },
createStack: createStackStub,
updateStack: updateStackStub,
describeChangeSet: {
ChangeSetName: 'new-service-dev-change-set',
ChangeSetId: 'some-change-set-id',
StackName: 'new-service-dev',
Status: 'CREATE_COMPLETE',
},
describeStackEvents: {
StackEvents: [
{
EventId: '1e2f3g4h',
StackName: 'new-service-dev',
LogicalResourceId: 'new-service-dev',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'UPDATE_COMPLETE',
},
],
},
describeStackResource: {
StackResourceDetail: { PhysicalResourceId: 's3-bucket-resource' },
},
validateTemplate: {},
listStackResources: {},
},
};
await runServerless({
fixture: 'function',
command: 'deploy',
awsRequestStubMap,
configExt: {
// Default, non-deterministic service-name invalidates this test as S3 Bucket cleanup relies on it
service: 'test-aws-deploy-with-existing-stack',
provider: {
deploymentMethod: 'direct',
deploymentBucket: {
maxPreviousDeploymentArtifacts: 1,
},
},
},
});
expect(createStackStub).not.to.be.called;
expect(updateStackStub).to.be.calledOnce;
const wasCloudFormationTemplateUploadInitiated = s3UploadStub.args.some((call) =>
call[0].Key.endsWith('compiled-cloudformation-template.json')
);
expect(wasCloudFormationTemplateUploadInitiated).to.be.true;
expect(deleteObjectsStub).to.be.calledWithExactly({
Bucket: 's3-bucket-resource',
Delete: {
Objects: [
{
Key: `${s3BucketPrefix}/1589988704351-2020-05-20T15:31:44.359Z/compiled-cloudformation-template.json`,
},
{ Key: `${s3BucketPrefix}/1589988704351-2020-05-20T15:31:44.359Z/artifact.zip` },
],
},
});
});
it('with existing stack - with deployment bucket resource missing from CloudFormation template', async () => {
const createStackStub = sinon.stub().resolves({});
const updateStackStub = sinon.stub().resolves({});
const describeStackResourceStub = sinon
.stub()
.onFirstCall()
.throws(() => {
const err = new Error('does not exist for stack');
err.providerError = {
code: 'ValidationError',
};
return err;
})
.onSecondCall()
.resolves({
StackResourceDetail: { PhysicalResourceId: 's3-bucket-resource' },
});
const awsRequestStubMap = {
...baseAwsRequestStubMap,
ECR: {
describeRepositories: sinon.stub().throws({
providerError: { code: 'RepositoryNotFoundException' },
}),
},
S3: {
listObjectsV2: { Contents: [] },
headBucket: () => {
const err = new Error();
err.code = 'AWS_S3_HEAD_BUCKET_NOT_FOUND';
throw err;
},
},
CloudFormation: {
describeStacks: { Stacks: [{}] },
validateTemplate: {},
createStack: createStackStub,
updateStack: updateStackStub,
getTemplate: () => {
return {
TemplateBody: JSON.stringify({}),
};
},
describeStackEvents: {
StackEvents: [
{
EventId: '1e2f3g4h',
StackName: 'new-service-dev',
LogicalResourceId: 'new-service-dev',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'UPDATE_COMPLETE',
},
],
},
describeStackResource: describeStackResourceStub,
},
};
const { serverless, awsNaming } = await runServerless({
fixture: 'function',
command: 'deploy',
awsRequestStubMap,
configExt: {
provider: {
deploymentMethod: 'direct',
},
},
lastLifecycleHookName: 'aws:deploy:deploy:checkForChanges',
});
expect(createStackStub).not.to.be.called;
expect(updateStackStub).to.be.calledWithExactly({
StackName: awsNaming.getStackName(),
Capabilities: ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'],
Parameters: [],
NotificationARNs: [],
Tags: [{ Key: 'STAGE', Value: 'dev' }],
TemplateBody: JSON.stringify({
Resources: serverless.service.provider.coreCloudFormationTemplate.Resources,
Outputs: serverless.service.provider.coreCloudFormationTemplate.Outputs,
}),
});
});
describe('custom deployment-related properties', () => {
let createStackStub;
let updateStackStub;
const deploymentRole = 'arn:xxx';
const notificationArns = ['arn:xxx', 'arn:yyy'];
const stackParameters = [
{
ParameterKey: 'key',
ParameterValue: 'val',
},
{
ParameterKey: 'key2',
ParameterValue: 'val2',
},
];
const stackPolicy = [
{
Effect: 'Allow',
Principal: '*',
Action: ['Update:*'],
Resource: '*',
},
];
const rollbackConfiguration = {
MonitoringTimeInMinutes: 20,
};
const disableRollback = true;
const stackTags = {
TAG: 'value',
ANOTHERTAG: 'anotherval',
};
before(async () => {
const describeStacksStub = sinon
.stub()
.onFirstCall()
.throws('error', 'stack does not exist')
.onSecondCall()
.resolves({ Stacks: [{}] });
createStackStub = sinon.stub().resolves({});
updateStackStub = sinon.stub().resolves({});
const awsRequestStubMap = {
...baseAwsRequestStubMap,
ECR: {
describeRepositories: sinon.stub().throws({
providerError: { code: 'RepositoryNotFoundException' },
}),
},
S3: {
deleteObjects: {},
listObjectsV2: { Contents: [] },
upload: {},
headBucket: {},
},
CloudFormation: {
describeStacks: describeStacksStub,
createStack: createStackStub,
updateStack: updateStackStub,
describeStackEvents: {
StackEvents: [
{
EventId: '1e2f3g4h',
StackName: 'new-service-dev',
LogicalResourceId: 'new-service-dev',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'CREATE_COMPLETE',
},
],
},
describeStackResource: {
StackResourceDetail: { PhysicalResourceId: 's3-bucket-resource' },
},
validateTemplate: {},
listStackResources: {},
},
};
await runServerless({
fixture: 'function',
command: 'deploy',
awsRequestStubMap,
configExt: {
provider: {
deploymentMethod: 'direct',
notificationArns,
rollbackConfiguration,
stackParameters,
stackPolicy,
stackTags,
disableRollback,
iam: {
deploymentRole,
},
},
},
});
});
it('should support custom deployment role', () => {
expect(createStackStub.getCall(0).args[0].RoleARN).to.equal(deploymentRole);
expect(updateStackStub.getCall(0).args[0].RoleARN).to.equal(deploymentRole);
});
it('should support `notificationsArns`', () => {
expect(createStackStub.getCall(0).args[0].NotificationARNs).to.deep.equal(notificationArns);
expect(updateStackStub.getCall(0).args[0].NotificationARNs).to.deep.equal(notificationArns);
});
it('should support `stackParameters`', () => {
expect(createStackStub.getCall(0).args[0].Parameters).to.deep.equal(stackParameters);
expect(updateStackStub.getCall(0).args[0].Parameters).to.deep.equal(stackParameters);
});
it('should support `stackPolicy`', () => {
expect(updateStackStub.getCall(0).args[0].StackPolicyBody).to.deep.equal(
JSON.stringify({ Statement: stackPolicy })
);
});
it('should support `rollbackConfiguration`', () => {
expect(updateStackStub.getCall(0).args[0].RollbackConfiguration).to.deep.equal(
rollbackConfiguration
);
});
it('should support `disableRollback`', () => {
expect(createStackStub.getCall(0).args[0].DisableRollback).to.be.true;
expect(updateStackStub.getCall(0).args[0].DisableRollback).to.be.true;
});
it('should support `stackTags`', () => {
expect(createStackStub.getCall(0).args[0].Tags).to.deep.equal([
{ Key: 'STAGE', Value: 'dev' },
{ Key: 'TAG', Value: 'value' },
{ Key: 'ANOTHERTAG', Value: 'anotherval' },
]);
expect(updateStackStub.getCall(0).args[0].Tags).to.deep.equal([
{ Key: 'STAGE', Value: 'dev' },
{ Key: 'TAG', Value: 'value' },
{ Key: 'ANOTHERTAG', Value: 'anotherval' },
]);
});
});
});
describe('with change-sets', () => {
it('with nonexistent stack - first deploy with custom deployment bucket', async () => {
const describeStacksStub = sinon
.stub()
.onFirstCall()
.throws('error', 'stack does not exist')
.onSecondCall()
.resolves({ Stacks: [{}] });
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: {},
getBucketLocation: () => {
return {
LocationConstraint: 'us-east-1',
};
},
},
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: {
StackEvents: [
{
EventId: '1e2f3g4h',
StackName: 'new-service-dev',
LogicalResourceId: 'new-service-dev',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'CREATE_COMPLETE',
},
],
},
validateTemplate: {},
listStackResources: {},
},
};
await runServerless({
fixture: 'function',
command: 'deploy',
awsRequestStubMap,
configExt: {
provider: {
deploymentBucket: 'existing-s3-bucket',
},
},
});
expect(createChangeSetStub).to.be.calledOnce;
expect(createChangeSetStub.getCall(0).args[0].ChangeSetType).to.equal('CREATE');
expect(executeChangeSetStub).to.be.calledOnce;
const wasCloudFormationTemplateUploadInitiated = s3UploadStub.args.some((call) =>
call[0].Key.endsWith('compiled-cloudformation-template.json')
);
expect(wasCloudFormationTemplateUploadInitiated).to.be.true;
expect(deleteObjectsStub).not.to.be.called;
});
it('with nonexistent stack - first deploy', async () => {
const describeStacksStub = sinon
.stub()
.onFirstCall()
.throws('error', 'stack does not exist')
.onSecondCall()
.resolves({ Stacks: [{}] });
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: {
StackEvents: [
{
EventId: '1e2f3g4h',
StackName: 'new-service-dev',
LogicalResourceId: 'new-service-dev',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'CREATE_COMPLETE',
},
],
},
describeStackResource: {
StackResourceDetail: { PhysicalResourceId: 's3-bucket-resource' },
},
validateTemplate: {},
listStackResources: {},
},
};
await runServerless({
fixture: 'function',
command: 'deploy',
awsRequestStubMap,
});
expect(createChangeSetStub).to.be.calledTwice;
expect(createChangeSetStub.getCall(0).args[0].ChangeSetType).to.equal('CREATE');
expect(createChangeSetStub.getCall(1).args[0].ChangeSetType).to.equal('UPDATE');
expect(executeChangeSetStub).to.be.calledTwice;
const wasCloudFormationTemplateUploadInitiated = s3UploadStub.args.some((call) =>
call[0].Key.endsWith('compiled-cloudformation-template.json')
);
expect(wasCloudFormationTemplateUploadInitiated).to.be.true;
expect(deleteObjectsStub).not.to.be.called;
});
it('with existing stack - subsequent deploy', async () => {
const s3BucketPrefix = 'serverless/test-aws-deploy-with-existing-stack/dev';
const s3UploadStub = sinon.stub().resolves();
const createChangeSetStub = sinon.stub().resolves({});
const executeChangeSetStub = sinon.stub().resolves({});
const listObjectsV2Stub = sinon
.stub()
.onFirstCall()
.resolves({ Contents: [] })
.onSecondCall()
.resolves({
Contents: [
{
Key: `${s3BucketPrefix}/1589988704351-2020-05-20T15:31:44.359Z/compiled-cloudformation-template.json`,
},
{
Key: `${s3BucketPrefix}/1589988704351-2020-05-20T15:31:44.359Z/artifact.zip`,
},
{
Key: `${s3BucketPrefix}/1589988704352-2020-05-20T15:31:44.359Z/compiled-cloudformation-template.json`,
},
{
Key: `${s3BucketPrefix}/1589988704352-2020-05-20T15:31:44.359Z/artifact.zip`,
},
],
});
const deleteObjectsStub = sinon.stub().resolves();
const awsRequestStubMap = {
...baseAwsRequestStubMap,
ECR: {
describeRepositories: sinon.stub().throws({
providerError: { code: 'RepositoryNotFoundException' },
}),
},
S3: {
deleteObjects: deleteObjectsStub,
listObjectsV2: listObjectsV2Stub,
upload: s3UploadStub,
headBucket: {},
},
CloudFormation: {
describeStacks: { Stacks: [{}] },
deleteChangeSet: {},
createChangeSet: createChangeSetStub,
executeChangeSet: executeChangeSetStub,
describeChangeSet: {
ChangeSetName: 'new-service-dev-change-set',
ChangeSetId: 'some-change-set-id',
StackName: 'new-service-dev',
Status: 'CREATE_COMPLETE',
},
describeStackEvents: {
StackEvents: [
{
EventId: '1e2f3g4h',
StackName: 'new-service-dev',
LogicalResourceId: 'new-service-dev',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'UPDATE_COMPLETE',
},
],
},
describeStackResource: {
StackResourceDetail: { PhysicalResourceId: 's3-bucket-resource' },
},
validateTemplate: {},
listStackResources: {},
},
};
await runServerless({
fixture: 'function',
command: 'deploy',
awsRequestStubMap,
configExt: {
// Default, non-deterministic service-name invalidates this test as S3 Bucket cleanup relies on it
service: 'test-aws-deploy-with-existing-stack',
provider: {
deploymentBucket: {
maxPreviousDeploymentArtifacts: 1,
},
},
},
});
expect(createChangeSetStub).to.be.calledOnce;
expect(createChangeSetStub.getCall(0).args[0].ChangeSetType).to.equal('UPDATE');
expect(executeChangeSetStub).to.be.calledOnce;
const wasCloudFormationTemplateUploadInitiated = s3UploadStub.args.some((call) =>
call[0].Key.endsWith('compiled-cloudformation-template.json')
);
expect(wasCloudFormationTemplateUploadInitiated).to.be.true;
expect(deleteObjectsStub).to.be.calledWithExactly({
Bucket: 's3-bucket-resource',
Delete: {
Objects: [
{
Key: `${s3BucketPrefix}/1589988704351-2020-05-20T15:31:44.359Z/compiled-cloudformation-template.json`,
},
{ Key: `${s3BucketPrefix}/1589988704351-2020-05-20T15:31:44.359Z/artifact.zip` },
],
},
});
});
it('with existing stack - subsequent deploy with empty changeset', async () => {
const createChangeSetStub = sinon.stub().resolves({});
const executeChangeSetStub = sinon.stub().resolves({});
const deleteChangeSetStub = sinon.stub().resolves();
const deleteObjectsStub = sinon.stub().resolves();
let objectsToRemove;
const listObjectsV2Stub = sinon
.stub()
.onFirstCall()
.resolves({ Contents: [] })
.onSecondCall()
.callsFake((params) => {
objectsToRemove = [
{
Key: `${params.Prefix}/compiled-cloudformation-template.json`,
},
{
Key: `${params.Prefix}/artifact.zip`,
},
];
return {
Contents: objectsToRemove,
};
});
const awsRequestStubMap = {
...baseAwsRequestStubMap,
ECR: {
describeRepositories: sinon.stub().throws({
providerError: { code: 'RepositoryNotFoundException' },
}),
},
S3: {
deleteObjects: deleteObjectsStub,
listObjectsV2: listObjectsV2Stub,
upload: {},
headBucket: {},
},
CloudFormation: {
describeStacks: { Stacks: [{}] },
deleteChangeSet: deleteChangeSetStub,
createChangeSet: createChangeSetStub,
executeChangeSet: executeChangeSetStub,
describeChangeSet: {
ChangeSetName: 'new-service-dev-change-set',
ChangeSetId: 'some-change-set-id',
StackName: 'new-service-dev',
Status: 'FAILED',
StatusReason: 'No updates are to be performed.',
},
describeStackResource: {
StackResourceDetail: { PhysicalResourceId: 's3-bucket-resource' },
},
validateTemplate: {},
listStackResources: {},
},
};
await runServerless({
fixture: 'function',
command: 'deploy',
awsRequestStubMap,
});
expect(createChangeSetStub).to.be.calledOnce;
expect(createChangeSetStub.getCall(0).args[0].ChangeSetType).to.equal('UPDATE');
expect(executeChangeSetStub).not.to.be.called;
expect(deleteChangeSetStub).to.be.calledTwice;
expect(deleteObjectsStub).to.be.calledWithExactly({
Bucket: 's3-bucket-resource',
Delete: { Objects: objectsToRemove },
});
});
it('should fail if cannot create a change set', async () => {
const awsRequestStubMap = {
...baseAwsRequestStubMap,
ECR: {
describeRepositories: sinon.stub().throws({
providerError: { code: 'RepositoryNotFoundException' },
}),
},
S3: {
deleteObjects: {},
listObjectsV2: { Contents: [] },
upload: {},
headBucket: {},
},
CloudFormation: {
describeStacks: { Stacks: [{}] },
deleteChangeSet: {},
createChangeSet: {},
executeChangeSet: {},
describeChangeSet: {
ChangeSetName: 'new-service-dev-change-set',
ChangeSetId: 'some-change-set-id',
StackName: 'new-service-dev',
Status: 'FAILED',
StatusReason: 'Some internal reason',
},
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_CLOUD_FORMATION_CHANGE_SET_CREATION_FAILED'
);
});
it('with existing stack - with deployment bucket resource missing from CloudFormation template', async () => {
const createChangeSetStub = sinon.stub().resolves({});
const executeChangeSetStub = sinon.stub().resolves({});
const describeStackResourceStub = sinon
.stub()
.onFirstCall()
.throws(() => {
const err = new Error('does not exist for stack');
err.providerError = {
code: 'ValidationError',
};
return err;
})
.onSecondCall()
.resolves({
StackResourceDetail: { PhysicalResourceId: 's3-bucket-resource' },
});
const awsRequestStubMap = {
...baseAwsRequestStubMap,
ECR: {
describeRepositories: sinon.stub().throws({
providerError: { code: 'RepositoryNotFoundException' },
}),
},
S3: {
listObjectsV2: { Contents: [] },
headBucket: () => {
const err = new Error();
err.code = 'AWS_S3_HEAD_BUCKET_NOT_FOUND';
throw err;
},
},
CloudFormation: {
describeStacks: { Stacks: [{}] },
validateTemplate: {},
deleteChangeSet: {},
createChangeSet: createChangeSetStub,
executeChangeSet: executeChangeSetStub,
describeChangeSet: {
ChangeSetName: 'new-service-dev-change-set',
ChangeSetId: 'some-change-set-id',
StackName: 'new-service-dev',
Status: 'CREATE_COMPLETE',
},
getTemplate: () => {
return {
TemplateBody: JSON.stringify({}),
};
},
describeStackEvents: {
StackEvents: [
{
EventId: '1e2f3g4h',
StackName: 'new-service-dev',
LogicalResourceId: 'new-service-dev',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'UPDATE_COMPLETE',
},
],
},
describeStackResource: describeStackResourceStub,
},
};
const { serverless, awsNaming } = await runServerless({
fixture: 'function',
command: 'deploy',
awsRequestStubMap,
lastLifecycleHookName: 'aws:deploy:deploy:checkForChanges',
});
expect(createChangeSetStub).to.be.calledWithExactly({
StackName: awsNaming.getStackName(),
ChangeSetName: awsNaming.getStackChangeSetName(),
ChangeSetType: 'UPDATE',
Capabilities: ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'],
Parameters: [],
NotificationARNs: [],
Tags: [{ Key: 'STAGE', Value: 'dev' }],
TemplateBody: JSON.stringify({
Resources: serverless.service.provider.coreCloudFormationTemplate.Resources,
Outputs: serverless.service.provider.coreCloudFormationTemplate.Outputs,
}),
});
expect(executeChangeSetStub).to.be.calledWithExactly({
StackName: awsNaming.getStackName(),
ChangeSetName: awsNaming.getStackChangeSetName(),
});
});
describe('custom deployment-related properties', () => {
let createChangeSetStub;
let executeChangeSetStub;
let setStackPolicyStub;
const deploymentRole = 'arn:xxx';
const notificationArns = ['arn:xxx', 'arn:yyy'];
const stackParameters = [
{
ParameterKey: 'key',
ParameterValue: 'val',
},
{
ParameterKey: 'key2',
ParameterValue: 'val2',
},
];
const stackPolicy = [
{
Effect: 'Allow',
Principal: '*',
Action: ['Update:*'],
Resource: '*',
},
];
const rollbackConfiguration = {
MonitoringTimeInMinutes: 20,
};
const disableRollback = true;
const stackTags = {
TAG: 'value',
ANOTHERTAG: 'anotherval',
};
before(async () => {
const describeStacksStub = sinon
.stub()
.onFirstCall()
.throws('error', 'stack does not exist')
.onSecondCall()
.resolves({ Stacks: [{}] });
createChangeSetStub = sinon.stub().resolves({});
executeChangeSetStub = sinon.stub().resolves({});
setStackPolicyStub = sinon.stub().resolves({});
const awsRequestStubMap = {
...baseAwsRequestStubMap,
ECR: {
describeRepositories: sinon.stub().throws({
providerError: { code: 'RepositoryNotFoundException' },
}),
},
S3: {
deleteObjects: {},
listObjectsV2: { Contents: [] },
upload: {},
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',
},
setStackPolicy: setStackPolicyStub,
describeStackEvents: {
StackEvents: [
{
EventId: '1e2f3g4h',
StackName: 'new-service-dev',
LogicalResourceId: 'new-service-dev',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'CREATE_COMPLETE',
},
],
},
describeStackResource: {
StackResourceDetail: { PhysicalResourceId: 's3-bucket-resource' },
},
validateTemplate: {},
listStackResources: {},
},
};
await runServerless({
fixture: 'function',
command: 'deploy',
awsRequestStubMap,
configExt: {
provider: {
notificationArns,
rollbackConfiguration,
stackParameters,
stackPolicy,
stackTags,
disableRollback,
iam: {
deploymentRole,
},
},
},
});
});
it('should support custom deployment role', () => {
expect(createChangeSetStub.getCall(0).args[0].RoleARN).to.equal(deploymentRole);
expect(createChangeSetStub.getCall(1).args[0].RoleARN).to.equal(deploymentRole);
});
it('should support `notificationsArns`', () => {
expect(createChangeSetStub.getCall(0).args[0].NotificationARNs).to.deep.equal(
notificationArns
);
expect(createChangeSetStub.getCall(1).args[0].NotificationARNs).to.deep.equal(
notificationArns
);
});
it('should support `stackParameters`', () => {
expect(createChangeSetStub.getCall(1).args[0].Parameters).to.deep.equal(stackParameters);
});
it('should support `stackPolicy`', () => {
expect(setStackPolicyStub.getCall(0).args[0].StackPolicyBody).to.equal(
JSON.stringify({ Statement: stackPolicy })
);
});
it('should only set `stackPolicy` after applying change set', () => {
expect(setStackPolicyStub).to.not.be.calledBefore(executeChangeSetStub);
});
it('should support `rollbackConfiguration`', () => {
expect(createChangeSetStub.getCall(1).args[0].RollbackConfiguration).to.deep.equal(
rollbackConfiguration
);
});
it('should support `disableRollback`', () => {
expect(executeChangeSetStub.getCall(0).args[0].DisableRollback).to.be.true;
expect(executeChangeSetStub.getCall(1).args[0].DisableRollback).to.be.true;
});
it('should support `stackTags`', () => {
expect(createChangeSetStub.getCall(0).args[0].Tags).to.deep.equal([
{ Key: 'STAGE', Value: 'dev' },
{ Key: 'TAG', Value: 'value' },
{ Key: 'ANOTHERTAG', Value: 'anotherval' },
]);
expect(createChangeSetStub.getCall(1).args[0].Tags).to.deep.equal([
{ Key: 'STAGE', Value: 'dev' },
{ Key: 'TAG', Value: 'value' },
{ Key: 'ANOTHERTAG', Value: 'anotherval' },
]);
});
});
});
it('with existing stack - should skip deploy if nothing changed', async () => {
const s3UploadStub = sinon.stub().resolves();
const listObjectsV2Stub = sinon.stub().resolves({
Contents: [
{
Key: 'serverless/test-package-artifact/dev/1589988704359-2020-05-20T15:31:44.359Z/compiled-cloudformation-template.json',
LastModified: new Date(),
ETag: '"5102a4cf710cae6497dba9e61b85d0a4"',
Size: 356,
StorageClass: 'STANDARD',
},
{
Key: 'serverless/test-package-artifact/dev/1589988704359-2020-05-20T15:31:44.359Z/serverless-state.json',
LastModified: new Date(),
ETag: '"5102a4cf710cae6497dba9e61b85d0a4"',
Size: 356,
StorageClass: 'STANDARD',
},
{
Key: 'serverless/test-package-artifact/dev/1589988704359-2020-05-20T15:31:44.359Z/my-own.zip',
LastModified: new Date(),
ETag: '"5102a4cf710cae6497dba9e61b85d0a4"',
Size: 356,
StorageClass: 'STANDARD',
},
],
});
const s3HeadObjectStub = sinon.stub();
s3HeadObjectStub
.withArgs({
Bucket: 's3-bucket-resource',
Key: 'serverless/test-package-artifact/dev/1589988704359-2020-05-20T15:31:44.359Z/compiled-cloudformation-template.json',
})
.returns({
Metadata: { filesha256: 'qxp+iwSTMhcRUfHzka4AE4XAWawS8GnEyBh1WpGb7Vw=' },
});
s3HeadObjectStub
.withArgs({
Bucket: 's3-bucket-resource',
Key: 'serverless/test-package-artifact/dev/1589988704359-2020-05-20T15:31:44.359Z/serverless-state.json',
})
.returns({
Metadata: { filesha256: 'JZ0oWM9ZWnYOxa3CRNeBRE5HAg+Q9RSwdxcKbik33d8=' },
});
s3HeadObjectStub
.withArgs({
Bucket: 's3-bucket-resource',
Key: 'serverless/test-package-artifact/dev/1589988704359-2020-05-20T15:31:44.359Z/my-own.zip',
})
.returns({
Metadata: { filesha256: 'T0qEYHOE4Xv2E8Ar03xGogAlElcdf/dQh/lh9ao7Glo=' },
});
const awsRequestStubMap = {
...baseAwsRequestStubMap,
S3: {
headObject: s3HeadObjectStub,
listObjectsV2: listObjectsV2Stub,
upload: s3UploadStub,
headBucket: {},
},
CloudFormation: {
describeStacks: { Stacks: [{}] },
describeStackEvents: {
StackEvents: [
{
EventId: '1e2f3g4h',
StackName: 'new-service-dev',
LogicalResourceId: 'new-service-dev',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'UPDATE_COMPLETE',
},
],
},
describeStackResource: {
StackResourceDetail: { PhysicalResourceId: 's3-bucket-resource' },
},
validateTemplate: {},
listStackResources: {},
},
};
const { serverless } = await runServerless({
fixture: 'package-artifact-in-serverless-dir',
command: 'deploy',
awsRequestStubMap,
configExt: {
// Default, non-deterministic service-name invalidates this test
service: 'test-aws-deploy-should-be-skipped',
},
});
expect(serverless.service.provider.shouldNotDeploy).to.be.true;
expect(s3UploadStub).to.not.be.called;
});
it('with existing stack - missing custom deployment bucket', async () => {
const awsRequestStubMap = {
...baseAwsRequestStubMap,
ECR: {
describeRepositories: sinon.stub().throws({
providerError: { code: 'RepositoryNotFoundException' },
}),
},
S3: {
getBucketLocation: () => {
throw new Error();
},
},
CloudFormation: {
describeStacks: { Stacks: [{}] },
validateTemplate: {},
},
};
await expect(
runServerless({
fixture: 'function',
command: 'deploy',
awsRequestStubMap,
lastLifecycleHookName: 'aws:deploy:deploy:checkForChanges',
configExt: {
provider: {
deploymentBucket: 'bucket-name',
},
},
})
).to.eventually.have.been.rejected.and.have.property('code', 'DEPLOYMENT_BUCKET_NOT_FOUND');
});
it('with existing stack - with custom deployment bucket in different region', async () => {
const awsRequestStubMap = {
...baseAwsRequestStubMap,
ECR: {
describeRepositories: sinon.stub().throws({
providerError: { code: 'RepositoryNotFoundException' },
}),
},
S3: {
getBucketLocation: () => {
return {
LocationConstraint: 'us-west-1',
};
},
},
CloudFormation: {
describeStacks: { Stacks: [{}] },
validateTemplate: {},
},
};
await expect(
runServerless({
fixture: 'function',
command: 'deploy',
awsRequestStubMap,
lastLifecycleHookName: 'aws:deploy:deploy:checkForChanges',
configExt: {
provider: {
deploymentBucket: 'bucket-name',
},
},
})
).to.eventually.have.been.rejected.and.have.property(
'code',
'DEPLOYMENT_BUCKET_INVALID_REGION'
);
});
it('with existing stack - with deployment bucket from CloudFormation deleted manually', async () => {
const awsRequestStubMap = {
...baseAwsRequestStubMap,
ECR: {
describeRepositories: sinon.stub().throws({
providerError: { code: 'RepositoryNotFoundException' },
}),
},
S3: {
headBucket: () => {
const err = new Error();
err.code = 'AWS_S3_HEAD_BUCKET_NOT_FOUND';
throw err;
},
},
CloudFormation: {
describeStacks: { Stacks: [{}] },
validateTemplate: {},
describeStackResource: {
StackResourceDetail: { PhysicalResourceId: 's3-bucket-resource' },
},
},
};
await expect(
runServerless({
fixture: 'function',
command: 'deploy',
awsRequestStubMap,
lastLifecycleHookName: 'aws:deploy:deploy:checkForChanges',
})
).to.eventually.have.been.rejected.and.have.property(
'code',
'DEPLOYMENT_BUCKET_REMOVED_MANUALLY'
);
});
});