Merge pull request #2189 from VivintSolar/deploy

Add a bucket to the provider for deployments.
This commit is contained in:
Eslam λ Hefnawy 2016-10-03 23:32:12 +07:00 committed by GitHub
commit 34ff43373c
24 changed files with 920 additions and 423 deletions

View File

@ -22,9 +22,13 @@ provider:
runtime: nodejs4.3 # Runtime used for all functions in this provider
stage: dev # Set the default stage used. Default is dev
region: us-east-1 # Overwrite the default region used. Default is us-east-1
deploymentBucket: com.serverless.${self:provider.region}.deploys # Overwrite the default deployment bucket
variableSyntax: '\${{([\s\S]+?)}}' # Overwrite the default "${}" variable syntax to be "${{}}" instead. This can be helpful if you want to use "${}" as a string without using it as a variable.
```
### Deployment S3Bucket
The bucket must exist beforehand and be in the same region as the deploy.
## Additional function configuration
```yaml

View File

@ -2,10 +2,11 @@
const chalk = require('chalk');
module.exports.SError = class ServerlessError extends Error {
constructor(message) {
constructor(message, statusCode) {
super(message);
this.name = this.constructor.name;
this.message = message;
this.statusCode = statusCode;
Error.captureStackTrace(this, this.constructor);
}
};

View File

@ -9,185 +9,139 @@ class AwsCompileFunctions {
this.options = options;
this.provider = 'aws';
this.compileFunctions = this.compileFunctions.bind(this);
this.compileFunction = this.compileFunction.bind(this);
this.hooks = {
'deploy:compileFunctions': this.compileFunctions.bind(this),
'deploy:compileFunctions': this.compileFunctions,
};
}
compileFunctions() {
if (typeof this.serverless.service.provider.iamRoleARN !== 'string') {
// merge in the iamRoleLambdaTemplate
const iamRoleLambdaExecutionTemplate = this.serverless.utils.readFileSync(
path.join(this.serverless.config.serverlessPath,
'plugins',
'aws',
'deploy',
'compile',
'functions',
'iam-role-lambda-execution-template.json')
);
compileFunction(functionName) {
const newFunction = this.cfLambdaFunctionTemplate();
const functionObject = this.serverless.service.getFunction(functionName);
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources,
iamRoleLambdaExecutionTemplate);
const artifactFilePath = this.serverless.service.package.individually ?
functionObject.artifact :
this.serverless.service.package.artifact;
// merge in the iamPolicyLambdaTemplate
const iamPolicyLambdaExecutionTemplate = this.serverless.utils.readFileSync(
path.join(this.serverless.config.serverlessPath,
'plugins',
'aws',
'deploy',
'compile',
'functions',
'iam-policy-lambda-execution-template.json')
);
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources,
iamPolicyLambdaExecutionTemplate);
// set the necessary variables for the IamPolicyLambda
this.serverless.service.provider.compiledCloudFormationTemplate
.Resources
.IamPolicyLambdaExecution
.Properties
.PolicyName = `${this.options.stage}-${this.serverless.service.service}-lambda`;
this.serverless.service.provider.compiledCloudFormationTemplate
.Resources
.IamPolicyLambdaExecution
.Properties
.PolicyDocument
.Statement[0]
.Resource = `arn:aws:logs:${this.options.region}:*:*`;
// add custom iam role statements
if (this.serverless.service.provider.iamRoleStatements &&
this.serverless.service.provider.iamRoleStatements instanceof Array) {
this.serverless.service.provider.compiledCloudFormationTemplate
.Resources
.IamPolicyLambdaExecution
.Properties
.PolicyDocument
.Statement = this.serverless.service.provider.compiledCloudFormationTemplate
.Resources
.IamPolicyLambdaExecution
.Properties
.PolicyDocument
.Statement.concat(this.serverless.service.provider.iamRoleStatements);
}
if (!artifactFilePath) {
throw new Error(`No artifact path is set for function: ${functionName}`);
}
const functionTemplate = `
{
"Type": "AWS::Lambda::Function",
"Properties": {
"Code": {
"S3Bucket": { "Ref": "ServerlessDeploymentBucket" },
"S3Key": "S3Key"
if (this.serverless.service.package.deploymentBucket) {
newFunction.Properties.Code.S3Bucket = this.serverless.service.package.deploymentBucket;
}
const s3Folder = this.serverless.service.package.artifactDirectoryName;
const s3FileName = artifactFilePath.split(path.sep).pop();
newFunction.Properties.Code.S3Key = `${s3Folder}/${s3FileName}`;
if (!functionObject.handler) {
const errorMessage = [
`Missing "handler" property in function ${functionName}`,
' Please make sure you point to the correct lambda handler.',
' For example: handler.hello.',
' Please check the docs for more info',
].join('');
throw new this.serverless.classes
.Error(errorMessage);
}
const Handler = functionObject.handler;
const FunctionName = functionObject.name;
const MemorySize = Number(functionObject.memorySize)
|| Number(this.serverless.service.provider.memorySize)
|| 1024;
const Timeout = Number(functionObject.timeout)
|| Number(this.serverless.service.provider.timeout)
|| 6;
const Runtime = this.serverless.service.provider.runtime
|| 'nodejs4.3';
newFunction.Properties.Handler = Handler;
newFunction.Properties.FunctionName = FunctionName;
newFunction.Properties.MemorySize = MemorySize;
newFunction.Properties.Timeout = Timeout;
newFunction.Properties.Runtime = Runtime;
if (functionObject.description) {
newFunction.Properties.Description = functionObject.description;
}
if (typeof this.serverless.service.provider.iamRoleARN === 'string') {
newFunction.Properties.Role = this.serverless.service.provider.iamRoleARN;
} else {
newFunction.Properties.Role = { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] };
}
if (!functionObject.vpc) functionObject.vpc = {};
if (!this.serverless.service.provider.vpc) this.serverless.service.provider.vpc = {};
newFunction.Properties.VpcConfig = {
SecurityGroupIds: functionObject.vpc.securityGroupIds ||
this.serverless.service.provider.vpc.securityGroupIds,
SubnetIds: functionObject.vpc.subnetIds || this.serverless.service.provider.vpc.subnetIds,
};
if (!newFunction.Properties.VpcConfig.SecurityGroupIds
|| !newFunction.Properties.VpcConfig.SubnetIds) {
delete newFunction.Properties.VpcConfig;
}
const normalizedFunctionName = functionName[0].toUpperCase() + functionName.substr(1);
const functionLogicalId = `${normalizedFunctionName}LambdaFunction`;
const newFunctionObject = {
[functionLogicalId]: newFunction,
};
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources,
newFunctionObject);
// Add function to Outputs section
const newOutput = this.cfOutputDescriptionTemplate();
newOutput.Value = { 'Fn::GetAtt': [functionLogicalId, 'Arn'] };
const newOutputObject = {
[`${functionLogicalId}Arn`]: newOutput,
};
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Outputs,
newOutputObject);
}
compileFunctions() {
this.serverless.service
.getAllFunctions()
.forEach((functionName) => this.compileFunction(functionName));
}
// Helper functions
cfLambdaFunctionTemplate() {
return {
Type: 'AWS::Lambda::Function',
Properties: {
Code: {
S3Bucket: {
Ref: 'ServerlessDeploymentBucket',
},
"FunctionName": "FunctionName",
"Handler": "Handler",
"MemorySize": "MemorySize",
"Role": "Role",
"Runtime": "Runtime",
"Timeout": "Timeout"
}
}
`;
S3Key: 'S3Key',
},
FunctionName: 'FunctionName',
Handler: 'Handler',
MemorySize: 'MemorySize',
Role: 'Role',
Runtime: 'Runtime',
Timeout: 'Timeout',
},
};
}
const outputTemplate = `
{
"Description": "Lambda function info",
"Value": "Value"
}
`;
this.serverless.service.getAllFunctions().forEach((functionName) => {
const newFunction = JSON.parse(functionTemplate);
const functionObject = this.serverless.service.getFunction(functionName);
const artifactFilePath = this.serverless.service.package.individually ?
functionObject.artifact :
this.serverless.service.package.artifact;
if (!artifactFilePath) {
throw new Error(`No artifact path is set for function: ${functionName}`);
}
const s3Folder = this.serverless.service.package.artifactDirectoryName;
const s3FileName = artifactFilePath.split(path.sep).pop();
newFunction.Properties.Code.S3Key = `${s3Folder}/${s3FileName}`;
if (!functionObject.handler) {
const errorMessage = [
`Missing "handler" property in function ${functionName}`,
' Please make sure you point to the correct lambda handler.',
' For example: handler.hello.',
' Please check the docs for more info',
].join('');
throw new this.serverless.classes
.Error(errorMessage);
}
const Handler = functionObject.handler;
const FunctionName = functionObject.name;
const MemorySize = Number(functionObject.memorySize)
|| Number(this.serverless.service.provider.memorySize)
|| 1024;
const Timeout = Number(functionObject.timeout)
|| Number(this.serverless.service.provider.timeout)
|| 6;
const Runtime = this.serverless.service.provider.runtime
|| 'nodejs4.3';
newFunction.Properties.Handler = Handler;
newFunction.Properties.FunctionName = FunctionName;
newFunction.Properties.MemorySize = MemorySize;
newFunction.Properties.Timeout = Timeout;
newFunction.Properties.Runtime = Runtime;
if (functionObject.description) {
newFunction.Properties.Description = functionObject.description;
}
if (typeof this.serverless.service.provider.iamRoleARN === 'string') {
newFunction.Properties.Role = this.serverless.service.provider.iamRoleARN;
} else {
newFunction.Properties.Role = { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] };
}
if (!functionObject.vpc) functionObject.vpc = {};
if (!this.serverless.service.provider.vpc) this.serverless.service.provider.vpc = {};
newFunction.Properties.VpcConfig = {
SecurityGroupIds: functionObject.vpc.securityGroupIds ||
this.serverless.service.provider.vpc.securityGroupIds,
SubnetIds: functionObject.vpc.subnetIds || this.serverless.service.provider.vpc.subnetIds,
};
if (!newFunction.Properties.VpcConfig.SecurityGroupIds
|| !newFunction.Properties.VpcConfig.SubnetIds) {
delete newFunction.Properties.VpcConfig;
}
const normalizedFunctionName = functionName[0].toUpperCase() + functionName.substr(1);
const functionLogicalId = `${normalizedFunctionName}LambdaFunction`;
const newFunctionObject = {
[functionLogicalId]: newFunction,
};
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources,
newFunctionObject);
// Add function to Outputs section
const newOutput = JSON.parse(outputTemplate);
newOutput.Value = { 'Fn::GetAtt': [functionLogicalId, 'Arn'] };
const newOutputObject = {
[`${functionLogicalId}Arn`]: newOutput,
};
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Outputs,
newOutputObject);
});
cfOutputDescriptionTemplate() {
return {
Description: 'Lambda function info',
Value: 'Value',
};
}
}

