This commit is contained in:
James Thomas 2017-07-12 11:27:01 +01:00
commit acedb40aaf
46 changed files with 6122 additions and 288 deletions

View File

@ -1,3 +1,16 @@
# 1.17.0 (05.07.2017)
- Cleanup F# build template output on macOS - #3897
- Add disable flag for OpenWhisk functions - #3830
- Only redeploy when the code/config changes - #3838
- Add opt-out config for dev dependency exclusion - #3877
- Add infinite stack trace for errors - #3839
- Fixed a bug with autocomplete - #3798
## Meta
- [Comparison since last release](https://github.com/serverless/serverless/compare/v1.16.1...v1.17.0)
# 1.16.1 (26.06.2017)
- CI/CD fix for the Serverless Platform - #3829

View File

@ -313,6 +313,7 @@ These consultants use the Serverless Framework and can help you build your serve
* [EPX Labs](http://www.epxlabs.com/) - runs [Serverless NYC Meetup](https://www.meetup.com/Serverless-NYC/)
* [Red Badger](https://red-badger.com)
* [Langa](http://langa.io/?utm_source=gh-serverless&utm_medium=github) - They built [Trails.js](http://github.com/trailsjs/trails)
* [Emerging Technology Advisors](https://www.emergingtechnologyadvisors.com)
----

View File

@ -18,9 +18,9 @@ menuItems:
The Serverless Framework is a CLI tool that allows users to build & deploy auto-scaling, pay-per-execution, event-driven functions.
Write your code, configure events to trigger your functions, then deploy & run those functions to your [cloud provider](#Supported-Providers) via the **serverless CLI**.
Write your code, configure events to trigger your functions, then deploy & run those functions to your [cloud provider](#Supported-Providers) via the **Serverless CLI**.
Getting started with serverless? **[Start here](./getting-started.md)**
Getting started with serverless? **[Start here](./getting-started.md)**.
Already using AWS or another cloud provider? Read on.

View File

@ -73,3 +73,41 @@ functions:
bucket: photos
event: s3:ObjectRemoved:*
```
## Custom bucket configuration
If you need to configure the bucket itself, you'll need to create the bucket and the Lambda Permission manually in
the Resources section while paying attention to some of the logical IDs. This relies on the Serverless naming convention. See the [Serverless Resource Reference](../guide/resources.md#aws-cloudformation-resource-reference) for details. These are the logical IDs that require your attention:
- The logical ID of the custom bucket in the Resources section needs to match the bucket name in the S3 event after the Serverless naming convention is applied to it.
- The Lambda Permission's logical ID needs to match the Serverless naming convention for Lambda Permissions for S3 events.
- The `FunctionName` in the Lambda Permission configuration needs to match the logical ID generated for the target Lambda function as determined by the Serverless naming convention.
The following example will work:
```yaml
functions:
resize:
handler: resize.handler
events:
- s3: photos
resources:
Resources:
S3BucketPhotos:
Type: AWS::S3::Bucket
Properties:
BucketName: my-custom-bucket-name
# add additional custom bucket configuration here
ResizeLambdaPermissionPhotosS3:
Type: "AWS::Lambda::Permission"
Properties:
FunctionName:
"Fn::GetAtt":
- ResizeLambdaFunction
- Arn
Principal: "s3.amazonaws.com"
Action: "lambda:InvokeFunction"
SourceAccount:
Ref: AWS::AccountId
SourceArn: "arn:aws:s3:::my-custom-bucket-name"
```

View File

@ -24,6 +24,8 @@ serverless deploy
Use this method when you have updated your Function, Event or Resource configuration in `serverless.yml` and you want to deploy that change (or multiple changes at the same time) to Amazon Web Services.
**Note:** You can always enforce a deployment using the `--force` option.
### How It Works
The Serverless Framework translates all syntax in `serverless.yml` to a single AWS CloudFormation template. By depending on CloudFormation for deployments, users of the Serverless Framework get the safety and reliability of CloudFormation.
@ -31,6 +33,8 @@ The Serverless Framework translates all syntax in `serverless.yml` to a single A
* An AWS CloudFormation template is created from your `serverless.yml`.
* If a Stack has not yet been created, then it is created with no resources except for an S3 Bucket, which will store zip files of your Function code.
* The code of your Functions is then packaged into zip files.
* Serverless fetches the hashes for all files of the previous deployment (if any) and compares them against the hashes of the local files.
* Serverless terminates the deployment process if all file hashes are the same.
* Zip files of your Functions' code are uploaded to your Code S3 Bucket.
* Any IAM Roles, Functions, Events and Resources are added to the AWS CloudFormation template.
* The CloudFormation Stack is updated with the new CloudFormation template.
@ -74,9 +78,13 @@ This deployment method does not touch your AWS CloudFormation Stack. Instead, i
serverless deploy function --function myFunction
```
**Note:** You can always enforce a deployment using the `--force` option.
### How It Works
* The Framework packages up the targeted AWS Lambda Function into a zip file.
* The Framework fetches the hash of the already uploaded function .zip file and compares it to the local .zip file hash.
* The Framework terminates if both hashes are the same.
* That zip file is uploaded to your S3 bucket using the same name as the previous function, which the CloudFormation stack is pointing to.
### Tips

View File

@ -330,7 +330,7 @@ functions:
The `onError` config currently only supports SNS topic arns due to a race condition when using SQS queue arns and updating the IAM role.
We're working on a fix so that SQS queue arns are be supported in the future.
We're working on a fix so that SQS queue arns will be supported in the future.
## KMS Keys

View File

@ -136,3 +136,10 @@ functions:
Serverless will auto-detect and exclude development dependencies based on the runtime your service is using.
This ensures that only the production relevant packages and modules are included in your zip file. Doing this drastically reduces the overall size of the deployment package which will be uploaded to the cloud provider.
You can opt-out of automatic dev dependency exclusion by setting the `excludeDevDependencies` package config to `false`:
```yml
package:
excludeDevDependencies: false
```

View File

@ -316,6 +316,8 @@ class MyPlugin {
module.exports = MyPlugin;
```
**Note:** [Variable references](./variables.md#reference-properties-in-serverlessyml) in the `serverless` instance are not resolved before a Plugin's constructor is called, so if you need these, make sure to wait to access those from your [hooks](#hooks).
### Command Naming
Command names need to be unique. If we load two commands and both want to specify the same command (e.g. we have an integrated command `deploy` and an external command also wants to use `deploy`) the Serverless CLI will print an error and exit. If you want to have your own `deploy` command you need to name it something different like `myCompanyDeploy` so they don't clash with existing plugins.

View File

@ -150,25 +150,43 @@ functions:
In the above example, you're referencing the entire `myCustomFile.yml` file in the `custom` property. You need to pass the path relative to your service directory. You can also request specific properties in that file as shown in the `schedule` property. It's completely recursive and you can go as deep as you want.
## Reference Variables in Javascript Files
To add dynamic data into your variables, reference javascript files by putting `${file(./myFile.js):someModule}` syntax in your `serverless.yml`. Here's an example:
You can reference JavaScript files to add dynamic data into your variables.
References can be either named or unnamed exports. To use the exported `someModule` in `myFile.js` you'd use the following code `${file(./myFile.js):someModule}`. For an unnamed export you'd write `${file(./myFile.js)}`.
Here are other examples:
```js
// myCustomFile.js
module.exports.hello = () => {
// scheduleConfig.js
module.exports.rate = () => {
// Code that generates dynamic data
return 'rate (10 minutes)';
}
```
```js
// config.js
module.exports = () => {
return {
property1: 'some value',
property2: 'some other value'
}
}
```
```yml
# serverless.yml
service: new-service
provider: aws
custom: ${file(./config.js)}
functions:
hello:
handler: handler.hello
events:
- schedule: ${file(./myCustomFile.js):hello} # Reference a specific module
- schedule: ${file(./scheduleConfig.js):rate} # Reference a specific module
```
You can also return an object and reference a specific property. Just make sure you are returning a valid object and referencing a valid property:

View File

@ -157,3 +157,10 @@ functions:
Serverless will auto-detect and exclude development dependencies based on the runtime your service is using.
This ensures that only the production relevant packages and modules are included in your zip file. Doing this drastically reduces the overall size of the deployment package which will be uploaded to the cloud provider.
You can opt-out of automatic dev dependency exclusion by setting the `excludeDevDependencies` package config to `false`:
```yml
package:
excludeDevDependencies: false
```

View File

@ -347,6 +347,8 @@ class MyPlugin {
module.exports = MyPlugin;
```
**Note:** [Variable references](./variables.md#reference-properties-in-serverlessyml) in the `serverless` instance are not resolved before a Plugin's constructor is called, so if you need these, make sure to wait to access those from your [hooks](#hooks).
### Command Naming
Command names need to be unique. If we load two commands and both want to specify

View File

@ -56,27 +56,40 @@ way, you can easily change the schedule for all functions whenever you like.
## Reference Variables in JavaScript Files
To add dynamic data into your variables, reference JavaScript files by putting
`${file(./myFile.js):someModule}` syntax in your `serverless.yml`. Here's an
example:
You can reference JavaScript files to add dynamic data into your variables.
References can be either named or unnamed exports. To use the exported `someModule` in `myFile.js` you'd use the following code `${file(./myFile.js):someModule}`. For an unnamed export you'd write `${file(./myFile.js)}`.
```js
// myCustomFile.js
module.exports.hello = () => {
// scheduleConfig.js
module.exports.cron = () => {
// Code that generates dynamic data
return 'cron(0 * * * *)';
}
```
```js
// config.js
module.exports = () => {
return {
property1: 'some value',
property2: 'some other value'
}
}
```
```yml
# serverless.yml
service: new-service
provider: azure
custom: ${file(./config.js)}
functions:
hello:
handler: handler.hello
events:
- timer: ${file(./myCustomFile.js):hello} # Reference a specific module
- timer: ${file(./scheduleConfig.js):cron} # Reference a specific module
```
You can also return an object and reference a specific property. Just make sure

View File

@ -129,3 +129,10 @@ functions:
Serverless will auto-detect and exclude development dependencies based on the runtime your service is using.
This ensures that only the production relevant packages and modules are included in your zip file. Doing this drastically reduces the overall size of the deployment package which will be uploaded to the cloud provider.
You can opt-out of automatic dev dependency exclusion by setting the `excludeDevDependencies` package config to `false`:
```yml
package:
excludeDevDependencies: false
```

View File

@ -315,6 +315,8 @@ class MyPlugin {
module.exports = MyPlugin;
```
**Note:** [Variable references](./variables.md#reference-properties-in-serverlessyml) in the `serverless` instance are not resolved before a Plugin's constructor is called, so if you need these, make sure to wait to access those from your [hooks](#hooks).
### Command Naming
Command names need to be unique. If we load two commands and both want to specify the same command (e.g. we have an integrated command `deploy` and an external command also wants to use `deploy`) the Serverless CLI will print an error and exit. If you want to have your own `deploy` command you need to name it something different like `myCompanyDeploy` so they don't clash with existing plugins.

View File

@ -52,29 +52,43 @@ In the above example you're setting a global event resource for all functions by
## Reference Variables in JavaScript Files
To add dynamic data into your variables, reference javascript files by putting `${file(./myFile.js):someModule}` syntax in your `serverless.yml`. Here's an example:
You can reference JavaScript files to add dynamic data into your variables.
References can be either named or unnamed exports. To use the exported `someModule` in `myFile.js` you'd use the following code `${file(./myFile.js):someModule}`. For an unnamed export you'd write `${file(./myFile.js)}`.
```javascript
// myCustomFile.js
module.exports.resource = () => {
// resources.js
module.exports.topic = () => {
// Code that generates dynamic data
return 'projects/*/topics/my-topic';
}
```
```js
// config.js
module.exports = () => {
return {
property1: 'some value',
property2: 'some other value'
}
}
```
```yml
# serverless.yml
service: new-service
provider: google
custom: ${file(./config.js)}
functions:
first:
handler: pubSub
events:
- event:
eventType: providers/cloud.pubsub/eventTypes/topics.publish
resource: ${file(./myCustomFile.js):resource} # Reference a specific module
resource: ${file(./resources.js):topic} # Reference a specific module
```
You can also return an object and reference a specific property. Just make sure you are returning a valid object and referencing a valid property:

View File

@ -32,7 +32,7 @@ functions:
crawl:
handler: crawl
events:
- schedule: cron(* * * * * *) // run every minute
- schedule: cron(* * * * *) // run every minute
```
This automatically generates a new trigger (``${service}_crawl_schedule_trigger`)

View File

@ -118,8 +118,39 @@ functions:
- some-file.js
```
### Exclude functions from packaging
Sometimes you might want to exclude functions from the packaging process. The `disable` config parameter which can be defined in the `package` config on the function level enables you a way to mark a function for exclusion.
```yml
service: my-service
package:
individually: true
exclude:
- '**/*'
functions:
hello:
handler: handler.hello
package:
include:
- handler.js
world:
handler: handler.hello
package:
disable: true
```
### Development dependencies
Serverless will auto-detect and exclude development dependencies based on the runtime your service is using.
This ensures that only the production relevant packages and modules are included in your zip file. Doing this drastically reduces the overall size of the deployment package which will be uploaded to the cloud provider.
You can opt-out of automatic dev dependency exclusion by setting the `excludeDevDependencies` package config to `false`:
```yml
package:
excludeDevDependencies: false
```

View File

@ -316,6 +316,8 @@ class MyPlugin {
module.exports = MyPlugin;
```
**Note:** [Variable references](./variables.md#reference-properties-in-serverlessyml) in the `serverless` instance are not resolved before a Plugin's constructor is called, so if you need these, make sure to wait to access those from your [hooks](#hooks).
### Command Naming
Command names need to be unique. If we load two commands and both want to specify the same command (e.g. we have an integrated command `deploy` and an external command also wants to use `deploy`) the Serverless CLI will print an error and exit. If you want to have your own `deploy` command you need to name it something different like `myCompanyDeploy` so they don't clash with existing plugins.

View File

@ -106,25 +106,41 @@ functions:
In the above example, you're referencing the entire `myCustomFile.yml` file in the `custom` property. You need to pass the path relative to your service directory. You can also request specific properties in that file as shown in the `schedule` property. It's completely recursive and you can go as deep as you want.
## Reference Variables in Javascript Files
To add dynamic data into your variables, reference javascript files by putting `${file(./myFile.js):someModule}` syntax in your `serverless.yml`. Here's an example:
You can reference JavaScript files to add dynamic data into your variables.
References can be either named or unnamed exports. To use the exported `someModule` in `myFile.js` you'd use the following code `${file(./myFile.js):someModule}`. For an unnamed export you'd write `${file(./myFile.js)}`.
```js
// myCustomFile.js
module.exports.hello = () => {
// scheduleConfig.js
module.exports.cron = () => {
// Code that generates dynamic data
return 'cron(0 * * * *)';
}
```
```js
// config.js
module.exports = () => {
return {
property1: 'some value',
property2: 'some other value'
}
}
```
```yml
# serverless.yml
service: new-service
provider: openwhisk
custom: ${file(./config.js)}
functions:
hello:
handler: handler.hello
events:
- schedule: ${file(./myCustomFile.js):hello} # Reference a specific module
- schedule: ${file(./scheduleConfig.js):cron} # Reference a specific module
```
You can also return an object and reference a specific property. Just make sure you are returning a valid object and referencing a valid property:

View File

@ -16,7 +16,7 @@ class Service {
this.provider = {
stage: 'dev',
region: 'us-east-1',
variableSyntax: '\\${([ :a-zA-Z0-9._,\\-\\/\\(\\)]+?)}',
variableSyntax: '\\${([ ~:a-zA-Z0-9._,\\-\\/\\(\\)]+?)}',
};
this.custom = {};
this.plugins = [];
@ -125,6 +125,7 @@ class Service {
that.package.artifact = serverlessFile.package.artifact;
that.package.exclude = serverlessFile.package.exclude;
that.package.include = serverlessFile.package.include;
that.package.excludeDevDependencies = serverlessFile.package.excludeDevDependencies;
}
return this;

View File

@ -31,7 +31,7 @@ describe('Service', () => {
expect(serviceInstance.provider).to.deep.equal({
stage: 'dev',
region: 'us-east-1',
variableSyntax: '\\${([ :a-zA-Z0-9._,\\-\\/\\(\\)]+?)}',
variableSyntax: '\\${([ ~:a-zA-Z0-9._,\\-\\/\\(\\)]+?)}',
});
expect(serviceInstance.custom).to.deep.equal({});
expect(serviceInstance.plugins).to.deep.equal([]);
@ -80,6 +80,7 @@ describe('Service', () => {
expect(serviceInstance.package.include.length).to.equal(1);
expect(serviceInstance.package.include[0]).to.equal('include-me');
expect(serviceInstance.package.artifact).to.equal('some/path/foo.zip');
expect(serviceInstance.package.excludeDevDependencies).to.equal(undefined);
});
it('should support string based provider config', () => {
@ -147,6 +148,7 @@ describe('Service', () => {
exclude: ['exclude-me'],
include: ['include-me'],
artifact: 'some/path/foo.zip',
excludeDevDependencies: false,
},
};
@ -172,6 +174,7 @@ describe('Service', () => {
expect(serviceInstance.package.include.length).to.equal(1);
expect(serviceInstance.package.include[0]).to.equal('include-me');
expect(serviceInstance.package.artifact).to.equal('some/path/foo.zip');
expect(serviceInstance.package.excludeDevDependencies).to.equal(false);
});
});
@ -225,6 +228,7 @@ describe('Service', () => {
expect(serviceInstance.package.include.length).to.equal(1);
expect(serviceInstance.package.include[0]).to.equal('include-me');
expect(serviceInstance.package.artifact).to.equal('some/path/foo.zip');
expect(serviceInstance.package.excludeDevDependencies).to.equal(undefined);
});
});
@ -278,6 +282,7 @@ describe('Service', () => {
expect(serviceInstance.package.include.length).to.equal(1);
expect(serviceInstance.package.include[0]).to.equal('include-me');
expect(serviceInstance.package.artifact).to.equal('some/path/foo.zip');
expect(serviceInstance.package.excludeDevDependencies).to.equal(undefined);
});
});

View File

@ -249,7 +249,7 @@ class Utils {
}
let hasCustomVariableSyntaxDefined = false;
const defaultVariableSyntax = '\\${([ :a-zA-Z0-9._,\\-\\/\\(\\)]+?)}';
const defaultVariableSyntax = '\\${([ ~:a-zA-Z0-9._,\\-\\/\\(\\)]+?)}';
// check if the variableSyntax in the provider section is defined
if (provider && provider.variableSyntax

View File

@ -5,6 +5,7 @@ const path = require('path');
const replaceall = require('replaceall');
const logWarning = require('./Error').logWarning;
const BbPromise = require('bluebird');
const os = require('os');
class Variables {
@ -13,7 +14,7 @@ class Variables {
this.service = this.serverless.service;
this.overwriteSyntax = RegExp(/,/g);
this.fileRefSyntax = RegExp(/^file\(([a-zA-Z0-9._\-/]+?)\)/g);
this.fileRefSyntax = RegExp(/^file\((~?[a-zA-Z0-9._\-/]+?)\)/g);
this.envRefSyntax = RegExp(/^env:/g);
this.optRefSyntax = RegExp(/^opt:/g);
this.selfRefSyntax = RegExp(/^self:/g);
@ -215,12 +216,14 @@ class Variables {
getValueFromFile(variableString) {
const matchedFileRefString = variableString.match(this.fileRefSyntax)[0];
const referencedFileRelativePath = matchedFileRefString
.replace(this.fileRefSyntax, (match, varName) => varName.trim());
const referencedFileFullPath = path.join(this.serverless.config.servicePath,
referencedFileRelativePath);
.replace(this.fileRefSyntax, (match, varName) => varName.trim())
.replace('~', os.homedir());
const referencedFileFullPath = (path.isAbsolute(referencedFileRelativePath) ?
referencedFileRelativePath :
path.join(this.serverless.config.servicePath, referencedFileRelativePath));
let fileExtension = referencedFileRelativePath.split('.');
fileExtension = fileExtension[fileExtension.length - 1];
// Validate file exists
if (!this.serverless.utils.fileExistsSync(referencedFileFullPath)) {
return BbPromise.resolve(undefined);
@ -231,9 +234,25 @@ class Variables {
// Process JS files
if (fileExtension === 'js') {
const jsFile = require(referencedFileFullPath); // eslint-disable-line global-require
let jsModule = variableString.split(':')[1];
jsModule = jsModule.split('.')[0];
valueToPopulate = jsFile[jsModule]();
const variableArray = variableString.split(':');
let returnValueFunction;
if (variableArray[1]) {
let jsModule = variableArray[1];
jsModule = jsModule.split('.')[0];
returnValueFunction = jsFile[jsModule];
} else {
returnValueFunction = jsFile;
}
if (typeof returnValueFunction !== 'function') {
throw new this.serverless.classes
.Error([
'Invalid variable syntax when referencing',
` file "${referencedFileRelativePath}".`,
' Check if your javascript is exporting a function that returns a value.',
].join(''));
}
valueToPopulate = returnValueFunction();
return BbPromise.resolve(valueToPopulate).then(valueToPopulateResolved => {
let deepProperties = variableString.replace(matchedFileRefString, '');
@ -274,7 +293,6 @@ class Variables {
return this.getDeepValue(deepProperties, valueToPopulate);
}
}
return BbPromise.resolve(valueToPopulate);
}

View File

@ -12,6 +12,7 @@ const testUtils = require('../../tests/utils');
const slsError = require('./Error');
const AwsProvider = require('../plugins/aws/provider/awsProvider');
const BbPromise = require('bluebird');
const os = require('os');
describe('Variables', () => {
describe('#constructor()', () => {
@ -535,6 +536,33 @@ describe('Variables', () => {
});
describe('#getValueFromFile()', () => {
it('should work for absolute paths with ~ ', () => {
const serverless = new Serverless();
const expectedFileName = `${os.homedir}/somedir/config.yml`;
const configYml = {
test: 1,
test2: 'test2',
testObj: {
sub: 2,
prob: 'prob',
},
};
const fileExistsStub = sinon
.stub(serverless.utils, 'fileExistsSync').returns(true);
const readFileSyncStub = sinon
.stub(serverless.utils, 'readFileSync').returns(configYml);
return serverless.variables.getValueFromFile('file(~/somedir/config.yml)')
.then(valueToPopulate => {
expect(fileExistsStub.calledWithMatch(expectedFileName));
expect(readFileSyncStub.calledWithMatch(expectedFileName));
expect(valueToPopulate).to.deep.equal(configYml);
readFileSyncStub.restore();
fileExistsStub.restore();
});
});
it('should populate an entire variable file', () => {
const serverless = new Serverless();
const SUtils = new Utils();
@ -639,6 +667,36 @@ describe('Variables', () => {
});
});
it('should populate an entire variable exported by a javascript file', () => {
const serverless = new Serverless();
const SUtils = new Utils();
const tmpDirPath = testUtils.getTmpDirPath();
const jsData = 'module.exports=function(){return { hello: "hello world" };};';
SUtils.writeFileSync(path.join(tmpDirPath, 'hello.js'), jsData);
serverless.config.update({ servicePath: tmpDirPath });
return serverless.variables.getValueFromFile('file(./hello.js)')
.then(valueToPopulate => {
expect(valueToPopulate.hello).to.equal('hello world');
});
});
it('should thow if property exported by a javascript file is not a function', () => {
const serverless = new Serverless();
const SUtils = new Utils();
const tmpDirPath = testUtils.getTmpDirPath();
const jsData = 'module.exports={ hello: "hello world" };';
SUtils.writeFileSync(path.join(tmpDirPath, 'hello.js'), jsData);
serverless.config.update({ servicePath: tmpDirPath });
expect(() => serverless.variables
.getValueFromFile('file(./hello.js)')).to.throw(Error);
});
it('should populate deep object from a javascript file', () => {
const serverless = new Serverless();
const SUtils = new Utils();

View File

@ -10,9 +10,10 @@
const BbPromise = require('bluebird');
const extendedValidate = require('./lib/extendedValidate');
const setBucketName = require('../lib/setBucketName');
const checkForChanges = require('./lib/checkForChanges');
const monitorStack = require('../lib/monitorStack');
const createStack = require('./lib/createStack');
const setBucketName = require('../lib/setBucketName');
const cleanupS3Bucket = require('./lib/cleanupS3Bucket');
const uploadArtifacts = require('./lib/uploadArtifacts');
const validateTemplate = require('./lib/validateTemplate');
@ -34,6 +35,7 @@ class AwsDeploy {
extendedValidate,
createStack,
setBucketName,
checkForChanges,
cleanupS3Bucket,
uploadArtifacts,
validateTemplate,
@ -51,6 +53,7 @@ class AwsDeploy {
deploy: {
lifecycleEvents: [
'createStack',
'checkForChanges',
'uploadArtifacts',
'validateTemplate',
'updateStack',
@ -94,20 +97,38 @@ class AwsDeploy {
'aws:deploy:deploy:createStack': () => BbPromise.bind(this)
.then(this.createStack),
'aws:deploy:deploy:uploadArtifacts': () => BbPromise.bind(this)
'aws:deploy:deploy:checkForChanges': () => BbPromise.bind(this)
.then(this.setBucketName)
.then(this.uploadArtifacts),
.then(this.checkForChanges),
'aws:deploy:deploy:uploadArtifacts': () => BbPromise.bind(this)
.then(() => {
if (this.serverless.service.provider.shouldNotDeploy) {
return BbPromise.resolve();
}
return BbPromise.bind(this).then(this.uploadArtifacts);
}),
'aws:deploy:deploy:validateTemplate': () => BbPromise.bind(this)
.then(this.validateTemplate),
.then(() => {
if (this.serverless.service.provider.shouldNotDeploy) {
return BbPromise.resolve();
}
return BbPromise.bind(this).then(this.validateTemplate);
}),
'aws:deploy:deploy:updateStack': () => BbPromise.bind(this)
.then(this.updateStack),
.then(() => {
if (this.serverless.service.provider.shouldNotDeploy) {
return BbPromise.resolve();
}
return BbPromise.bind(this).then(this.updateStack);
}),
// Deploy finalize inner lifecycle
'aws:deploy:finalize:cleanup': () => BbPromise.bind(this)
.then(() => {
if (this.options.noDeploy) {
if (this.options.noDeploy || this.serverless.service.provider.shouldNotDeploy) {
return BbPromise.resolve();
}
return this.cleanupS3Bucket();

View File

@ -84,9 +84,7 @@ describe('AwsDeploy', () => {
let spawnStub;
let createStackStub;
let setBucketNameStub;
let uploadArtifactsStub;
let validateTemplateStub;
let updateStackStub;
let checkForChangesStub;
beforeEach(() => {
spawnStub = sinon
@ -95,21 +93,15 @@ describe('AwsDeploy', () => {
.stub(awsDeploy, 'createStack').resolves();
setBucketNameStub = sinon
.stub(awsDeploy, 'setBucketName').resolves();
uploadArtifactsStub = sinon
.stub(awsDeploy, 'uploadArtifacts').resolves();
validateTemplateStub = sinon
.stub(awsDeploy, 'validateTemplate').resolves();
updateStackStub = sinon
.stub(awsDeploy, 'updateStack').resolves();
checkForChangesStub = sinon
.stub(awsDeploy, 'checkForChanges').resolves();
});
afterEach(() => {
serverless.pluginManager.spawn.restore();
awsDeploy.createStack.restore();
awsDeploy.setBucketName.restore();
awsDeploy.uploadArtifacts.restore();
awsDeploy.validateTemplate.restore();
awsDeploy.updateStack.restore();
awsDeploy.checkForChanges.restore();
});
describe('"before:deploy:deploy" hook', () => {
@ -193,24 +185,96 @@ describe('AwsDeploy', () => {
})
);
it('should run "aws:deploy:deploy:uploadArtifacts" hook', () => awsDeploy
.hooks['aws:deploy:deploy:uploadArtifacts']().then(() => {
it('should run "aws:deploy:deploy:checkForChanges" hook', () => awsDeploy
.hooks['aws:deploy:deploy:checkForChanges']().then(() => {
expect(setBucketNameStub.calledOnce).to.equal(true);
expect(uploadArtifactsStub.calledAfter(setBucketNameStub)).to.equal(true);
expect(checkForChangesStub.calledAfter(setBucketNameStub)).to.equal(true);
})
);
it('should run "aws:deploy:deploy:validateTemplate" hook', () => expect(awsDeploy
.hooks['aws:deploy:deploy:validateTemplate']()).to.be.fulfilled.then(() => {
expect(validateTemplateStub).to.have.been.calledOnce;
})
);
describe('"aws:deploy:deploy:uploadArtifacts" hook', () => {
let uploadArtifactsStub;
it('should run "aws:deploy:deploy:updateStack" hook', () => awsDeploy
.hooks['aws:deploy:deploy:updateStack']().then(() => {
expect(updateStackStub.calledOnce).to.equal(true);
})
);
beforeEach(() => {
uploadArtifactsStub = sinon
.stub(awsDeploy, 'uploadArtifacts').resolves();
});
afterEach(() => {
awsDeploy.uploadArtifacts.restore();
});
it('should upload the artifacts if a deployment is necessary', () => expect(awsDeploy
.hooks['aws:deploy:deploy:uploadArtifacts']()).to.be.fulfilled.then(() => {
expect(uploadArtifactsStub).to.have.been.calledOnce;
})
);
it('should resolve if no deployment is necessary', () => {
awsDeploy.serverless.service.provider.shouldNotDeploy = true;
return expect(awsDeploy
.hooks['aws:deploy:deploy:uploadArtifacts']()).to.be.fulfilled.then(() => {
expect(uploadArtifactsStub).to.not.have.been.called;
});
});
});
describe('"aws:deploy:deploy:validateTemplate" hook', () => {
let validateTemplateStub;
beforeEach(() => {
validateTemplateStub = sinon
.stub(awsDeploy, 'validateTemplate').resolves();
});
afterEach(() => {
awsDeploy.validateTemplate.restore();
});
it('should validate the template if a deployment is necessary', () => expect(awsDeploy
.hooks['aws:deploy:deploy:validateTemplate']()).to.be.fulfilled.then(() => {
expect(validateTemplateStub).to.have.been.calledOnce;
})
);
it('should resolve if no deployment is necessary', () => {
awsDeploy.serverless.service.provider.shouldNotDeploy = true;
return expect(awsDeploy
.hooks['aws:deploy:deploy:validateTemplate']()).to.be.fulfilled.then(() => {
expect(validateTemplateStub).to.not.have.been.called;
});
});
});
describe('"aws:deploy:deploy:updateStack" hook', () => {
let updateStackStub;
beforeEach(() => {
updateStackStub = sinon
.stub(awsDeploy, 'updateStack').resolves();
});
afterEach(() => {
awsDeploy.updateStack.restore();
});
it('should update the stack if a deployment is necessary', () => expect(awsDeploy
.hooks['aws:deploy:deploy:updateStack']()).to.be.fulfilled.then(() => {
expect(updateStackStub).to.have.been.calledOnce;
})
);
it('should resolve if no deployment is necessary', () => {
awsDeploy.serverless.service.provider.shouldNotDeploy = true;
return expect(awsDeploy
.hooks['aws:deploy:deploy:updateStack']()).to.be.fulfilled.then(() => {
expect(updateStackStub).to.not.have.been.called;
});
});
});
describe('"aws:deploy:finalize:cleanup" hook', () => {
let cleanupS3BucketStub;
@ -258,6 +322,15 @@ describe('AwsDeploy', () => {
.to.equal(true);
});
});
it('should not cleanup if a deployment was not necessary', () => {
awsDeploy.serverless.service.provider.shouldNotDeploy = true;
return awsDeploy.hooks['aws:deploy:finalize:cleanup']().then(() => {
expect(cleanupS3BucketStub.called).to.equal(false);
expect(spawnAwsCommonCleanupTempDirStub.called).to.equal(false);
});
});
});
});
});

View File

@ -0,0 +1,119 @@
'use strict';
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const globby = require('globby');
const BbPromise = require('bluebird');
const _ = require('lodash');
const normalizeFiles = require('../../lib/normalizeFiles');
module.exports = {
checkForChanges() {
this.serverless.service.provider.shouldNotDeploy = false;
if (this.options.force) {
return BbPromise.resolve();
}
return BbPromise.bind(this)
.then(this.getMostRecentObjects)
.then(this.getObjectMetadata)
.then(this.checkIfDeploymentIsNecessary);
},
getMostRecentObjects() {
const service = this.serverless.service.service;
const params = {
Bucket: this.bucketName,
Prefix: `serverless/${service}/${this.options.stage}`,
};
return this.provider.request('S3',
'listObjectsV2',
params,
this.options.stage,
this.options.region
).then((result) => {
if (result && result.Contents && result.Contents.length) {
const objects = result.Contents;
const ordered = _.orderBy(objects, ['Key'], ['desc']);
const firstKey = ordered[0].Key;
const directory = firstKey.substring(0, firstKey.lastIndexOf('/'));
const mostRecentObjects = ordered.filter((obj) => {
const objKey = obj.Key;
const objDirectory = objKey.substring(0, objKey.lastIndexOf('/'));
return directory === objDirectory;
});
return BbPromise.resolve(mostRecentObjects);
}
return BbPromise.resolve([]);
});
},
getObjectMetadata(objects) {
if (objects && objects.length) {
const headObjectObjects = objects
.map((obj) => this.provider.request('S3',
'headObject',
{
Bucket: this.bucketName,
Key: obj.Key,
},
this.options.stage,
this.options.region
));
return BbPromise.all(headObjectObjects)
.then((result) => result);
}
return BbPromise.resolve([]);
},
checkIfDeploymentIsNecessary(objects) {
if (objects && objects.length) {
const remoteHashes = objects.map((object) => object.Metadata.filesha256 || '');
const serverlessDirPath = path.join(this.serverless.config.servicePath, '.serverless');
// create a hash of the CloudFormation body
const compiledCfTemplate = this.serverless.service.provider.compiledCloudFormationTemplate;
const normCfTemplate = normalizeFiles.normalizeCloudFormationTemplate(compiledCfTemplate);
const localCfHash = crypto
.createHash('sha256')
.update(JSON.stringify(normCfTemplate))
.digest('base64');
// create hashes for all the zip files
const zipFiles = globby.sync(['**.zip'], { cwd: serverlessDirPath, dot: true, silent: true });
const zipFilePaths = zipFiles.map((zipFile) => path.join(serverlessDirPath, zipFile));
const zipFileHashes = zipFilePaths.map((zipFilePath) => {
// TODO refactor to be async (use util function to compute checksum async)
const zipFile = fs.readFileSync(zipFilePath);
return crypto.createHash('sha256').update(zipFile).digest('base64');
});
const localHashes = zipFileHashes;
localHashes.push(localCfHash);
if (_.isEqual(remoteHashes.sort(), localHashes.sort())) {
this.serverless.service.provider.shouldNotDeploy = true;
const message = [
'Service files not changed. Skipping deployment...',
].join('');
this.serverless.cli.log(message);
}
}
return BbPromise.resolve();
},
};

View File

@ -0,0 +1,491 @@
'use strict';
/* eslint-disable no-unused-expressions */
const fs = require('fs');
const path = require('path');
const globby = require('globby');
const sinon = require('sinon');
const chai = require('chai');
const proxyquire = require('proxyquire');
const normalizeFiles = require('../../lib/normalizeFiles');
const AwsProvider = require('../../provider/awsProvider');
const AwsDeploy = require('../index');
const Serverless = require('../../../../Serverless');
// Configure chai
chai.use(require('chai-as-promised'));
chai.use(require('sinon-chai'));
const expect = require('chai').expect;
describe('checkForChanges', () => {
let serverless;
let awsDeploy;
let s3Key;
let cryptoStub;
beforeEach(() => {
serverless = new Serverless();
serverless.config.servicePath = 'my-service';
serverless.setProvider('aws', new AwsProvider(serverless));
serverless.service.service = 'my-service';
const options = {
stage: 'dev',
region: 'us-east-1',
};
awsDeploy = new AwsDeploy(serverless, options);
awsDeploy.bucketName = 'deployment-bucket';
awsDeploy.serverless.service.provider.compiledCloudFormationTemplate = {
foo: 'bar',
};
s3Key = `serverless/${serverless.service.service}/${options.stage}`;
awsDeploy.serverless.cli = { log: sinon.spy() };
cryptoStub = {
createHash: function () { return this; }, // eslint-disable-line
update: function () { return this; }, // eslint-disable-line
digest: sinon.stub(),
};
const checkForChanges = proxyquire('./checkForChanges.js', {
crypto: cryptoStub,
});
Object.assign(
awsDeploy,
checkForChanges
);
});
describe('#checkForChanges()', () => {
let getMostRecentObjectsStub;
let getObjectMetadataStub;
let checkIfDeploymentIsNecessaryStub;
beforeEach(() => {
getMostRecentObjectsStub = sinon
.stub(awsDeploy, 'getMostRecentObjects').resolves();
getObjectMetadataStub = sinon
.stub(awsDeploy, 'getObjectMetadata').resolves();
checkIfDeploymentIsNecessaryStub = sinon
.stub(awsDeploy, 'checkIfDeploymentIsNecessary').resolves();
});
afterEach(() => {
awsDeploy.getMostRecentObjects.restore();
awsDeploy.getObjectMetadata.restore();
awsDeploy.checkIfDeploymentIsNecessary.restore();
});
it('should run promise chain in order', () => expect(awsDeploy.checkForChanges())
.to.be.fulfilled.then(() => {
expect(getMostRecentObjectsStub).to.have.been.calledOnce;
expect(getObjectMetadataStub).to.have.been.calledAfter(getMostRecentObjectsStub);
expect(checkIfDeploymentIsNecessaryStub).to.have.been.calledAfter(getObjectMetadataStub);
expect(awsDeploy.serverless.service.provider.shouldNotDeploy).to.equal(false);
})
);
it('should resolve if the "force" option is used', () => {
awsDeploy.options.force = true;
return expect(awsDeploy.checkForChanges())
.to.be.fulfilled.then(() => {
expect(getMostRecentObjectsStub).to.not.have.been.called;
expect(getObjectMetadataStub).to.not.have.been.called;
expect(checkIfDeploymentIsNecessaryStub).to.not.have.been.called;
expect(awsDeploy.serverless.service.provider.shouldNotDeploy).to.equal(false);
});
});
});
describe('#getMostRecentObjects()', () => {
let listObjectsV2Stub;
beforeEach(() => {
listObjectsV2Stub = sinon
.stub(awsDeploy.provider, 'request');
});
afterEach(() => {
awsDeploy.provider.request.restore();
});
it('should resolve if no result is returned', () => {
listObjectsV2Stub.resolves();
return expect(awsDeploy.getMostRecentObjects()).to.be.fulfilled.then((result) => {
expect(listObjectsV2Stub).to.have.been.calledWithExactly(
'S3',
'listObjectsV2',
{
Bucket: awsDeploy.bucketName,
Prefix: 'serverless/my-service/dev',
},
awsDeploy.options.stage,
awsDeploy.options.region
);
expect(result).to.deep.equal([]);
});
});
it('should resolve if result array is empty', () => {
const serviceObjects = {
Contents: [],
};
listObjectsV2Stub.resolves(serviceObjects);
return expect(awsDeploy.getMostRecentObjects()).to.be.fulfilled.then((result) => {
expect(listObjectsV2Stub).to.have.been.calledWithExactly(
'S3',
'listObjectsV2',
{
Bucket: awsDeploy.bucketName,
Prefix: 'serverless/my-service/dev',
},
awsDeploy.options.stage,
awsDeploy.options.region
);
expect(result).to.deep.equal([]);
});
});
it('should resolve with the most recently deployed objects', () => {
const serviceObjects = {
Contents: [
{ Key: `${s3Key}/151224711231-2016-08-18T15:43:00/artifact.zip` },
{ Key: `${s3Key}/151224711231-2016-08-18T15:43:00/cloudformation.json` },
{ Key: `${s3Key}/141264711231-2016-08-18T15:42:00/artifact.zip` },
{ Key: `${s3Key}/141264711231-2016-08-18T15:42:00/cloudformation.json` },
],
};
listObjectsV2Stub.resolves(serviceObjects);
return expect(awsDeploy.getMostRecentObjects()).to.be.fulfilled.then((result) => {
expect(listObjectsV2Stub).to.have.been.calledWithExactly(
'S3',
'listObjectsV2',
{
Bucket: awsDeploy.bucketName,
Prefix: 'serverless/my-service/dev',
},
awsDeploy.options.stage,
awsDeploy.options.region
);
expect(result).to.deep.equal([
{ Key: `${s3Key}/151224711231-2016-08-18T15:43:00/cloudformation.json` },
{ Key: `${s3Key}/151224711231-2016-08-18T15:43:00/artifact.zip` },
]);
});
});
});
describe('#getObjectMetadata()', () => {
let headObjectStub;
beforeEach(() => {
headObjectStub = sinon
.stub(awsDeploy.provider, 'request').resolves();
});
afterEach(() => {
awsDeploy.provider.request.restore();
});
it('should resolve if no input is provided', () => expect(awsDeploy.getObjectMetadata())
.to.be.fulfilled.then((result) => {
expect(headObjectStub).to.not.have.been.called;
expect(result).to.deep.equal([]);
})
);
it('should resolve if no objects are provided as input', () => {
const input = [];
return expect(awsDeploy.getObjectMetadata(input)).to.be.fulfilled.then((result) => {
expect(headObjectStub).to.not.have.been.called;
expect(result).to.deep.equal([]);
});
});
it('should request the object detailed information', () => {
const input = [
{ Key: `${s3Key}/151224711231-2016-08-18T15:43:00/artifact.zip` },
{ Key: `${s3Key}/151224711231-2016-08-18T15:43:00/cloudformation.json` },
{ Key: `${s3Key}/141264711231-2016-08-18T15:42:00/artifact.zip` },
{ Key: `${s3Key}/141264711231-2016-08-18T15:42:00/cloudformation.json` },
];
return expect(awsDeploy.getObjectMetadata(input)).to.be.fulfilled.then(() => {
expect(headObjectStub.callCount).to.equal(4);
expect(headObjectStub).to.have.been.calledWithExactly(
'S3',
'headObject',
{
Bucket: awsDeploy.bucketName,
Key: `${s3Key}/151224711231-2016-08-18T15:43:00/artifact.zip`,
},
awsDeploy.options.stage,
awsDeploy.options.region
);
expect(headObjectStub).to.have.been.calledWithExactly(
'S3',
'headObject',
{
Bucket: awsDeploy.bucketName,
Key: `${s3Key}/151224711231-2016-08-18T15:43:00/cloudformation.json`,
},
awsDeploy.options.stage,
awsDeploy.options.region
);
expect(headObjectStub).to.have.been.calledWithExactly(
'S3',
'headObject',
{
Bucket: awsDeploy.bucketName,
Key: `${s3Key}/141264711231-2016-08-18T15:42:00/artifact.zip`,
},
awsDeploy.options.stage,
awsDeploy.options.region
);
expect(headObjectStub).to.have.been.calledWithExactly(
'S3',
'headObject',
{
Bucket: awsDeploy.bucketName,
Key: `${s3Key}/141264711231-2016-08-18T15:42:00/cloudformation.json`,
},
awsDeploy.options.stage,
awsDeploy.options.region
);
});
});
});
describe('#checkIfDeploymentIsNecessary()', () => {
let normalizeCloudFormationTemplateStub;
let globbySyncStub;
let readFileSyncStub;
beforeEach(() => {
normalizeCloudFormationTemplateStub = sinon
.stub(normalizeFiles, 'normalizeCloudFormationTemplate')
.returns();
globbySyncStub = sinon
.stub(globby, 'sync');
readFileSyncStub = sinon
.stub(fs, 'readFileSync')
.returns();
});
afterEach(() => {
normalizeFiles.normalizeCloudFormationTemplate.restore();
globby.sync.restore();
fs.readFileSync.restore();
});
it('should resolve if no input is provided', () => expect(awsDeploy
.checkIfDeploymentIsNecessary()).to.be.fulfilled.then(() => {
expect(normalizeCloudFormationTemplateStub).to.not.have.been.called;
expect(globbySyncStub).to.not.have.been.called;
expect(readFileSyncStub).to.not.have.been.called;
expect(awsDeploy.serverless.cli.log).to.not.have.been.called;
})
);
it('should resolve if no objects are provided as input', () => {
const input = [];
return expect(awsDeploy.checkIfDeploymentIsNecessary(input))
.to.be.fulfilled.then(() => {
expect(normalizeCloudFormationTemplateStub).to.not.have.been.called;
expect(globbySyncStub).to.not.have.been.called;
expect(readFileSyncStub).to.not.have.been.called;
expect(awsDeploy.serverless.cli.log).to.not.have.been.called;
});
});
it('should not set a flag if there are more remote hashes', () => {
globbySyncStub.returns(['my-service.zip']);
cryptoStub.createHash().update().digest.onCall(0).returns('local-hash-cf-template');
cryptoStub.createHash().update().digest.onCall(1).returns('local-hash-zip-file-1');
const input = [
{ Metadata: { filesha256: 'remote-hash-cf-template' } },
{ Metadata: { filesha256: 'remote-hash-zip-file-1' } },
{ Metadata: { /* no filesha256 available */ } }, // will be translated to ''
];
return expect(awsDeploy.checkIfDeploymentIsNecessary(input))
.to.be.fulfilled.then(() => {
expect(normalizeCloudFormationTemplateStub).to.have.been.calledOnce;
expect(globbySyncStub).to.have.been.calledOnce;
expect(readFileSyncStub).to.have.been.calledOnce;
expect(awsDeploy.serverless.cli.log).to.not.have.been.called;
expect(normalizeCloudFormationTemplateStub).to.have.been.calledWithExactly(
awsDeploy.serverless.service.provider.compiledCloudFormationTemplate
);
expect(globbySyncStub).to.have.been.calledWithExactly(
['**.zip'],
{
cwd: path.join(awsDeploy.serverless.config.servicePath, '.serverless'),
dot: true,
silent: true,
}
);
expect(readFileSyncStub).to.have.been.calledWithExactly(
'my-service/.serverless/my-service.zip'
);
expect(awsDeploy.serverless.service.provider.shouldNotDeploy).to.equal(undefined);
});
});
it('should not set a flag if remote and local hashes are different', () => {
globbySyncStub.returns(['my-service.zip']);
cryptoStub.createHash().update().digest.onCall(0).returns('local-hash-cf-template');
cryptoStub.createHash().update().digest.onCall(1).returns('local-hash-zip-file-1');
const input = [
{ Metadata: { filesha256: 'remote-hash-cf-template' } },
{ Metadata: { filesha256: 'remote-hash-zip-file-1' } },
];
return expect(awsDeploy.checkIfDeploymentIsNecessary(input))
.to.be.fulfilled.then(() => {
expect(normalizeCloudFormationTemplateStub).to.have.been.calledOnce;
expect(globbySyncStub).to.have.been.calledOnce;
expect(readFileSyncStub).to.have.been.calledOnce;
expect(awsDeploy.serverless.cli.log).to.not.have.been.called;
expect(normalizeCloudFormationTemplateStub).to.have.been.calledWithExactly(
awsDeploy.serverless.service.provider.compiledCloudFormationTemplate
);
expect(globbySyncStub).to.have.been.calledWithExactly(
['**.zip'],
{
cwd: path.join(awsDeploy.serverless.config.servicePath, '.serverless'),
dot: true,
silent: true,
}
);
expect(readFileSyncStub).to.have.been.calledWithExactly(
'my-service/.serverless/my-service.zip'
);
expect(awsDeploy.serverless.service.provider.shouldNotDeploy).to.equal(undefined);
});
});
it('should not set a flag if remote and local hashes are the same but are duplicated', () => {
globbySyncStub.returns(['func1.zip', 'func2.zip']);
cryptoStub.createHash().update().digest.onCall(0).returns('remote-hash-cf-template');
// happens when package.individually is used
cryptoStub.createHash().update().digest.onCall(1).returns('remote-hash-zip-file-1');
cryptoStub.createHash().update().digest.onCall(2).returns('remote-hash-zip-file-1');
const input = [
{ Metadata: { filesha256: 'remote-hash-cf-template' } },
{ Metadata: { filesha256: 'remote-hash-zip-file-1' } },
];
return expect(awsDeploy.checkIfDeploymentIsNecessary(input))
.to.be.fulfilled.then(() => {
expect(normalizeCloudFormationTemplateStub).to.have.been.calledOnce;
expect(globbySyncStub).to.have.been.calledOnce;
expect(readFileSyncStub).to.have.been.calledTwice;
expect(awsDeploy.serverless.cli.log).to.not.have.been.called;
expect(normalizeCloudFormationTemplateStub).to.have.been.calledWithExactly(
awsDeploy.serverless.service.provider.compiledCloudFormationTemplate
);
expect(globbySyncStub).to.have.been.calledWithExactly(
['**.zip'],
{
cwd: path.join(awsDeploy.serverless.config.servicePath, '.serverless'),
dot: true,
silent: true,
}
);
expect(readFileSyncStub).to.have.been.calledWithExactly(
'my-service/.serverless/func1.zip'
);
expect(readFileSyncStub).to.have.been.calledWithExactly(
'my-service/.serverless/func2.zip'
);
expect(awsDeploy.serverless.service.provider.shouldNotDeploy).to.equal(undefined);
});
});
it('should set a flag if the remote and local hashes are equal', () => {
globbySyncStub.returns(['my-service.zip']);
cryptoStub.createHash().update().digest.onCall(0).returns('hash-cf-template');
cryptoStub.createHash().update().digest.onCall(1).returns('hash-zip-file-1');
const input = [
{ Metadata: { filesha256: 'hash-cf-template' } },
{ Metadata: { filesha256: 'hash-zip-file-1' } },
];
return expect(awsDeploy.checkIfDeploymentIsNecessary(input))
.to.be.fulfilled.then(() => {
expect(normalizeCloudFormationTemplateStub).to.have.been.calledOnce;
expect(globbySyncStub).to.have.been.calledOnce;
expect(readFileSyncStub).to.have.been.calledOnce;
expect(awsDeploy.serverless.cli.log).to.have.been.calledOnce;
expect(normalizeCloudFormationTemplateStub).to.have.been.calledWithExactly(
awsDeploy.serverless.service.provider.compiledCloudFormationTemplate
);
expect(globbySyncStub).to.have.been.calledWithExactly(
['**.zip'],
{
cwd: path.join(awsDeploy.serverless.config.servicePath, '.serverless'),
dot: true,
silent: true,
}
);
expect(readFileSyncStub).to.have.been.calledWithExactly(
'my-service/.serverless/my-service.zip'
);
expect(awsDeploy.serverless.service.provider.shouldNotDeploy).to.equal(true);
});
});
it('should set a flag if the remote and local hashes are duplicated and equal', () => {
globbySyncStub.returns(['func1.zip', 'func2.zip']);
cryptoStub.createHash().update().digest.onCall(0).returns('hash-cf-template');
// happens when package.individually is used
cryptoStub.createHash().update().digest.onCall(1).returns('hash-zip-file-1');
cryptoStub.createHash().update().digest.onCall(2).returns('hash-zip-file-1');
const input = [
{ Metadata: { filesha256: 'hash-cf-template' } },
{ Metadata: { filesha256: 'hash-zip-file-1' } },
{ Metadata: { filesha256: 'hash-zip-file-1' } },
];
return expect(awsDeploy.checkIfDeploymentIsNecessary(input))
.to.be.fulfilled.then(() => {
expect(normalizeCloudFormationTemplateStub).to.have.been.calledOnce;
expect(globbySyncStub).to.have.been.calledOnce;
expect(readFileSyncStub).to.have.been.calledTwice;
expect(awsDeploy.serverless.cli.log).to.have.been.calledOnce;
expect(normalizeCloudFormationTemplateStub).to.have.been.calledWithExactly(
awsDeploy.serverless.service.provider.compiledCloudFormationTemplate
);
expect(globbySyncStub).to.have.been.calledWithExactly(
['**.zip'],
{
cwd: path.join(awsDeploy.serverless.config.servicePath, '.serverless'),
dot: true,
silent: true,
}
);
expect(readFileSyncStub).to.have.been.calledWithExactly(
'my-service/.serverless/func1.zip'
);
expect(readFileSyncStub).to.have.been.calledWithExactly(
'my-service/.serverless/func2.zip'
);
expect(awsDeploy.serverless.service.provider.shouldNotDeploy).to.equal(true);
});
});
});
});

View File

@ -33,8 +33,16 @@ module.exports = {
this.serverless.service.package.individually) {
// artifact file validation (multiple function artifacts)
this.serverless.service.getAllFunctions().forEach(functionName => {
const artifactFileName = this.provider.naming.getFunctionArtifactName(functionName);
const artifactFilePath = path.join(this.packagePath, artifactFileName);
let artifactFileName = this.provider.naming.getFunctionArtifactName(functionName);
let artifactFilePath = path.join(this.packagePath, artifactFileName);
// check if an artifact is used in function package level
const functionObject = this.serverless.service.getFunction(functionName);
if (_.has(functionObject, ['package', 'artifact'])) {
artifactFilePath = functionObject.package.artifact;
artifactFileName = path.basename(artifactFilePath);
}
if (!this.serverless.utils.fileExistsSync(artifactFilePath)) {
throw new this.serverless.classes
.Error(`No ${artifactFileName} file found in the package path you provided.`);
@ -42,8 +50,14 @@ module.exports = {
});
} else if (!_.isEmpty(this.serverless.service.functions)) {
// artifact file validation (single service artifact)
const artifactFileName = this.provider.naming.getServiceArtifactName();
const artifactFilePath = path.join(this.packagePath, artifactFileName);
let artifactFilePath;
let artifactFileName;
if (this.serverless.service.package.artifact) {
artifactFileName = artifactFilePath = this.serverless.service.package.artifact;
} else {
artifactFileName = this.provider.naming.getServiceArtifactName();
artifactFilePath = path.join(this.packagePath, artifactFileName);
}
if (!this.serverless.utils.fileExistsSync(artifactFilePath)) {
throw new this.serverless.classes
.Error(`No ${artifactFileName} file found in the package path you provided.`);

View File

@ -46,57 +46,106 @@ describe('extendedValidate', () => {
});
describe('extendedValidate()', () => {
it('should throw error if state file does not exist', () => {
sinon.stub(awsDeploy.serverless.utils, 'fileExistsSync').returns(false);
expect(() => awsDeploy.extendedValidate()).to.throw(Error);
let fileExistsSyncStub;
let readFileSyncStub;
beforeEach(() => {
fileExistsSyncStub = sinon
.stub(awsDeploy.serverless.utils, 'fileExistsSync');
readFileSyncStub = sinon
.stub(awsDeploy.serverless.utils, 'readFileSync');
awsDeploy.serverless.service.package.individually = false;
});
afterEach(() => {
awsDeploy.serverless.utils.fileExistsSync.restore();
awsDeploy.serverless.utils.readFileSync.restore();
});
it('should throw error if state file does not exist', () => {
fileExistsSyncStub.returns(false);
expect(() => awsDeploy.extendedValidate()).to.throw(Error);
});
it('should throw error if packaged individually but functions packages do not exist', () => {
const fileExistsSyncStub = sinon.stub(awsDeploy.serverless.utils, 'fileExistsSync');
fileExistsSyncStub.onCall(0).returns(true);
fileExistsSyncStub.onCall(1).returns(false);
sinon.stub(awsDeploy.serverless.utils, 'readFileSync').returns(stateFileMock);
readFileSyncStub.returns(stateFileMock);
awsDeploy.serverless.service.package.individually = true;
expect(() => awsDeploy.extendedValidate()).to.throw(Error);
awsDeploy.serverless.service.package.individually = false;
awsDeploy.serverless.utils.fileExistsSync.restore();
awsDeploy.serverless.utils.readFileSync.restore();
});
it('should throw error if service package does not exist', () => {
const fileExistsSyncStub = sinon.stub(awsDeploy.serverless.utils, 'fileExistsSync');
fileExistsSyncStub.onCall(0).returns(true);
fileExistsSyncStub.onCall(1).returns(false);
sinon.stub(awsDeploy.serverless.utils, 'readFileSync').returns(stateFileMock);
readFileSyncStub.returns(stateFileMock);
expect(() => awsDeploy.extendedValidate()).to.throw(Error);
awsDeploy.serverless.utils.fileExistsSync.restore();
awsDeploy.serverless.utils.readFileSync.restore();
});
it('should not throw error if service has no functions and no service package available', () => { // eslint-disable-line max-len
const functionsTmp = stateFileMock.service.functions;
it('should not throw error if service has no functions and no service package', () => {
stateFileMock.service.functions = {};
sinon.stub(awsDeploy.serverless.utils, 'fileExistsSync').returns(true);
sinon.stub(awsDeploy.serverless.utils, 'readFileSync').returns(stateFileMock);
fileExistsSyncStub.returns(true);
readFileSyncStub.returns(stateFileMock);
return awsDeploy.extendedValidate().then(() => {
stateFileMock.service.functions = functionsTmp;
awsDeploy.serverless.utils.fileExistsSync.restore();
awsDeploy.serverless.utils.readFileSync.restore();
expect(fileExistsSyncStub.calledOnce).to.equal(true);
expect(readFileSyncStub.calledOnce).to.equal(true);
});
});
it('should not throw error if service has no functions and no function packages available', () => { // eslint-disable-line max-len
const functionsTmp = stateFileMock.service.functions;
it('should not throw error if service has no functions and no function packages', () => {
stateFileMock.service.functions = {};
awsDeploy.serverless.service.package.individually = true;
sinon.stub(awsDeploy.serverless.utils, 'fileExistsSync').returns(true);
sinon.stub(awsDeploy.serverless.utils, 'readFileSync').returns(stateFileMock);
fileExistsSyncStub.returns(true);
readFileSyncStub.returns(stateFileMock);
return awsDeploy.extendedValidate().then(() => {
awsDeploy.serverless.service.package.individually = false;
stateFileMock.service.functions = functionsTmp;
awsDeploy.serverless.utils.fileExistsSync.restore();
awsDeploy.serverless.utils.readFileSync.restore();
expect(fileExistsSyncStub.calledOnce).to.equal(true);
expect(readFileSyncStub.calledOnce).to.equal(true);
});
});
it('should use function package level artifact when provided', () => {
stateFileMock.service.functions = {
first: {
package: {
artifact: 'artifact.zip',
},
},
};
awsDeploy.serverless.service.package.individually = true;
fileExistsSyncStub.returns(true);
readFileSyncStub.returns(stateFileMock);
return awsDeploy.extendedValidate().then(() => {
expect(fileExistsSyncStub.calledTwice).to.equal(true);
expect(readFileSyncStub.calledOnce).to.equal(true);
expect(fileExistsSyncStub).to.have.been.calledWithExactly('artifact.zip');
});
});
it('should throw error if specified package artifact does not exist', () => {
// const fileExistsSyncStub = sinon.stub(awsDeploy.serverless.utils, 'fileExistsSync');
fileExistsSyncStub.onCall(0).returns(true);
fileExistsSyncStub.onCall(1).returns(false);
readFileSyncStub.returns(stateFileMock);
awsDeploy.serverless.service.package.artifact = 'some/file.zip';
expect(() => awsDeploy.extendedValidate()).to.throw(Error);
delete awsDeploy.serverless.service.package.artifact;
});
it('should not throw error if specified package artifact exists', () => {
// const fileExistsSyncStub = sinon.stub(awsDeploy.serverless.utils, 'fileExistsSync');
fileExistsSyncStub.onCall(0).returns(true);
fileExistsSyncStub.onCall(1).returns(true);
readFileSyncStub.returns(stateFileMock);
awsDeploy.serverless.service.package.artifact = 'some/file.zip';
return awsDeploy.extendedValidate().then(() => {
delete awsDeploy.serverless.service.package.artifact;
});
});
});

View File

@ -3,22 +3,39 @@
/* eslint-disable no-use-before-define */
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const BbPromise = require('bluebird');
const filesize = require('filesize');
const path = require('path');
const normalizeFiles = require('../../lib/normalizeFiles');
module.exports = {
uploadArtifacts() {
return BbPromise.bind(this)
.then(this.uploadCloudFormationFile)
.then(this.uploadFunctions);
},
uploadCloudFormationFile() {
this.serverless.cli.log('Uploading CloudFormation file to S3...');
const compiledTemplateFileName = 'compiled-cloudformation-template.json';
const body = JSON.stringify(this.serverless.service.provider.compiledCloudFormationTemplate);
const compiledCfTemplate = this.serverless.service.provider.compiledCloudFormationTemplate;
const normCfTemplate = normalizeFiles.normalizeCloudFormationTemplate(compiledCfTemplate);
const fileHash = crypto
.createHash('sha256')
.update(JSON.stringify(normCfTemplate))
.digest('base64');
let params = {
Bucket: this.bucketName,
Key: `${this.serverless.service.package.artifactDirectoryName}/${compiledTemplateFileName}`,
Body: body,
Body: JSON.stringify(compiledCfTemplate),
ContentType: 'application/json',
Metadata: {
filesha256: fileHash,
},
};
const deploymentBucketObject = this.serverless.service.provider.deploymentBucketObject;
@ -36,11 +53,18 @@ module.exports = {
uploadZipFile(artifactFilePath) {
const fileName = artifactFilePath.split(path.sep).pop();
// TODO refactor to be async (use util function to compute checksum async)
const data = fs.readFileSync(artifactFilePath);
const fileHash = crypto.createHash('sha256').update(data).digest('base64');
let params = {
Bucket: this.bucketName,
Key: `${this.serverless.service.package.artifactDirectoryName}/${fileName}`,
Body: fs.createReadStream(artifactFilePath),
ContentType: 'application/zip',
Metadata: {
filesha256: fileHash,
},
};
const deploymentBucketObject = this.serverless.service.provider.deploymentBucketObject;
@ -89,12 +113,6 @@ module.exports = {
return BbPromise.resolve();
});
},
uploadArtifacts() {
return BbPromise.bind(this)
.then(this.uploadCloudFormationFile)
.then(this.uploadFunctions);
},
};
function setServersideEncryptionOptions(putParams, deploymentBucketOptions) {

View File

@ -1,17 +1,20 @@
'use strict';
const sinon = require('sinon');
const fs = require('fs');
const path = require('path');
const expect = require('chai').expect;
const proxyquire = require('proxyquire');
const normalizeFiles = require('../../lib/normalizeFiles');
const AwsProvider = require('../../provider/awsProvider');
const AwsDeploy = require('../index');
const Serverless = require('../../../../Serverless');
const testUtils = require('../../../../../tests/utils');
const fs = require('fs');
describe('uploadArtifacts', () => {
let serverless;
let awsDeploy;
let cryptoStub;
beforeEach(() => {
serverless = new Serverless();
@ -29,19 +32,66 @@ describe('uploadArtifacts', () => {
handler: 'foo',
},
};
awsDeploy.serverless.service.provider.compiledCloudFormationTemplate = {
foo: 'bar',
};
awsDeploy.serverless.cli = new serverless.classes.CLI();
cryptoStub = {
createHash: function () { return this; }, // eslint-disable-line
update: function () { return this; }, // eslint-disable-line
digest: sinon.stub(),
};
const uploadArtifacts = proxyquire('./uploadArtifacts.js', {
crypto: cryptoStub,
});
Object.assign(
awsDeploy,
uploadArtifacts
);
});
describe('#uploadArtifacts()', () => {
it('should run promise chain in order', () => {
const uploadCloudFormationFileStub = sinon
.stub(awsDeploy, 'uploadCloudFormationFile').resolves();
const uploadFunctionsStub = sinon
.stub(awsDeploy, 'uploadFunctions').resolves();
return awsDeploy.uploadArtifacts().then(() => {
expect(uploadCloudFormationFileStub.calledOnce)
.to.be.equal(true);
expect(uploadFunctionsStub.calledAfter(uploadCloudFormationFileStub)).to.be.equal(true);
awsDeploy.uploadCloudFormationFile.restore();
awsDeploy.uploadFunctions.restore();
});
});
});
describe('#uploadCloudFormationFile()', () => {
it('should upload the CloudFormation file to the S3 bucket', () => {
awsDeploy.serverless.service.provider.compiledCloudFormationTemplate = { key: 'value' };
let normalizeCloudFormationTemplateStub;
let putObjectStub;
const putObjectStub = sinon
.stub(awsDeploy.provider, 'request').resolves();
beforeEach(() => {
normalizeCloudFormationTemplateStub = sinon
.stub(normalizeFiles, 'normalizeCloudFormationTemplate')
.returns();
putObjectStub = sinon
.stub(awsDeploy.provider, 'request')
.resolves();
});
afterEach(() => {
normalizeFiles.normalizeCloudFormationTemplate.restore();
awsDeploy.provider.request.restore();
});
it('should upload the CloudFormation file to the S3 bucket', () => {
cryptoStub.createHash().update().digest.onCall(0).returns('local-hash-cf-template');
return awsDeploy.uploadCloudFormationFile().then(() => {
expect(putObjectStub.calledOnce).to.be.equal(true);
expect(normalizeCloudFormationTemplateStub.calledOnce).to.equal(true);
expect(putObjectStub.calledOnce).to.equal(true);
expect(putObjectStub.calledWithExactly(
'S3',
'putObject',
@ -49,27 +99,28 @@ describe('uploadArtifacts', () => {
Bucket: awsDeploy.bucketName,
Key: `${awsDeploy.serverless.service.package
.artifactDirectoryName}/compiled-cloudformation-template.json`,
Body: JSON.stringify(awsDeploy.serverless.service.provider
.compiledCloudFormationTemplate),
Body: JSON.stringify({ foo: 'bar' }),
ContentType: 'application/json',
Metadata: {
filesha256: 'local-hash-cf-template',
},
},
awsDeploy.options.stage,
awsDeploy.options.region
)).to.be.equal(true);
awsDeploy.provider.request.restore();
expect(normalizeCloudFormationTemplateStub.calledWithExactly({ foo: 'bar' }))
.to.equal(true);
});
});
it('should upload to a bucket with server side encryption bucket policy', () => {
awsDeploy.serverless.service.provider.compiledCloudFormationTemplate = { key: 'value' };
it('should upload the CloudFormation file to a bucket with SSE bucket policy', () => {
cryptoStub.createHash().update().digest.onCall(0).returns('local-hash-cf-template');
awsDeploy.serverless.service.provider.deploymentBucketObject = {
serverSideEncryption: 'AES256',
};
const putObjectStub = sinon
.stub(awsDeploy.provider, 'request').resolves();
return awsDeploy.uploadCloudFormationFile().then(() => {
expect(normalizeCloudFormationTemplateStub.calledOnce).to.equal(true);
expect(putObjectStub.calledOnce).to.be.equal(true);
expect(putObjectStub.calledWithExactly(
'S3',
@ -78,36 +129,54 @@ describe('uploadArtifacts', () => {
Bucket: awsDeploy.bucketName,
Key: `${awsDeploy.serverless.service.package
.artifactDirectoryName}/compiled-cloudformation-template.json`,
Body: JSON.stringify(awsDeploy.serverless.service.provider
.compiledCloudFormationTemplate),
Body: JSON.stringify({ foo: 'bar' }),
ContentType: 'application/json',
ServerSideEncryption: 'AES256',
Metadata: {
filesha256: 'local-hash-cf-template',
},
},
awsDeploy.options.stage,
awsDeploy.options.region
)).to.be.equal(true);
awsDeploy.provider.request.restore();
expect(normalizeCloudFormationTemplateStub.calledWithExactly({ foo: 'bar' }))
.to.equal(true);
});
});
});
describe('#uploadZipFile()', () => {
let readFileSyncStub;
let putObjectStub;
beforeEach(() => {
readFileSyncStub = sinon
.stub(fs, 'readFileSync')
.returns();
putObjectStub = sinon
.stub(awsDeploy.provider, 'request')
.resolves();
});
afterEach(() => {
fs.readFileSync.restore();
awsDeploy.provider.request.restore();
});
it('should throw for null artifact paths', () => {
sinon.stub(awsDeploy.provider, 'request').resolves();
expect(() => awsDeploy.uploadZipFile(null)).to.throw(Error);
});
it('should upload the .zip file to the S3 bucket', () => {
cryptoStub.createHash().update().digest.onCall(0).returns('local-hash-zip-file');
const tmpDirPath = testUtils.getTmpDirPath();
const artifactFilePath = path.join(tmpDirPath, 'artifact.zip');
serverless.utils.writeFileSync(artifactFilePath, 'artifact.zip file content');
const putObjectStub = sinon
.stub(awsDeploy.provider, 'request').resolves();
return awsDeploy.uploadZipFile(artifactFilePath).then(() => {
expect(putObjectStub.calledOnce).to.be.equal(true);
expect(readFileSyncStub.calledOnce).to.equal(true);
expect(putObjectStub.calledWithExactly(
'S3',
'putObject',
@ -116,26 +185,30 @@ describe('uploadArtifacts', () => {
Key: `${awsDeploy.serverless.service.package.artifactDirectoryName}/artifact.zip`,
Body: sinon.match.object.and(sinon.match.has('path', artifactFilePath)),
ContentType: 'application/zip',
Metadata: {
filesha256: 'local-hash-zip-file',
},
},
awsDeploy.options.stage,
awsDeploy.options.region
)).to.be.equal(true);
awsDeploy.provider.request.restore();
expect(readFileSyncStub.calledWithExactly(artifactFilePath)).to.equal(true);
});
});
it('should upload to a bucket with server side encryption bucket policy', () => {
it('should upload the .zip file to a bucket with SSE bucket policy', () => {
cryptoStub.createHash().update().digest.onCall(0).returns('local-hash-zip-file');
const tmpDirPath = testUtils.getTmpDirPath();
const artifactFilePath = path.join(tmpDirPath, 'artifact.zip');
serverless.utils.writeFileSync(artifactFilePath, 'artifact.zip file content');
awsDeploy.serverless.service.provider.deploymentBucketObject = {
serverSideEncryption: 'AES256',
};
const putObjectStub = sinon
.stub(awsDeploy.provider, 'request').resolves();
return awsDeploy.uploadZipFile(artifactFilePath).then(() => {
expect(putObjectStub.calledOnce).to.be.equal(true);
expect(readFileSyncStub.calledOnce).to.equal(true);
expect(putObjectStub.calledWithExactly(
'S3',
'putObject',
@ -145,11 +218,14 @@ describe('uploadArtifacts', () => {
Body: sinon.match.object.and(sinon.match.has('path', artifactFilePath)),
ContentType: 'application/zip',
ServerSideEncryption: 'AES256',
Metadata: {
filesha256: 'local-hash-zip-file',
},
},
awsDeploy.options.stage,
awsDeploy.options.region
)).to.be.equal(true);
awsDeploy.provider.request.restore();
expect(readFileSyncStub.calledWithExactly(artifactFilePath)).to.equal(true);
});
});
});
@ -246,22 +322,4 @@ describe('uploadArtifacts', () => {
});
});
});
describe('#uploadArtifacts()', () => {
it('should run promise chain in order', () => {
const uploadCloudFormationFileStub = sinon
.stub(awsDeploy, 'uploadCloudFormationFile').resolves();
const uploadFunctionsStub = sinon
.stub(awsDeploy, 'uploadFunctions').resolves();
return awsDeploy.uploadArtifacts().then(() => {
expect(uploadCloudFormationFileStub.calledOnce)
.to.be.equal(true);
expect(uploadFunctionsStub.calledAfter(uploadCloudFormationFileStub)).to.be.equal(true);
awsDeploy.uploadCloudFormationFile.restore();
awsDeploy.uploadFunctions.restore();
});
});
});
});

View File

@ -1,6 +1,8 @@
'use strict';
const BbPromise = require('bluebird');
const _ = require('lodash');
const crypto = require('crypto');
const path = require('path');
const fs = require('fs');
const validate = require('../lib/validate');
@ -15,12 +17,14 @@ class AwsDeployFunction {
path.join(this.serverless.config.servicePath || '.', '.serverless');
this.provider = this.serverless.getProvider('aws');
// used to store data received via AWS SDK
this.serverless.service.provider.remoteFunctionData = null;
Object.assign(this, validate);
this.hooks = {
'deploy:function:initialize': () => BbPromise.bind(this)
.then(this.validate)
.then(this.logStatus)
.then(this.checkIfFunctionExists),
'deploy:function:packageFunction': () => this.serverless.pluginManager
@ -32,11 +36,6 @@ class AwsDeployFunction {
};
}
logStatus() {
this.serverless.cli.log(`Deploying function: ${this.options.function}...`);
return BbPromise.resolve();
}
checkIfFunctionExists() {
// check if the function exists in the service
this.options.functionObj = this.serverless.service.getFunction(this.options.function);
@ -46,43 +45,59 @@ class AwsDeployFunction {
FunctionName: this.options.functionObj.name,
};
this.provider.request(
return this.provider.request(
'Lambda',
'getFunction',
params,
this.options.stage, this.options.region
).catch(() => {
)
.then((result) => {
this.serverless.service.provider.remoteFunctionData = result;
return result;
})
.catch(() => {
const errorMessage = [
`The function "${this.options.function}" you want to update is not yet deployed.`,
' Please run "serverless deploy" to deploy your service.',
' After that you can redeploy your services functions with the',
' "serverless deploy function" command.',
].join('');
throw new this.serverless.classes
.Error(errorMessage);
throw new this.serverless.classes.Error(errorMessage);
});
return BbPromise.resolve();
}
deployFunction() {
const artifactFileName = this.provider.naming
.getFunctionArtifactName(this.options.function);
const artifactFilePath = path.join(this.packagePath, artifactFileName);
let artifactFilePath = this.serverless.service.package.artifact ||
path.join(this.packagePath, artifactFileName);
// check if an artifact is used in function package level
const functionObject = this.serverless.service.getFunction(this.options.function);
if (_.has(functionObject, ['package', 'artifact'])) {
artifactFilePath = functionObject.package.artifact;
}
const data = fs.readFileSync(artifactFilePath);
const remoteHash = this.serverless.service.provider.remoteFunctionData.Configuration.CodeSha256;
const localHash = crypto.createHash('sha256').update(data).digest('base64');
if (remoteHash === localHash && !this.options.force) {
this.serverless.cli.log('Code not changed. Skipping function deployment.');
return BbPromise.resolve();
}
const params = {
FunctionName: this.options.functionObj.name,
ZipFile: data,
};
// Get function stats
const stats = fs.statSync(artifactFilePath);
this.serverless.cli.log(
`Uploading function: ${this.options.function} (${filesize(stats.size)})...`
);
// Perform upload
return this.provider.request(
'Lambda',
'updateFunctionCode',

View File

@ -4,14 +4,16 @@ const expect = require('chai').expect;
const sinon = require('sinon');
const path = require('path');
const fs = require('fs');
const proxyquire = require('proxyquire');
const AwsProvider = require('../provider/awsProvider');
const AwsDeployFunction = require('./index');
const Serverless = require('../../../Serverless');
const testUtils = require('../../../../tests/utils');
describe('AwsDeployFunction', () => {
let AwsDeployFunction;
let serverless;
let awsDeployFunction;
let cryptoStub;
beforeEach(() => {
serverless = new Serverless();
@ -44,6 +46,14 @@ describe('AwsDeployFunction', () => {
};
serverless.init();
serverless.setProvider('aws', new AwsProvider(serverless));
cryptoStub = {
createHash: function () { return this; }, // eslint-disable-line
update: function () { return this; }, // eslint-disable-line
digest: sinon.stub(),
};
AwsDeployFunction = proxyquire('./index.js', {
crypto: cryptoStub,
});
awsDeployFunction = new AwsDeployFunction(serverless, options);
});
@ -61,15 +71,24 @@ describe('AwsDeployFunction', () => {
});
describe('#checkIfFunctionExists()', () => {
let getFunctionStub;
beforeEach(() => {
getFunctionStub = sinon
.stub(awsDeployFunction.provider, 'request')
.resolves({ func: { name: 'first' } });
});
afterEach(() => {
awsDeployFunction.provider.request.restore();
});
it('it should throw error if function is not provided', () => {
serverless.service.functions = null;
expect(() => awsDeployFunction.checkIfFunctionExists()).to.throw(Error);
});
it('should check if the function is deployed', () => {
const getFunctionStub = sinon
.stub(awsDeployFunction.provider, 'request').resolves();
it('should check if the function is deployed and save the result', () => {
awsDeployFunction.serverless.service.functions = {
first: {
name: 'first',
@ -88,30 +107,57 @@ describe('AwsDeployFunction', () => {
awsDeployFunction.options.stage,
awsDeployFunction.options.region
)).to.be.equal(true);
awsDeployFunction.provider.request.restore();
expect(awsDeployFunction.serverless.service.provider.remoteFunctionData).to.deep.equal({
func: {
name: 'first',
},
});
});
});
});
describe('#deployFunction()', () => {
let artifactFilePath;
let updateFunctionCodeStub;
let statSyncStub;
let readFileSyncStub;
beforeEach(() => {
// write a file to disc to simulate that the deployment artifact exists
awsDeployFunction.packagePath = testUtils.getTmpDirPath();
artifactFilePath = path.join(awsDeployFunction.packagePath, 'first.zip');
serverless.utils.writeFileSync(artifactFilePath, 'first.zip file content');
updateFunctionCodeStub = sinon
.stub(awsDeployFunction.provider, 'request')
.resolves();
statSyncStub = sinon
.stub(fs, 'statSync')
.returns({ size: 1024 });
sinon.spy(awsDeployFunction.serverless.cli, 'log');
readFileSyncStub = sinon
.stub(fs, 'readFileSync')
.returns();
awsDeployFunction.serverless.service.provider.remoteFunctionData = {
Configuration: {
CodeSha256: 'remote-hash-zip-file',
},
};
});
it('should deploy the function', () => {
// deploy the function artifact not the service artifact
const updateFunctionCodeStub = sinon
.stub(awsDeployFunction.provider, 'request').resolves();
afterEach(() => {
awsDeployFunction.provider.request.restore();
fs.statSync.restore();
fs.readFileSync.restore();
});
it('should deploy the function if the hashes are different', () => {
cryptoStub.createHash().update().digest.onCall(0).returns('local-hash-zip-file');
return awsDeployFunction.deployFunction().then(() => {
const data = fs.readFileSync(artifactFilePath);
expect(updateFunctionCodeStub.calledOnce).to.be.equal(true);
expect(readFileSyncStub.called).to.equal(true);
expect(updateFunctionCodeStub.calledWithExactly(
'Lambda',
'updateFunctionCode',
@ -122,22 +168,95 @@ describe('AwsDeployFunction', () => {
awsDeployFunction.options.stage,
awsDeployFunction.options.region
)).to.be.equal(true);
awsDeployFunction.provider.request.restore();
expect(readFileSyncStub.calledWithExactly(artifactFilePath)).to.equal(true);
});
});
it('should deploy the function if the hashes are same but the "force" option is used', () => {
awsDeployFunction.options.force = true;
cryptoStub.createHash().update().digest.onCall(0).returns('remote-hash-zip-file');
return awsDeployFunction.deployFunction().then(() => {
const data = fs.readFileSync(artifactFilePath);
expect(updateFunctionCodeStub.calledOnce).to.be.equal(true);
expect(readFileSyncStub.called).to.equal(true);
expect(updateFunctionCodeStub.calledWithExactly(
'Lambda',
'updateFunctionCode',
{
FunctionName: 'first',
ZipFile: data,
},
awsDeployFunction.options.stage,
awsDeployFunction.options.region
)).to.be.equal(true);
expect(readFileSyncStub.calledWithExactly(artifactFilePath)).to.equal(true);
});
});
it('should resolve if the hashes are the same', () => {
cryptoStub.createHash().update().digest.onCall(0).returns('remote-hash-zip-file');
return awsDeployFunction.deployFunction().then(() => {
const expected = 'Code not changed. Skipping function deployment.';
expect(updateFunctionCodeStub.calledOnce).to.be.equal(false);
expect(readFileSyncStub.calledOnce).to.equal(true);
expect(awsDeployFunction.serverless.cli.log.calledWithExactly(expected)).to.equal(true);
expect(readFileSyncStub.calledWithExactly(artifactFilePath)).to.equal(true);
});
});
it('should log artifact size', () => {
sinon.stub(fs, 'statSync').returns({ size: 1024 });
sinon.stub(awsDeployFunction.provider, 'request').resolves();
sinon.spy(awsDeployFunction.serverless.cli, 'log');
// awnY7Oi280gp5kTCloXzsqJCO4J766x6hATWqQsN/uM= <-- hash of the local zip file
readFileSyncStub.returns(new Buffer('my-service.zip content'));
return awsDeployFunction.deployFunction().then(() => {
const expected = 'Uploading function: first (1 KB)...';
expect(awsDeployFunction.serverless.cli.log.calledWithExactly(expected)).to.be.equal(true);
awsDeployFunction.provider.request.restore();
fs.statSync.restore();
expect(readFileSyncStub.calledOnce).to.equal(true);
expect(statSyncStub.calledOnce).to.equal(true);
expect(awsDeployFunction.serverless.cli.log.calledWithExactly(expected)).to.be.equal(true);
expect(readFileSyncStub.calledWithExactly(artifactFilePath)).to.equal(true);
});
});
describe('when artifact is provided', () => {
let getFunctionStub;
const artifactZipFile = 'artifact.zip';
beforeEach(() => {
getFunctionStub = sinon.stub(serverless.service, 'getFunction').returns({
handler: true,
package: {
artifact: artifactZipFile,
},
});
});
afterEach(() => {
serverless.service.getFunction.restore();
});
it('should read the provided artifact', () => awsDeployFunction.deployFunction().then(() => {
const data = fs.readFileSync(artifactZipFile);
expect(readFileSyncStub).to.have.been.calledWithExactly(artifactZipFile);
expect(statSyncStub).to.have.been.calledWithExactly(artifactZipFile);
expect(getFunctionStub).to.have.been.calledWithExactly('first');
expect(updateFunctionCodeStub.calledOnce).to.equal(true);
expect(updateFunctionCodeStub.calledWithExactly(
'Lambda',
'updateFunctionCode',
{
FunctionName: 'first',
ZipFile: data,
},
awsDeployFunction.options.stage,
awsDeployFunction.options.region
)).to.be.equal(true);
}));
});
});
});

View File

@ -0,0 +1,19 @@
'use strict';
const _ = require('lodash');
module.exports = {
normalizeCloudFormationTemplate(template) {
const normalizedTemplate = _.cloneDeep(template);
// reset all the S3Keys for AWS::Lambda::Function resources
_.forEach(normalizedTemplate.Resources, (value) => {
if (value.Type && value.Type === 'AWS::Lambda::Function') {
const newVal = value;
newVal.Properties.Code.S3Key = '';
}
});
return normalizedTemplate;
},
};

View File

@ -0,0 +1,58 @@
'use strict';
const expect = require('chai').expect;
const normalizeFiles = require('./normalizeFiles');
describe('normalizeFiles', () => {
describe('#normalizeCloudFormationTemplate()', () => {
it('should reset the S3 code keys for Lambda functions', () => {
const input = {
Resources: {
MyLambdaFunction: {
Type: 'AWS::Lambda::Function',
Properties: {
Code: {
S3Key: 'some-s3-key-for-the-code',
},
},
},
},
};
const result = normalizeFiles.normalizeCloudFormationTemplate(input);
expect(result).to.deep.equal({
Resources: {
MyLambdaFunction: {
Type: 'AWS::Lambda::Function',
Properties: {
Code: {
S3Key: '',
},
},
},
},
});
});
it('should keep other resources untouched', () => {
const input = {
Resources: {
MyOtherResource: {
Type: 'AWS::XXX::XXX',
},
},
};
const result = normalizeFiles.normalizeCloudFormationTemplate(input);
expect(result).to.deep.equal({
Resources: {
MyOtherResource: {
Type: 'AWS::XXX::XXX',
},
},
});
});
});
});

View File

@ -146,11 +146,22 @@ class AwsProvider {
f()
.then(resolve)
.catch((e) => {
if (e.statusCode === 429) {
const err = e;
if (err.statusCode === 429) {
that.serverless.cli.log("'Too many requests' received, sleeping 5 seconds");
setTimeout(doCall, 5000);
} else {
reject(e);
if (err.message.match(/(.+security token.+is )+(invalid)*|(expired)*/)) {
const errorMessage = [
'The AWS security token included in the request is invalid / expired.\n',
' Please re-authenticate if you\'re using MFA.\n',
' Or check your ~./aws/credentials file and verify your keys are correct.\n',
' More information on setting up credentials',
` can be found here: ${chalk.green('https://git.io/vXsdd')}.`,
].join('');
err.message = errorMessage;
}
reject(new this.serverless.classes.Error(err.message, err.statusCode));
}
});
};

View File

@ -3,6 +3,7 @@
const expect = require('chai').expect;
const proxyquire = require('proxyquire');
const sinon = require('sinon');
const chalk = require('chalk');
const AwsProvider = require('./awsProvider');
const Serverless = require('../../../Serverless');
@ -214,6 +215,43 @@ describe('AwsProvider', () => {
.catch(() => done());
});
it('should ask to re-authenticate MFA or check if AWS credentials are valid', (done) => {
const error = {
statusCode: 403,
message: 'The security token included in the request is invalid',
};
class FakeS3 {
constructor(credentials) {
this.credentials = credentials;
}
error() {
return {
send(cb) {
cb(error);
},
};
}
}
awsProvider.sdk = {
S3: FakeS3,
};
awsProvider.request('S3', 'error', {})
.then(() => done('Should not succeed'))
.catch((err) => {
const errorMessage = [
'The AWS security token included in the request is invalid / expired.\n',
' Please re-authenticate if you\'re using MFA.\n',
' Or check your ~./aws/credentials file and verify your keys are correct.\n',
' More information on setting up credentials',
` can be found here: ${chalk.green('https://git.io/vXsdd')}.`,
].join('');
expect(err.message).to.equal(errorMessage);
done();
})
.catch(done);
});
it('should return ref to docs for missing credentials', (done) => {
const error = {
statusCode: 403,

View File

@ -1,8 +1,13 @@
#!/bin/bash
isMacOs=`uname -a | grep Darwin`
#install zip
apt-get -qq update
apt-get -qq -y install zip
if [ -z "$isMacOs" ]
then
apt-get -qq update
apt-get -qq -y install zip
fi
dotnet restore

View File

@ -44,6 +44,9 @@ class Deploy {
usage: 'Show all stack events during deployment',
shortcut: 'v',
},
force: {
usage: 'Forces a deployment to take place',
},
},
commands: {
function: {
@ -67,6 +70,9 @@ class Deploy {
usage: 'Region of the function',
shortcut: 'r',
},
force: {
usage: 'Forces a deployment to take place',
},
},
},
list: {

View File

@ -1,6 +1,7 @@
'use strict';
const BbPromise = require('bluebird');
const path = require('path');
const _ = require('lodash');
module.exports = {
@ -35,7 +36,10 @@ module.exports = {
const functionObject = this.serverless.service.getFunction(functionName);
functionObject.package = functionObject.package || {};
if (functionObject.package.disable) {
this.serverless.cli.log('Packaging disabled for function: ' + functionName);
this.serverless.cli.log(`Packaging disabled for function: "${functionName}"`);
return BbPromise.resolve();
}
if (functionObject.package.artifact) {
return BbPromise.resolve();
}
if (functionObject.package.individually || this.serverless.service
@ -47,7 +51,7 @@ module.exports = {
});
return BbPromise.all(packagePromises).then(() => {
if (shouldPackageService) {
if (shouldPackageService && !this.serverless.service.package.artifact) {
return this.packageAll();
}
return BbPromise.resolve();
@ -74,12 +78,29 @@ module.exports = {
const functionObject = this.serverless.service.getFunction(functionName);
const funcPackageConfig = functionObject.package || {};
// use the artifact in function config if provided
if (funcPackageConfig.artifact) {
const filePath = path.join(this.serverless.config.servicePath, funcPackageConfig.artifact);
functionObject.package.artifact = filePath;
return BbPromise.resolve(filePath);
}
// use the artifact in service config if provided
if (this.serverless.service.package.artifact) {
const filePath = path.join(this.serverless.config.servicePath,
this.serverless.service.package.artifact);
funcPackageConfig.artifact = filePath;
return BbPromise.resolve(filePath);
}
const exclude = this.getExcludes(funcPackageConfig.exclude);
const include = this.getIncludes(funcPackageConfig.include);
const zipFileName = `${functionName}.zip`;
return this.zipService(exclude, include, zipFileName).then(artifactPath => {
functionObject.artifact = artifactPath;
functionObject.package = {
artifact: artifactPath,
};
return artifactPath;
});
},

View File

@ -161,13 +161,24 @@ describe('#packageService()', () => {
));
});
it('should not package service with only disabled functions', () => {
it('should not package functions if package artifact specified', () => {
serverless.service.package.artifact = 'some/file.zip';
const packageAllStub = sinon.stub(packagePlugin, 'packageAll').resolves();
return expect(packagePlugin.packageService()).to.be.fulfilled
.then(() => expect(packageAllStub).to.not.be.called);
});
it('should package functions individually if package artifact specified', () => {
serverless.service.package.artifact = 'some/file.zip';
serverless.service.package.individually = true;
serverless.service.functions = {
'test-one': {
name: 'test-one',
package: {
disable: true,
},
},
'test-two': {
name: 'test-two',
},
};
@ -178,10 +189,36 @@ describe('#packageService()', () => {
return expect(packagePlugin.packageService()).to.be.fulfilled
.then(() => BbPromise.join(
expect(packageFunctionStub).to.not.be.calledOnce,
expect(packageAllStub).to.not.be.calledOnce
expect(packageFunctionStub).to.be.calledTwice,
expect(packageAllStub).to.not.be.called
));
});
it('should package single functions individually if package artifact specified', () => {
serverless.service.package.artifact = 'some/file.zip';
serverless.service.functions = {
'test-one': {
name: 'test-one',
package: {
individually: true,
},
},
'test-two': {
name: 'test-two',
},
};
const packageFunctionStub = sinon
.stub(packagePlugin, 'packageFunction').resolves((func) => func.name);
const packageAllStub = sinon
.stub(packagePlugin, 'packageAll').resolves((func) => func.name);
return expect(packagePlugin.packageService()).to.be.fulfilled
.then(() => BbPromise.join(
expect(packageFunctionStub).to.be.calledOnce,
expect(packageAllStub).to.not.be.called
));
});
});
describe('#packageAll()', () => {
@ -273,5 +310,49 @@ describe('#packageService()', () => {
),
]));
});
it('should return function artifact file path', () => {
const servicePath = 'test';
const funcName = 'test-func';
serverless.config.servicePath = servicePath;
serverless.service.functions = {};
serverless.service.functions[funcName] = {
name: `test-proj-${funcName}`,
package: {
artifact: 'artifact.zip',
},
};
return expect(packagePlugin.packageFunction(funcName)).to.eventually
.equal('test/artifact.zip')
.then(() => BbPromise.all([
expect(getExcludesStub).to.not.have.been.called,
expect(getIncludesStub).to.not.have.been.called,
expect(zipServiceStub).to.not.have.been.called,
]));
});
it('should return service artifact file path', () => {
const servicePath = 'test';
const funcName = 'test-func';
serverless.config.servicePath = servicePath;
serverless.service.functions = {};
serverless.service.package = {
artifact: 'artifact.zip',
};
serverless.service.functions[funcName] = {
name: `test-proj-${funcName}`,
};
return expect(packagePlugin.packageFunction(funcName)).to.eventually
.equal('test/artifact.zip')
.then(() => BbPromise.all([
expect(getExcludesStub).to.not.have.been.called,
expect(getIncludesStub).to.not.have.been.called,
expect(zipServiceStub).to.not.have.been.called,
]));
});
});
});

View File

@ -27,10 +27,18 @@ module.exports = {
excludeDevDependencies(params) {
const servicePath = this.serverless.config.servicePath;
const exAndInNode = excludeNodeDevDependencies(servicePath);
params.exclude = _.union(params.exclude, exAndInNode.exclude);
params.include = _.union(params.include, exAndInNode.include);
let excludeDevDependencies = this.serverless.service.package.excludeDevDependencies;
if (excludeDevDependencies === undefined || excludeDevDependencies === null) {
excludeDevDependencies = true;
}
if (excludeDevDependencies) {
const exAndInNode = excludeNodeDevDependencies(servicePath);
params.exclude = _.union(params.exclude, exAndInNode.exclude);
params.include = _.union(params.include, exAndInNode.include);
}
return BbPromise.resolve(params);
},
@ -90,6 +98,7 @@ module.exports = {
zip.append(fs.readFileSync(fullPath), {
name: filePath,
mode: stats.mode,
date: new Date(0), // necessary to get the same hash when zipping the same content
});
}
});
@ -139,7 +148,8 @@ function excludeNodeDevDependencies(servicePath) {
.execSync('npm ls --prod=true --parseable=true --silent')
.toString().trim();
const prodDependencyPaths = prodDependencies.match(/(node_modules\/.*)/g);
const nodeModulesRegex = new RegExp(`${path.join('node_modules', path.sep)}.*`, 'g');
const prodDependencyPaths = prodDependencies.match(nodeModulesRegex);
let pathToDep = '';
// if the package.json file is not in the root of the service path

View File

@ -68,6 +68,15 @@ describe('zipService', () => {
});
describe('#excludeDevDependencies()', () => {
it('should resolve when opted out of dev dependency exclusion', () => {
packagePlugin.serverless.service.package.excludeDevDependencies = false;
return expect(packagePlugin.excludeDevDependencies(params)).to.be
.fulfilled.then((updatedParams) => {
expect(updatedParams).to.deep.equal(params);
});
});
describe('when dealing with Node.js runtimes', () => {
let globbySyncStub;
let processChdirStub;

4510
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "serverless",
"version": "1.16.1",
"version": "1.17.0",
"engines": {
"node": ">=4.0"
},