mirror of
https://github.com/serverless/serverless.git
synced 2026-01-25 15:07:39 +00:00
Merge pull request #2189 from VivintSolar/deploy
Add a bucket to the provider for deployments.
This commit is contained in:
commit
34ff43373c
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@ -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',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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];
|
||||
|
||||
109
lib/plugins/aws/deploy/lib/configureStack.js
Normal file
109
lib/plugins/aws/deploy/lib/configureStack.js
Normal 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();
|
||||
},
|
||||
|
||||
};
|
||||
@ -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');
|
||||
|
||||
@ -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();
|
||||
},
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -8,3 +8,4 @@ require('./cleanupS3Bucket');
|
||||
require('./uploadArtifacts');
|
||||
require('./updateStack');
|
||||
require('./index');
|
||||
require('./configureStack');
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
193
lib/plugins/aws/deploy/tests/configureStack.js
Normal file
193
lib/plugins/aws/deploy/tests/configureStack.js
Normal 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);
|
||||
});
|
||||
});
|
||||
@ -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' }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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));
|
||||
});
|
||||
});
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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');
|
||||
});
|
||||
|
||||
@ -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'))
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user