2024-05-29 11:51:04 -04:00

1491 lines
47 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 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()
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: 'sazQTKx8BgZJIMV2cJhXcOT68Q8KaP9mHdI9C2dST40=',
},
})
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: 'fSU2tLfTe72BW+k8hJvm6VkzHtssCtrTG+uqGGg4YzI=',
},
})
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',
)
})
it('should throw when deployment bucket cannot be accessed', async () => {
const awsRequestStubMap = {
...baseAwsRequestStubMap,
ECR: {
describeRepositories: sinon.stub().throws({
providerError: { code: 'RepositoryNotFoundException' },
}),
},
S3: {
headBucket: () => {
const err = new Error()
err.code = 'AWS_S3_HEAD_BUCKET_FORBIDDEN'
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',
'AWS_S3_HEAD_BUCKET_FORBIDDEN',
)
})
})