View File

@ -80,77 +80,6 @@ describe('AwsCompileFunctions', () => {
.to.deep.equal(`${s3Folder}/${s3FileName}`);
});
it('should merge the IamRoleLambdaExecution template into the CloudFormation template', () => {
const IamRoleLambdaExecutionTemplate = awsCompileFunctions.serverless.utils.readFileSync(
path.join(
__dirname,
'..',
'iam-role-lambda-execution-template.json'
)
);
awsCompileFunctions.compileFunctions();
expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate
.Resources.IamRoleLambdaExecution
).to.deep.equal(IamRoleLambdaExecutionTemplate.IamRoleLambdaExecution);
});
it('should merge IamPolicyLambdaExecution template into the CloudFormation template', () => {
awsCompileFunctions.compileFunctions();
// we check for the type here because a deep equality check will error out due to
// the updates which are made after the merge (they are tested in a separate test)
expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate
.Resources.IamPolicyLambdaExecution.Type
).to.deep.equal('AWS::IAM::Policy');
});
it('should update the necessary variables for the IamPolicyLambdaExecution', () => {
awsCompileFunctions.compileFunctions();
expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate
.Resources
.IamPolicyLambdaExecution
.Properties
.PolicyName
).to.equal(
`${
awsCompileFunctions.options.stage
}-${
awsCompileFunctions.serverless.service.service
}-lambda`
);
expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate
.Resources
.IamPolicyLambdaExecution
.Properties
.PolicyDocument
.Statement[0]
.Resource
).to.equal(`arn:aws:logs:${awsCompileFunctions.options.region}:*:*`);
});
it('should add custom IAM policy statements', () => {
awsCompileFunctions.serverless.service.provider.name = 'aws';
awsCompileFunctions.serverless.service.provider.iamRoleStatements = [
{
Effect: 'Allow',
Action: [
'something:SomethingElse',
],
Resource: 'some:aws:arn:xxx:*:*',
},
];
awsCompileFunctions.compileFunctions();
expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate
.Resources.IamPolicyLambdaExecution.Properties.PolicyDocument.Statement[1]
).to.deep.equal(awsCompileFunctions.serverless.service.provider.iamRoleStatements[0]);
});
it('should add iamRoleARN', () => {
awsCompileFunctions.serverless.service.provider.name = 'aws';
awsCompileFunctions.serverless.service.provider.iamRoleARN = 'some:aws:arn:xxx:*:*';
@ -185,7 +114,7 @@ describe('AwsCompileFunctions', () => {
name: 'new-service-dev-func',
},
};
const compliedFunction = {
const compiledFunction = {
Type: 'AWS::Lambda::Function',
Properties: {
Code: {
@ -207,7 +136,7 @@ describe('AwsCompileFunctions', () => {
expect(
awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate
.Resources.FuncLambdaFunction
).to.deep.equal(compliedFunction);
).to.deep.equal(compiledFunction);
});
it('should create a function resource with VPC config', () => {
@ -358,6 +287,55 @@ describe('AwsCompileFunctions', () => {
).to.deep.equal(compiledFunction);
});
it('should use a custom bucket if specified', () => {
const bucketName = 'com.serverless.deploys';
awsCompileFunctions.serverless.service.package.deploymentBucket = bucketName;
awsCompileFunctions.serverless.service.provider.runtime = 'python2.7';
awsCompileFunctions.serverless.service.provider.memorySize = 128;
awsCompileFunctions.serverless.service.functions = {
func: {
handler: 'func.function.handler',
name: 'new-service-dev-func',
},
};
const compiledFunction = {
Type: 'AWS::Lambda::Function',
Properties: {
Code: {
S3Key: `${awsCompileFunctions.serverless.service.package.artifactDirectoryName}/${
awsCompileFunctions.serverless.service.package.artifact}`,
S3Bucket: bucketName,
},
FunctionName: 'new-service-dev-func',
Handler: 'func.function.handler',
MemorySize: 128,
Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] },
Runtime: 'python2.7',
Timeout: 6,
},
};
const coreCloudFormationTemplate = awsCompileFunctions.serverless.utils.readFileSync(
path.join(
__dirname,
'..',
'..',
'..',
'lib',
'core-cloudformation-template.json'
)
);
awsCompileFunctions.serverless.service.provider
.compiledCloudFormationTemplate = coreCloudFormationTemplate;
awsCompileFunctions.compileFunctions();
expect(
awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate
.Resources.FuncLambdaFunction
).to.deep.equal(compiledFunction);
});
it('should include description if specified', () => {
awsCompileFunctions.serverless.service.functions = {
func: {

View File

@ -10,6 +10,7 @@ const setBucketName = require('./lib/setBucketName');
const cleanupS3Bucket = require('./lib/cleanupS3Bucket');
const uploadArtifacts = require('./lib/uploadArtifacts');
const updateStack = require('./lib/updateStack');
const configureStack = require('./lib/configureStack');
const SDK = require('../');
@ -30,12 +31,17 @@ class AwsDeploy {
cleanupS3Bucket,
uploadArtifacts,
updateStack,
monitorStack
monitorStack,
configureStack
);
this.hooks = {
'before:deploy:initialize': () => BbPromise.bind(this)
.then(this.validate),
.then(this.validate),
'deploy:initialize': () => BbPromise.bind(this)
.then(this.configureStack)
.then(this.mergeCustomProviderResources),
'deploy:setupProviderConfiguration': () => BbPromise.bind(this)
.then(this.createStack)
@ -44,8 +50,6 @@ class AwsDeploy {
'before:deploy:compileFunctions': () => BbPromise.bind(this)
.then(this.generateArtifactDirectoryName),
'before:deploy:deploy': () => BbPromise.bind(this).then(this.mergeCustomProviderResources),
'deploy:deploy': () => BbPromise.bind(this)
.then(this.setBucketName)
.then(this.cleanupS3Bucket)

View File

@ -7,19 +7,26 @@ module.exports = {
getObjectsToRemove() {
// 4 old ones + the one which will be uploaded after the cleanup = 5
const directoriesToKeepCount = 4;
const serviceStage = `${this.serverless.service.service}/${this.options.stage}`;
return this.sdk.request('S3',
'listObjectsV2',
{ Bucket: this.bucketName },
{
Bucket: this.bucketName,
Prefix: `serverless/${serviceStage}`,
},
this.options.stage,
this.options.region)
.then((result) => {
if (result.Contents.length) {
let directories = [];
const regex = new RegExp(
`serverless/${serviceStage}/(.+-.+-.+-.+)`
);
// get the unique directory names
result.Contents.forEach((obj) => {
const match = obj.Key.match(/(.+\-.+\-.+\-.+)\//);
const match = obj.Key.match(regex);
if (match) {
const directoryName = match[1];

View File

@ -0,0 +1,109 @@
'use strict';
const _ = require('lodash');
const BbPromise = require('bluebird');
const path = require('path');
module.exports = {
configureStack() {
this.serverless.service.provider
.compiledCloudFormationTemplate = this.serverless.utils.readFileSync(
path.join(this.serverless.config.serverlessPath,
'plugins',
'aws',
'deploy',
'lib',
'core-cloudformation-template.json')
);
if (typeof this.serverless.service.provider.iamRoleARN !== 'string') {
// merge in the iamRoleLambdaTemplate
const iamRoleLambdaExecutionTemplate = this.serverless.utils.readFileSync(
path.join(this.serverless.config.serverlessPath,
'plugins',
'aws',
'deploy',
'lib',
'iam-role-lambda-execution-template.json')
);
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources,
iamRoleLambdaExecutionTemplate);
// merge in the iamPolicyLambdaTemplate
const iamPolicyLambdaExecutionTemplate = this.serverless.utils.readFileSync(
path.join(this.serverless.config.serverlessPath,
'plugins',
'aws',
'deploy',
'lib',
'iam-policy-lambda-execution-template.json')
);
// set the necessary variables for the IamPolicyLambda
iamPolicyLambdaExecutionTemplate
.IamPolicyLambdaExecution
.Properties
.PolicyName = `${this.options.stage}-${this.serverless.service.service}-lambda`;
iamPolicyLambdaExecutionTemplate
.IamPolicyLambdaExecution
.Properties
.PolicyDocument
.Statement[0]
.Resource = `arn:aws:logs:${this.options.region}:*:*`;
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources,
iamPolicyLambdaExecutionTemplate);
// add custom iam role statements
if (this.serverless.service.provider.iamRoleStatements &&
this.serverless.service.provider.iamRoleStatements instanceof Array) {
this.serverless.service.provider.compiledCloudFormationTemplate
.Resources
.IamPolicyLambdaExecution
.Properties
.PolicyDocument
.Statement = this.serverless.service.provider.compiledCloudFormationTemplate
.Resources
.IamPolicyLambdaExecution
.Properties
.PolicyDocument
.Statement.concat(this.serverless.service.provider.iamRoleStatements);
}
}
const bucketName = this.serverless.service.provider.deploymentBucket;
if (bucketName) {
return BbPromise.bind(this)
.then(() => this.validateS3BucketName(bucketName))
.then(() => this.sdk.request('S3',
'getBucketLocation',
{
Bucket: bucketName,
},
this.options.stage,
this.options.region
))
.then(result => {
if (result.LocationConstraint !== this.options.region) {
throw new this.serverless.classes.Error(
'Deployment bucket is not in the same region as the lambda function'
);
}
this.bucketName = bucketName;
this.serverless.service.package.deploymentBucket = bucketName;
this.serverless.service.provider.compiledCloudFormationTemplate
.Outputs.ServerlessDeploymentBucketName.Value = bucketName;
delete this.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ServerlessDeploymentBucket;
});
}
return BbPromise.resolve();
},
};

View File

@ -7,7 +7,7 @@ module.exports = {
create() {
this.serverless.cli.log('Creating Stack...');
const stackName = `${this.serverless.service.service}-${this.options.stage}`;
const coreCloudFormationTemplate = this.loadCoreCloudFormationTemplate();
const params = {
StackName: stackName,
OnFailure: 'ROLLBACK',
@ -15,7 +15,8 @@ module.exports = {
'CAPABILITY_IAM',
],
Parameters: [],
TemplateBody: JSON.stringify(coreCloudFormationTemplate),
TemplateBody: JSON.stringify(this.serverless.service.provider
.compiledCloudFormationTemplate),
Tags: [{
Key: 'STAGE',
Value: this.options.stage,
@ -41,8 +42,6 @@ module.exports = {
].join('');
throw new this.serverless.classes.Error(errorMessage);
}
this.serverless.service.provider
.compiledCloudFormationTemplate = this.loadCoreCloudFormationTemplate();
return BbPromise.bind(this)
// always write the template to disk, whether we are deploying or not
@ -70,17 +69,6 @@ module.exports = {
},
// helper methods
loadCoreCloudFormationTemplate() {
return this.serverless.utils.readFileSync(
path.join(this.serverless.config.serverlessPath,
'plugins',
'aws',
'deploy',
'lib',
'core-cloudformation-template.json')
);
},
writeCreateTemplateToDisk() {
const cfTemplateFilePath = path.join(this.serverless.config.servicePath,
'.serverless', 'cloudformation-template-create-stack.json');

View File

@ -5,8 +5,10 @@ const BbPromise = require('bluebird');
module.exports = {
generateArtifactDirectoryName() {
const date = new Date();
const serviceStage = `${this.serverless.service.service}/${this.options.stage}`;
const dateString = `${date.getTime().toString()}-${date.toISOString()}`;
this.serverless.service.package
.artifactDirectoryName = `${date.getTime().toString()}-${date.toISOString()}`;
.artifactDirectoryName = `serverless/${serviceStage}/${dateString}`;
return BbPromise.resolve();
},

View File

@ -4,6 +4,10 @@ const BbPromise = require('bluebird');
module.exports = {
setBucketName() {
if (this.bucketName) {
return BbPromise.resolve(this.bucketName);
}
if (this.options.noDeploy) {
return BbPromise.resolve();
}

View File

@ -16,6 +16,7 @@ module.exports = {
Bucket: this.bucketName,
Key: `${this.serverless.service.package.artifactDirectoryName}/${fileName}`,
Body: body,
ContentType: 'application/json',
};
return this.sdk.request('S3',
@ -38,6 +39,7 @@ module.exports = {
Bucket: this.bucketName,
Key: `${this.serverless.service.package.artifactDirectoryName}/${fileName}`,
Body: body,
ContentType: 'application/zip',
};
return this.sdk.request('S3',

View File

@ -8,3 +8,4 @@ require('./cleanupS3Bucket');
require('./uploadArtifacts');
require('./updateStack');
require('./index');
require('./configureStack');

View File

@ -9,13 +9,16 @@ const Serverless = require('../../../../Serverless');
describe('cleanupS3Bucket', () => {
let serverless;
let awsDeploy;
let s3Key;
beforeEach(() => {
serverless = new Serverless();
serverless.service.service = 'cleanupS3Bucket';
const options = {
stage: 'dev',
region: 'us-east-1',
};
s3Key = `serverless/${serverless.service.service}/${options.stage}`;
awsDeploy = new AwsDeploy(serverless, options);
awsDeploy.bucketName = 'deployment-bucket';
awsDeploy.serverless.cli = new serverless.classes.CLI();
@ -35,6 +38,7 @@ describe('cleanupS3Bucket', () => {
expect(listObjectsStub.args[0][0]).to.be.equal('S3');
expect(listObjectsStub.args[0][1]).to.be.equal('listObjectsV2');
expect(listObjectsStub.args[0][2].Bucket).to.be.equal(awsDeploy.bucketName);
expect(listObjectsStub.args[0][2].Prefix).to.be.equal(`${s3Key}`);
expect(listObjectsStub.calledWith(awsDeploy.options.stage, awsDeploy.options.region));
awsDeploy.sdk.request.restore();
});
@ -43,18 +47,18 @@ describe('cleanupS3Bucket', () => {
it('should return all to be removed service objects (except the last 4)', () => {
const serviceObjects = {
Contents: [
{ Key: '151224711231-2016-08-18T15:42:00/artifact.zip' },
{ Key: '151224711231-2016-08-18T15:42:00/cloudformation.json' },
{ Key: '141264711231-2016-08-18T15:42:00/artifact.zip' },
{ Key: '141264711231-2016-08-18T15:42:00/cloudformation.json' },
{ Key: '141321321541-2016-08-18T11:23:02/artifact.zip' },
{ Key: '141321321541-2016-08-18T11:23:02/cloudformation.json' },
{ Key: '142003031341-2016-08-18T12:46:04/artifact.zip' },
{ Key: '142003031341-2016-08-18T12:46:04/cloudformation.json' },
{ Key: '113304333331-2016-08-18T13:40:06/artifact.zip' },
{ Key: '113304333331-2016-08-18T13:40:06/cloudformation.json' },
{ Key: '903940390431-2016-08-18T23:42:08/artifact.zip' },
{ Key: '903940390431-2016-08-18T23:42:08/cloudformation.json' },
{ Key: `${s3Key}/151224711231-2016-08-18T15:42:00/artifact.zip` },
{ Key: `${s3Key}/151224711231-2016-08-18T15:42:00/cloudformation.json` },
{ Key: `${s3Key}/141264711231-2016-08-18T15:42:00/artifact.zip` },
{ Key: `${s3Key}/141264711231-2016-08-18T15:42:00/cloudformation.json` },
{ Key: `${s3Key}/141321321541-2016-08-18T11:23:02/artifact.zip` },
{ Key: `${s3Key}/141321321541-2016-08-18T11:23:02/cloudformation.json` },
{ Key: `${s3Key}/142003031341-2016-08-18T12:46:04/artifact.zip` },
{ Key: `${s3Key}/142003031341-2016-08-18T12:46:04/cloudformation.json` },
{ Key: `${s3Key}/113304333331-2016-08-18T13:40:06/artifact.zip` },
{ Key: `${s3Key}/113304333331-2016-08-18T13:40:06/cloudformation.json` },
{ Key: `${s3Key}/903940390431-2016-08-18T23:42:08/artifact.zip` },
{ Key: `${s3Key}/903940390431-2016-08-18T23:42:08/cloudformation.json` },
],
};
@ -63,25 +67,42 @@ describe('cleanupS3Bucket', () => {
return awsDeploy.getObjectsToRemove().then((objectsToRemove) => {
expect(objectsToRemove).to.not
.include({ Key: '141321321541-2016-08-18T11:23:02/artifact.zip' });
.include(
{ Key: `${s3Key}${s3Key}/141321321541-2016-08-18T11:23:02/artifact.zip` });
expect(objectsToRemove).to.not
.include({ Key: '141321321541-2016-08-18T11:23:02/cloudformation.json' });
.include(
{ Key: `${s3Key}${s3Key}/141321321541-2016-08-18T11:23:02/cloudformation.json` });
expect(objectsToRemove).to.not
.include({ Key: '142003031341-2016-08-18T12:46:04/artifact.zip' });
.include(
{ Key: `${s3Key}${s3Key}/142003031341-2016-08-18T12:46:04/artifact.zip` });
expect(objectsToRemove).to.not
.include({ Key: '142003031341-2016-08-18T12:46:04/cloudformation.json' });
.include(
{ Key: `${s3Key}${s3Key}/142003031341-2016-08-18T12:46:04/cloudformation.json` });
expect(objectsToRemove).to.not
.include({ Key: '151224711231-2016-08-18T15:42:00/artifact.zip' });
.include(
{ Key: `${s3Key}${s3Key}/151224711231-2016-08-18T15:42:00/artifact.zip` });
expect(objectsToRemove).to.not
.include({ Key: '151224711231-2016-08-18T15:42:00/cloudformation.json' });
.include(
{ Key: `${s3Key}${s3Key}/151224711231-2016-08-18T15:42:00/cloudformation.json` });
expect(objectsToRemove).to.not
.include({ Key: '903940390431-2016-08-18T23:42:08/artifact.zip' });
.include(
{ Key: `${s3Key}${s3Key}/903940390431-2016-08-18T23:42:08/artifact.zip` });
expect(objectsToRemove).to.not
.include({ Key: '903940390431-2016-08-18T23:42:08/cloudformation.json' });
.include(
{ Key: `${s3Key}${s3Key}/903940390431-2016-08-18T23:42:08/cloudformation.json` });
expect(listObjectsStub.calledOnce).to.be.equal(true);
expect(listObjectsStub.args[0][0]).to.be.equal('S3');
expect(listObjectsStub.args[0][1]).to.be.equal('listObjectsV2');
expect(listObjectsStub.args[0][2].Bucket).to.be.equal(awsDeploy.bucketName);
expect(listObjectsStub.args[0][2].Prefix).to.be.equal(`${s3Key}`);
expect(listObjectsStub.calledWith(awsDeploy.options.stage, awsDeploy.options.region));
awsDeploy.sdk.request.restore();
});
@ -90,12 +111,12 @@ describe('cleanupS3Bucket', () => {
it('should return an empty array if there are less than 4 directories available', () => {
const serviceObjects = {
Contents: [
{ Key: '151224711231-2016-08-18T15:42:00/artifact.zip' },
{ Key: '151224711231-2016-08-18T15:42:00/cloudformation.json' },
{ Key: '141264711231-2016-08-18T15:42:00/artifact.zip' },
{ Key: '141264711231-2016-08-18T15:42:00/cloudformation.json' },
{ Key: '141321321541-2016-08-18T11:23:02/artifact.zip' },
{ Key: '141321321541-2016-08-18T11:23:02/cloudformation.json' },
{ Key: `${s3Key}151224711231-2016-08-18T15:42:00/artifact.zip` },
{ Key: `${s3Key}151224711231-2016-08-18T15:42:00/cloudformation.json` },
{ Key: `${s3Key}141264711231-2016-08-18T15:42:00/artifact.zip` },
{ Key: `${s3Key}141264711231-2016-08-18T15:42:00/cloudformation.json` },
{ Key: `${s3Key}141321321541-2016-08-18T11:23:02/artifact.zip` },
{ Key: `${s3Key}141321321541-2016-08-18T11:23:02/cloudformation.json` },
],
};
@ -108,6 +129,7 @@ describe('cleanupS3Bucket', () => {
expect(listObjectsStub.args[0][0]).to.be.equal('S3');
expect(listObjectsStub.args[0][1]).to.be.equal('listObjectsV2');
expect(listObjectsStub.args[0][2].Bucket).to.be.equal(awsDeploy.bucketName);
expect(listObjectsStub.args[0][2].Prefix).to.be.equal(`${s3Key}`);
expect(listObjectsStub.calledWith(awsDeploy.options.stage, awsDeploy.options.region));
awsDeploy.sdk.request.restore();
});
@ -116,14 +138,14 @@ describe('cleanupS3Bucket', () => {
it('should resolve if there are exactly 4 directories available', () => {
const serviceObjects = {
Contents: [
{ Key: '151224711231-2016-08-18T15:42:00/artifact.zip' },
{ Key: '151224711231-2016-08-18T15:42:00/cloudformation.json' },
{ Key: '141264711231-2016-08-18T15:42:00/artifact.zip' },
{ Key: '141264711231-2016-08-18T15:42:00/cloudformation.json' },
{ Key: '141321321541-2016-08-18T11:23:02/artifact.zip' },
{ Key: '141321321541-2016-08-18T11:23:02/cloudformation.json' },
{ Key: '142003031341-2016-08-18T12:46:04/artifact.zip' },
{ Key: '142003031341-2016-08-18T12:46:04/cloudformation.json' },
{ Key: `${s3Key}151224711231-2016-08-18T15:42:00/artifact.zip` },
{ Key: `${s3Key}151224711231-2016-08-18T15:42:00/cloudformation.json` },
{ Key: `${s3Key}141264711231-2016-08-18T15:42:00/artifact.zip` },
{ Key: `${s3Key}141264711231-2016-08-18T15:42:00/cloudformation.json` },
{ Key: `${s3Key}141321321541-2016-08-18T11:23:02/artifact.zip` },
{ Key: `${s3Key}141321321541-2016-08-18T11:23:02/cloudformation.json` },
{ Key: `${s3Key}142003031341-2016-08-18T12:46:04/artifact.zip` },
{ Key: `${s3Key}142003031341-2016-08-18T12:46:04/cloudformation.json` },
],
};
@ -136,6 +158,7 @@ describe('cleanupS3Bucket', () => {
expect(listObjectsStub.args[0][0]).to.be.equal('S3');
expect(listObjectsStub.args[0][1]).to.be.equal('listObjectsV2');
expect(listObjectsStub.args[0][2].Bucket).to.be.equal(awsDeploy.bucketName);
expect(listObjectsStub.args[0][2].Prefix).to.be.equal(`${s3Key}`);
expect(listObjectsStub.calledWith(awsDeploy.options.stage, awsDeploy.options.region));
awsDeploy.sdk.request.restore();
});
@ -159,10 +182,10 @@ describe('cleanupS3Bucket', () => {
it('should remove all old service files from the S3 bucket if available', () => {
const objectsToRemove = [
{ Key: '113304333331-2016-08-18T13:40:06/artifact.zip' },
{ Key: '113304333331-2016-08-18T13:40:06/cloudformation.json' },
{ Key: '141264711231-2016-08-18T15:42:00/artifact.zip' },
{ Key: '141264711231-2016-08-18T15:42:00/cloudformation.json' },
{ Key: `${s3Key}113304333331-2016-08-18T13:40:06/artifact.zip` },
{ Key: `${s3Key}113304333331-2016-08-18T13:40:06/cloudformation.json` },
{ Key: `${s3Key}141264711231-2016-08-18T15:42:00/artifact.zip` },
{ Key: `${s3Key}141264711231-2016-08-18T15:42:00/cloudformation.json` },
];
return awsDeploy.removeObjects(objectsToRemove).then(() => {

View File

@ -0,0 +1,193 @@
'use strict';
const sinon = require('sinon');
const BbPromise = require('bluebird');
const path = require('path');
const expect = require('chai').expect;
const Serverless = require('../../../../Serverless');
const AwsSdk = require('../');
describe('#configureStack', () => {
let awsSdk;
let serverless;
beforeEach(() => {
serverless = new Serverless();
const options = {
stage: 'dev',
region: 'us-east-1',
};
awsSdk = new AwsSdk(serverless, options);
awsSdk.serverless.cli = new serverless.classes.CLI();
});
it('should validate the region for the given S3 bucket', () => {
const bucketName = 'com.serverless.deploys';
const getBucketLocationStub = sinon
.stub(awsSdk.sdk, 'request').returns(
BbPromise.resolve({ LocationConstraint: awsSdk.options.region })
);
awsSdk.serverless.service.provider.deploymentBucket = bucketName;
return awsSdk.configureStack()
.then(() => {
expect(getBucketLocationStub.args[0][0]).to.equal('S3');
expect(getBucketLocationStub.args[0][1]).to.equal('getBucketLocation');
expect(getBucketLocationStub.args[0][2].Bucket).to.equal(bucketName);
});
});
it('should reject an S3 bucket in the wrong region', () => {
const bucketName = 'com.serverless.deploys';
const createStackStub = sinon
.stub(awsSdk.sdk, 'request').returns(
BbPromise.resolve({ LocationConstraint: 'us-west-1' })
);
awsSdk.serverless.service.provider.deploymentBucket = 'com.serverless.deploys';
return awsSdk.configureStack()
.catch((err) => {
expect(createStackStub.args[0][0]).to.equal('S3');
expect(createStackStub.args[0][1]).to.equal('getBucketLocation');
expect(createStackStub.args[0][2].Bucket).to.equal(bucketName);
expect(err.message).to.contain('not in the same region');
})
.then(() => {});
});
it('should merge the IamRoleLambdaExecution template into the CloudFormation template', () => {
const IamRoleLambdaExecutionTemplate = awsSdk.serverless.utils.readFileSync(
path.join(
__dirname,
'..',
'lib',
'iam-role-lambda-execution-template.json'
)
);
return awsSdk.configureStack()
.then(() => {
expect(awsSdk.serverless.service.provider.compiledCloudFormationTemplate
.Resources.IamRoleLambdaExecution
).to.deep.equal(IamRoleLambdaExecutionTemplate.IamRoleLambdaExecution);
});
});
it('should merge IamPolicyLambdaExecution template into the CloudFormation template', () =>
awsSdk.configureStack()
.then(() => {
// we check for the type here because a deep equality check will error out due to
// the updates which are made after the merge (they are tested in a separate test)
expect(awsSdk.serverless.service.provider.compiledCloudFormationTemplate
.Resources.IamPolicyLambdaExecution.Type
).to.deep.equal('AWS::IAM::Policy');
})
);
it('should update the necessary variables for the IamPolicyLambdaExecution', () =>
awsSdk.configureStack()
.then(() => {
expect(awsSdk.serverless.service.provider.compiledCloudFormationTemplate
.Resources
.IamPolicyLambdaExecution
.Properties
.PolicyName
).to.equal(
`${
awsSdk.options.stage
}-${
awsSdk.serverless.service.service
}-lambda`
);
expect(awsSdk.serverless.service.provider.compiledCloudFormationTemplate
.Resources
.IamPolicyLambdaExecution
.Properties
.PolicyDocument
.Statement[0]
.Resource
).to.equal(`arn:aws:logs:${awsSdk.options.region}:*:*`);
})
);
it('should add custom IAM policy statements', () => {
awsSdk.serverless.service.provider.name = 'aws';
awsSdk.serverless.service.provider.iamRoleStatements = [
{
Effect: 'Allow',
Action: [
'something:SomethingElse',
],
Resource: 'some:aws:arn:xxx:*:*',
},
];
return awsSdk.configureStack()
.then(() => {
expect(awsSdk.serverless.service.provider.compiledCloudFormationTemplate
.Resources.IamPolicyLambdaExecution.Properties.PolicyDocument.Statement[1]
).to.deep.equal(awsSdk.serverless.service.provider.iamRoleStatements[0]);
});
});
it('should use a custom bucket if specified', () => {
const bucketName = 'com.serverless.deploys';
awsSdk.serverless.service.provider.deploymentBucket = bucketName;
const coreCloudFormationTemplate = awsSdk.serverless.utils.readFileSync(
path.join(
__dirname,
'..',
'lib',
'core-cloudformation-template.json'
)
);
awsSdk.serverless.service.provider
.compiledCloudFormationTemplate = coreCloudFormationTemplate;
sinon
.stub(awsSdk.sdk, 'request')
.returns(BbPromise.resolve({ LocationConstraint: awsSdk.options.region }));
return awsSdk.configureStack()
.then(() => {
expect(
awsSdk.serverless.service.provider.compiledCloudFormationTemplate
.Outputs.ServerlessDeploymentBucketName.Value
).to.equal(bucketName);
// eslint-disable-next-line no-unused-expressions
expect(
awsSdk.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ServerlessDeploymentBucket
).to.not.exist;
});
});
it('should not add IamPolicyLambdaExecution', () => {
awsSdk.serverless.service.provider.iamRoleARN = 'some:aws:arn:xxx:*:*';
return awsSdk.configureStack()
.then(() => expect(
awsSdk.serverless.service.provider.compiledCloudFormationTemplate
.Resources.IamPolicyLambdaExecution
).to.not.exist);
});
it('should not add IamRole', () => {
awsSdk.serverless.service.provider.iamRoleARN = 'some:aws:arn:xxx:*:*';
return awsSdk.configureStack()
.then(() => expect(
awsSdk.serverless.service.provider.compiledCloudFormationTemplate
.Resources.IamRoleLambdaExecution
).to.not.exist);
});
});

View File

@ -9,7 +9,6 @@ const Serverless = require('../../../../Serverless');
const testUtils = require('../../../../../tests/utils');
describe('createStack', () => {
let serverless;
let awsDeploy;
const tmpDirPath = testUtils.getTmpDirPath();
@ -25,7 +24,7 @@ describe('createStack', () => {
};
beforeEach(() => {
serverless = new Serverless();
const serverless = new Serverless();
serverless.utils.writeFileSync(serverlessYmlPath, serverlessYml);
serverless.config.servicePath = tmpDirPath;
const options = {
@ -46,6 +45,9 @@ describe('createStack', () => {
'core-cloudformation-template.json')
);
awsDeploy.serverless.service.provider
.compiledCloudFormationTemplate = coreCloudFormationTemplate;
const createStackStub = sinon
.stub(awsDeploy.sdk, 'request').returns(BbPromise.resolve());
@ -60,7 +62,6 @@ describe('createStack', () => {
.to.deep.equal([{ Key: 'STAGE', Value: awsDeploy.options.stage }]);
expect(createStackStub.calledOnce).to.be.equal(true);
expect(createStackStub.calledWith(awsDeploy.options.stage, awsDeploy.options.region));
awsDeploy.sdk.request.restore();
});
});
});
@ -69,7 +70,16 @@ describe('createStack', () => {
it('should store the core CloudFormation template in the provider object', () => {
sinon.stub(awsDeploy.sdk, 'request').returns(BbPromise.resolve());
const coreCloudFormationTemplate = awsDeploy.loadCoreCloudFormationTemplate();
const coreCloudFormationTemplate = awsDeploy.serverless.utils.readFileSync(
path.join(__dirname,
'..',
'lib',
'core-cloudformation-template.json')
);
awsDeploy.serverless.service.provider
.compiledCloudFormationTemplate = coreCloudFormationTemplate;
const writeCreateTemplateToDiskStub = sinon
.stub(awsDeploy, 'writeCreateTemplateToDisk').returns(BbPromise.resolve());
@ -77,8 +87,6 @@ describe('createStack', () => {
expect(writeCreateTemplateToDiskStub.calledOnce).to.be.equal(true);
expect(awsDeploy.serverless.service.provider.compiledCloudFormationTemplate)
.to.deep.equal(coreCloudFormationTemplate);
awsDeploy.sdk.request.restore();
});
});
@ -90,8 +98,6 @@ describe('createStack', () => {
return awsDeploy.createStack().then(() => {
expect(createStub.called).to.be.equal(false);
awsDeploy.create.restore();
awsDeploy.sdk.request.restore();
});
});
@ -106,9 +112,6 @@ describe('createStack', () => {
return awsDeploy.createStack().then(() => {
expect(writeCreateTemplateToDiskStub.calledOnce).to.be.equal(true);
expect(createStub.called).to.be.equal(false);
awsDeploy.writeCreateTemplateToDisk.restore();
awsDeploy.create.restore();
});
});
@ -119,12 +122,10 @@ describe('createStack', () => {
.stub(awsDeploy, 'writeCreateTemplateToDisk').returns(BbPromise.resolve());
sinon.stub(awsDeploy.sdk, 'request').returns(BbPromise.resolve());
return awsDeploy.createStack().then(() => {
return awsDeploy.createStack().then((res) => {
expect(writeCreateTemplateToDiskStub.calledOnce).to.be.equal(true);
expect(awsDeploy.sdk.request.called).to.be.equal(true);
awsDeploy.writeCreateTemplateToDisk.restore();
awsDeploy.sdk.request.restore();
expect(res).to.equal('alreadyCreated');
});
});
@ -142,9 +143,6 @@ describe('createStack', () => {
expect(createStub.called).to.be.equal(false);
expect(e.name).to.be.equal('ServerlessError');
expect(e.message).to.be.equal(errorMock);
awsDeploy.create.restore();
awsDeploy.sdk.request.restore();
});
});
@ -160,22 +158,10 @@ describe('createStack', () => {
return awsDeploy.createStack().then(() => {
expect(createStub.calledOnce).to.be.equal(true);
awsDeploy.create.restore();
awsDeploy.sdk.request.restore();
});
});
});
describe('#loadCoreCloudFormationTemplate', () => {
it('should load the core CloudFormation template', () => {
const template = awsDeploy.loadCoreCloudFormationTemplate();
expect(template.Resources.ServerlessDeploymentBucket.Type)
.to.equal('AWS::S3::Bucket');
});
});
describe('#writeCreateTemplateToDisk', () => {
it('should write the compiled CloudFormation template into the .serverless directory', () => {
awsDeploy.serverless.service.provider.compiledCloudFormationTemplate = { key: 'value' };
@ -185,8 +171,10 @@ describe('createStack', () => {
'cloudformation-template-create-stack.json');
return awsDeploy.writeCreateTemplateToDisk().then(() => {
expect(serverless.utils.fileExistsSync(templatePath)).to.equal(true);
expect(serverless.utils.readFileSync(templatePath)).to.deep.equal({ key: 'value' });
expect(awsDeploy.serverless.utils.fileExistsSync(templatePath)).to.equal(true);
expect(awsDeploy.serverless.utils.readFileSync(templatePath)).to.deep.equal(
{ key: 'value' }
);
});
});
});

View File

@ -7,27 +7,32 @@ const BbPromise = require('bluebird');
const sinon = require('sinon');
describe('AwsDeploy', () => {
const serverless = new Serverless();
const options = {
stage: 'dev',
region: 'us-east-1',
};
const awsDeploy = new AwsDeploy(serverless, options);
awsDeploy.serverless.cli = new serverless.classes.CLI();
let awsDeploy;
beforeEach(() => {
const serverless = new Serverless();
const options = {
stage: 'dev',
region: 'us-east-1',
};
awsDeploy = new AwsDeploy(serverless, options);
awsDeploy.serverless.cli = new serverless.classes.CLI();
});
describe('#constructor()', () => {
it('should have hooks', () => expect(awsDeploy.hooks).to.be.not.empty);
it('should set the provider variable to "aws"', () => expect(awsDeploy.provider)
.to.equal('aws'));
});
describe('hooks', () => {
it('should run "before:deploy:initialize" hook promise chain in order', () => {
const validateStub = sinon
.stub(awsDeploy, 'validate').returns(BbPromise.resolve());
return awsDeploy.hooks['before:deploy:initialize']().then(() => {
expect(validateStub.calledOnce).to.be.equal(true);
awsDeploy.validate.restore();
});
});
@ -40,8 +45,6 @@ describe('AwsDeploy', () => {
return awsDeploy.hooks['deploy:setupProviderConfiguration']().then(() => {
expect(createStackStub.calledOnce).to.be.equal(true);
expect(monitorStackStub.calledOnce).to.be.equal(true);
awsDeploy.createStack.restore();
awsDeploy.monitorStack.restore();
});
});
@ -51,17 +54,19 @@ describe('AwsDeploy', () => {
return awsDeploy.hooks['before:deploy:compileFunctions']().then(() => {
expect(generateArtifactDirectoryNameStub.calledOnce).to.be.equal(true);
awsDeploy.generateArtifactDirectoryName.restore();
});
});
it('should run "before:deploy:deploy" promise chain in order', () => {
it('should run "deploy:initialize" promise chain in order', () => {
const configureStackStub = sinon
.stub(awsDeploy, 'configureStack').returns(BbPromise.resolve());
const mergeCustomProviderResourcesStub = sinon
.stub(awsDeploy, 'mergeCustomProviderResources').returns(BbPromise.resolve());
return awsDeploy.hooks['before:deploy:deploy']().then(() => {
return awsDeploy.hooks['deploy:initialize']().then(() => {
expect(configureStackStub.calledOnce).to.be.equal(true);
expect(mergeCustomProviderResourcesStub.calledOnce).to.be.equal(true);
awsDeploy.mergeCustomProviderResources.restore();
});
});
@ -87,12 +92,20 @@ describe('AwsDeploy', () => {
.to.be.equal(true);
expect(monitorStackStub.calledAfter(updateStackStub))
.to.be.equal(true);
});
});
it('should notify about noDeploy', () => {
sinon.stub(awsDeploy, 'setBucketName').returns(BbPromise.resolve());
sinon.stub(awsDeploy, 'cleanupS3Bucket').returns(BbPromise.resolve());
sinon.stub(awsDeploy, 'uploadArtifacts').returns(BbPromise.resolve());
sinon.stub(awsDeploy, 'updateStack').returns(BbPromise.resolve());
sinon.stub(awsDeploy, 'monitorStack').returns(BbPromise.resolve());
sinon.stub(awsDeploy.serverless.cli, 'log').returns();
awsDeploy.options.noDeploy = true;
return awsDeploy.hooks['deploy:deploy']().then(() => {
awsDeploy.setBucketName.restore();
awsDeploy.cleanupS3Bucket.restore();
awsDeploy.uploadArtifacts.restore();
awsDeploy.updateStack.restore();
awsDeploy.monitorStack.restore();
});
});
});

View File

@ -42,4 +42,12 @@ describe('#setBucketName()', () => {
awsDeploy.sdk.getServerlessDeploymentBucketName.restore();
});
});
it('should resolve if the bucketName is already set', () => {
const bucketName = 'someBucket';
awsDeploy.bucketName = bucketName;
return awsDeploy.setBucketName()
.then(() => expect(getServerlessDeploymentBucketNameStub.calledOnce).to.be.false)
.then(() => expect(awsDeploy.bucketName).to.equal(bucketName));
});
});

View File

@ -69,7 +69,7 @@ class SDK {
].join('');
err.message = errorMessage;
}
reject(new this.serverless.classes.Error(err.message));
reject(new this.serverless.classes.Error(err.message, err.statusCode));
} else {
resolve(data);
}
@ -99,9 +99,7 @@ class SDK {
},
stage,
region
).then((result) =>
result.StackResourceDetail.PhysicalResourceId
);
).then((result) => result.StackResourceDetail.PhysicalResourceId);
}
getStackName(stage) {

View File

@ -18,4 +18,45 @@ module.exports = {
return BbPromise.resolve();
},
/**
* Retrieved 9/27/2016 from http://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html
* Bucket names must be at least 3 and no more than 63 characters long.
* Bucket names must be a series of one or more labels.
* Adjacent labels are separated by a single period (.).
* Bucket names can contain lowercase letters, numbers, and hyphens.
* Each label must start and end with a lowercase letter or a number.
* Bucket names must not be formatted as an IP address (e.g., 192.168.5.4).
* @param bucketName
*/
validateS3BucketName(bucketName) {
return BbPromise.resolve()
.then(() => {
let error;
if (!bucketName) {
error = 'Bucket name cannot be undefined or empty';
} else if (bucketName.length < 3) {
error = `Bucket name is shorter than 3 characters. ${bucketName}`;
} else if (bucketName.length > 63) {
error = `Bucket name is longer than 63 characters. ${bucketName}`;
} else if (/^[^a-z0-9]/.test(bucketName)) {
error = `Bucket name must start with a letter or number. ${bucketName}`;
} else if (/[^a-z0-9]$/.test(bucketName)) {
error = `Bucket name must end with a letter or number. ${bucketName}`;
} else if (/[A-Z]/.test(bucketName)) {
error = `Bucket name cannot contain uppercase letters. ${bucketName}`;
} else if (!/^[a-z0-9][a-z.0-9-]+[a-z0-9]$/.test(bucketName)) {
error = `Bucket name contains invalid characters, [a-z.0-9-] ${bucketName}`;
} else if (/\.{2,}/.test(bucketName)) {
error = `Bucket name cannot contain consecutive periods (.) ${bucketName}`;
} else if (/^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(bucketName)) {
error = `Bucket name cannot look like an IPv4 address. ${bucketName}`;
}
if (error) {
throw new this.serverless.classes.Error(error);
}
return true;
});
},
};

View File

@ -14,8 +14,10 @@ module.exports = {
this.objectsInBucket = [];
this.serverless.cli.log('Getting all objects in S3 bucket...');
const serviceStage = `${this.serverless.service.service}/${this.options.stage}`;
return this.sdk.request('S3', 'listObjectsV2', {
Bucket: this.bucketName,
Prefix: `serverless/${serviceStage}`,
}, this.options.stage, this.options.region).then((result) => {
if (result) {
result.Contents.forEach((object) => {

View File

@ -7,38 +7,43 @@ const Serverless = require('../../../Serverless');
const AwsSdk = require('../');
describe('AWS SDK', () => {
let awsSdk;
let serverless;
beforeEach(() => {
serverless = new Serverless();
const options = {
stage: 'dev',
region: 'us-east-1',
};
awsSdk = new AwsSdk(serverless, options);
awsSdk.serverless.cli = new serverless.classes.CLI();
});
describe('#constructor()', () => {
it('should set AWS instance', () => {
const serverless = new Serverless();
const awsSdk = new AwsSdk(serverless);
expect(typeof awsSdk.sdk).to.not.equal('undefined');
});
it('should set Serverless instance', () => {
const serverless = new Serverless();
const awsSdk = new AwsSdk(serverless);
expect(typeof awsSdk.serverless).to.not.equal('undefined');
});
it('should set AWS proxy', () => {
const serverless = new Serverless();
process.env.proxy = 'http://a.b.c.d:n';
const awsSdk = new AwsSdk(serverless);
const newAwsSdk = new AwsSdk(serverless);
expect(typeof awsSdk.sdk.config.httpOptions.agent).to.not.equal('undefined');
expect(typeof newAwsSdk.sdk.config.httpOptions.agent).to.not.equal('undefined');
// clear env
delete process.env.proxy;
});
it('should set AWS timeout', () => {
const serverless = new Serverless();
process.env.AWS_CLIENT_TIMEOUT = '120000';
const awsSdk = new AwsSdk(serverless);
const newAwsSdk = new AwsSdk(serverless);
expect(typeof awsSdk.sdk.config.httpOptions.timeout).to.not.equal('undefined');
expect(typeof newAwsSdk.sdk.config.httpOptions.timeout).to.not.equal('undefined');
// clear env
delete process.env.AWS_CLIENT_TIMEOUT;
@ -59,12 +64,10 @@ describe('AWS SDK', () => {
};
}
}
const serverless = new Serverless();
const awsSdk = new AwsSdk(serverless);
awsSdk.sdk = {
S3: FakeS3,
};
serverless.service.environment = {
awsSdk.serverless.service.environment = {
vars: {},
stages: {
dev: {
@ -75,42 +78,127 @@ describe('AWS SDK', () => {
},
},
};
serverless.service.environment.stages.dev.regions['us-east-1'] = {
vars: {},
};
return awsSdk.request('S3', 'putObject', {}, 'dev', 'us-east-1').then(data => {
expect(data.called).to.equal(true);
});
});
it('should retry if error code is 429', function (done) {
this.timeout(10000);
let first = true;
const error = {
statusCode: 429,
message: 'Testing retry',
};
class FakeS3 {
constructor(credentials) {
this.credentials = credentials;
}
error() {
return {
send(cb) {
if (first) {
cb(error);
} else {
cb(undefined, {});
}
first = false;
},
};
}
}
awsSdk.sdk = {
S3: FakeS3,
};
awsSdk.request('S3', 'error', {}, 'dev', 'us-east-1')
.then(data => {
// eslint-disable-next-line no-unused-expressions
expect(data).to.exist;
// eslint-disable-next-line no-unused-expressions
expect(first).to.be.false;
done();
})
.catch(done);
});
it('should reject errors', (done) => {
const error = {
statusCode: 500,
message: 'Some error message',
};
class FakeS3 {
constructor(credentials) {
this.credentials = credentials;
}
error() {
return {
send(cb) {
cb(error);
},
};
}
}
awsSdk.sdk = {
S3: FakeS3,
};
awsSdk.request('S3', 'error', {}, 'dev', 'us-east-1')
.then(() => done('Should not succeed'))
.catch(() => done());
});
it('should return ref to docs for missing credentials', (done) => {
const error = {
statusCode: 403,
message: 'Missing credentials in config',
};
class FakeS3 {
constructor(credentials) {
this.credentials = credentials;
}
error() {
return {
send(cb) {
cb(error);
},
};
}
}
awsSdk.sdk = {
S3: FakeS3,
};
awsSdk.request('S3', 'error', {}, 'dev', 'us-east-1')
.then(() => done('Should not succeed'))
.catch((err) => {
expect(err.message).to.contain('https://git.io/viZAC');
done();
})
.catch(done);
});
});
describe('#getCredentials()', () => {
it('should set region for credentials', () => {
const serverless = new Serverless();
const awsSdk = new AwsSdk(serverless);
const credentials = awsSdk.getCredentials('testregion');
expect(credentials.region).to.equal('testregion');
});
it('should get credentials from provider', () => {
const serverless = new Serverless();
const awsSdk = new AwsSdk(serverless);
serverless.service.provider.profile = 'notDefault';
const credentials = awsSdk.getCredentials();
expect(credentials.credentials.profile).to.equal('notDefault');
});
it('should not set credentials if empty profile is set', () => {
const serverless = new Serverless();
const awsSdk = new AwsSdk(serverless);
serverless.service.provider.profile = '';
const credentials = awsSdk.getCredentials('testregion');
expect(credentials).to.eql({ region: 'testregion' });
});
it('should not set credentials if profile is not set', () => {
const serverless = new Serverless();
const awsSdk = new AwsSdk(serverless);
serverless.service.provider.profile = undefined;
const credentials = awsSdk.getCredentials('testregion');
expect(credentials).to.eql({ region: 'testregion' });
@ -119,8 +207,6 @@ describe('AWS SDK', () => {
describe('#getServerlessDeploymentBucketName', () => {
it('should return the name of the serverless deployment bucket', () => {
const serverless = new Serverless();
const awsSdk = new AwsSdk(serverless);
const options = {
stage: 'dev',
region: 'us-east-1',
@ -152,9 +238,7 @@ describe('AWS SDK', () => {
describe('#getStackName', () => {
it('should return the stack name', () => {
const serverless = new Serverless();
serverless.service.service = 'myservice';
const awsSdk = new AwsSdk(serverless);
expect(awsSdk.getStackName('dev')).to.equal('myservice-dev');
});

View File

@ -4,7 +4,7 @@ const expect = require('chai').expect;
const validate = require('../lib/validate');
const Serverless = require('../../../Serverless');
describe('#validate()', () => {
describe('#validate', () => {
const serverless = new Serverless();
const awsPlugin = {};
@ -20,50 +20,143 @@ describe('#validate()', () => {
Object.assign(awsPlugin, validate);
});
it('should succeed if inside service (servicePath defined)', () => {
expect(() => awsPlugin.validate()).to.not.throw(Error);
});
describe('#validate()', () => {
it('should succeed if inside service (servicePath defined)', () => {
expect(() => awsPlugin.validate()).to.not.throw(Error);
});
it('should throw error if not inside service (servicePath not defined)', () => {
awsPlugin.serverless.config.servicePath = false;
expect(() => awsPlugin.validate()).to.throw(Error);
});
it('should throw error if not inside service (servicePath not defined)', () => {
awsPlugin.serverless.config.servicePath = false;
expect(() => awsPlugin.validate()).to.throw(Error);
});
// NOTE: starting here, test order is important
// NOTE: starting here, test order is important
it('should default to "dev" if stage is not provided', () => {
awsPlugin.options.stage = false;
return awsPlugin.validate().then(() => {
expect(awsPlugin.options.stage).to.equal('dev');
it('should default to "dev" if stage is not provided', () => {
awsPlugin.options.stage = false;
return awsPlugin.validate().then(() => {
expect(awsPlugin.options.stage).to.equal('dev');
});
});
it('should use the service.defaults stage if present', () => {
awsPlugin.options.stage = false;
awsPlugin.serverless.service.defaults = {
stage: 'some-stage',
};
return awsPlugin.validate().then(() => {
expect(awsPlugin.options.stage).to.equal('some-stage');
});
});
it('should default to "us-east-1" region if region is not provided', () => {
awsPlugin.options.region = false;
return awsPlugin.validate().then(() => {
expect(awsPlugin.options.region).to.equal('us-east-1');
});
});
it('should use the service.defaults region if present', () => {
awsPlugin.options.region = false;
awsPlugin.serverless.service.defaults = {
region: 'some-region',
};
return awsPlugin.validate().then(() => {
expect(awsPlugin.options.region).to.equal('some-region');
});
});
});
it('should use the service.defaults stage if present', () => {
awsPlugin.options.stage = false;
awsPlugin.serverless.service.defaults = {
stage: 'some-stage',
};
describe('#validateS3BucketName()', () => {
it('should reject an ip address as a name', () =>
awsPlugin.validateS3BucketName('127.0.0.1')
.then(() => {
throw new Error('Should not get here');
})
.catch(err => expect(err.message).to.contain('cannot look like an IPv4 address'))
);
return awsPlugin.validate().then(() => {
expect(awsPlugin.options.stage).to.equal('some-stage');
it('should reject names that are too long', () => {
const bucketName = Array.from({ length: 64 }, () => 'j').join('');
return awsPlugin.validateS3BucketName(bucketName)
.then(() => {
throw new Error('Should not get here');
})
.catch(err => expect(err.message).to.contain('longer than 63 characters'));
});
});
it('should default to "us-east-1" region if region is not provided', () => {
awsPlugin.options.region = false;
return awsPlugin.validate().then(() => {
expect(awsPlugin.options.region).to.equal('us-east-1');
});
});
it('should reject names that are too short', () =>
awsPlugin.validateS3BucketName('12')
.then(() => {
throw new Error('Should not get here');
})
.catch(err => expect(err.message).to.contain('shorter than 3 characters'))
);
it('should use the service.defaults region if present', () => {
awsPlugin.options.region = false;
awsPlugin.serverless.service.defaults = {
region: 'some-region',
};
it('should reject names that contain invalid characters', () =>
awsPlugin.validateS3BucketName('this has b@d characters')
.then(() => {
throw new Error('Should not get here');
})
.catch(err => expect(err.message).to.contain('contains invalid characters'))
);
return awsPlugin.validate().then(() => {
expect(awsPlugin.options.region).to.equal('some-region');
});
it('should reject names that have consecutive periods', () =>
awsPlugin.validateS3BucketName('otherwise..valid.name')
.then(() => {
throw new Error('Should not get here');
})
.catch(err => expect(err.message).to.contain('cannot contain consecutive periods'))
);
it('should reject names that start with a dash', () =>
awsPlugin.validateS3BucketName('-invalid.name')
.then(() => {
throw new Error('Should not get here');
})
.catch(err => expect(err.message).to.contain('start with a letter or number'))
);
it('should reject names that start with a period', () =>
awsPlugin.validateS3BucketName('.invalid.name')
.then(() => {
throw new Error('Should not get here');
})
.catch(err => expect(err.message).to.contain('start with a letter or number'))
);
it('should reject names that end with a dash', () =>
awsPlugin.validateS3BucketName('invalid.name-')
.then(() => {
throw new Error('Should not get here');
})
.catch(err => expect(err.message).to.contain('end with a letter or number'))
);
it('should reject names that end with a period', () =>
awsPlugin.validateS3BucketName('invalid.name.')
.then(() => {
throw new Error('Should not get here');
})
.catch(err => expect(err.message).to.contain('end with a letter or number'))
);
it('should reject names that contain uppercase letters', () =>
awsPlugin.validateS3BucketName('otherwise.Valid.name')
.then(() => {
throw new Error('Should not get here');
})
.catch(err => expect(err.message).to.contain('cannot contain uppercase letters'))
);
it('should accept valid names', () =>
awsPlugin.validateS3BucketName('1.this.is.valid.2')
.then(() => awsPlugin.validateS3BucketName('another.valid.name'))
.then(() => awsPlugin.validateS3BucketName('1-2-3'))
.then(() => awsPlugin.validateS3BucketName('123'))
.then(() => awsPlugin.validateS3BucketName('should.be.allowed-to-mix'))
);
});
});