mirror of
https://github.com/serverless/serverless.git
synced 2025-12-08 19:46:03 +00:00
1491 lines
47 KiB
JavaScript
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',
|
|
)
|
|
})
|
|
})
|