Merge with master.

There was a merge issue with createStack
This commit is contained in:
David Tanner 2016-10-03 09:11:15 -06:00
commit 4d34dfde44
61 changed files with 1037 additions and 232 deletions

10
.editorconfig Normal file
View File

@ -0,0 +1,10 @@
root = true ; top-most EditorConfig file
; Unix-style newlines with a newline ending every file
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

View File

@ -1,3 +1,4 @@
coverage
node_modules
tmp
tmpdirs-serverless

View File

@ -1,8 +1,7 @@
<!--
1. Please check out and follow our Contributing Guidelines: https://github.com/serverless/serverless/blob/master/CONTRIBUTING.md
2. Fill out the whole template so we have a good overview on the issue
3. Do not remove any section of the template. If something is not applicable leave it empty but leave it in the PR
3. Please follow the template, otherwise we'll have to ask you to update it
2. Do not remove any section of the template. If something is not applicable leave it empty but leave it in the PR
3. Please follow the template, otherwise we'll have to ask you to update it and it will take longer until your PR is merged
-->
## What did you implement:
@ -23,8 +22,14 @@ If this is a nontrivial change please briefly describe your implementation so it
<!--
Add any applicable config, commands, screenshots or other resources
to make it easy for us to verify this works, e.g. an example serverless.yml
or AWS CLI commands to trigger something.
to make it easy for us to verify this works. The easier you make it for us
to review a PR, the faster we can review and merge it.
Examples:
* serverless.yml - Fully functioning to easily deploy changes
* Screenshots - Showing the difference between your output and the master
* AWS CLI commands - To list AWS resources and show that the correct config is in place
* Other - Anything else that comes to mind to help us evaluate
-->
@ -35,4 +40,7 @@ or AWS CLI commands to trigger something.
- [ ] Fix linting errors
- [ ] Make sure code coverage hasn't dropped
- [ ] Provide verification config/commands/resources
- [ ] Leave a comment that this is ready for review once you've finished the implementation
- [ ] Change ready for review message below
***Is this ready for review?:*** NO

8
.gitignore vendored
View File

@ -27,15 +27,17 @@ build/Release
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
node_modules
#IDE Stuff
# IDE stuff
**/.idea
#OS STUFF
# OS stuff
.DS_Store
.tmp
#SERVERLESS STUFF
# Serverless stuff
admin.env
.env
tmp
.coveralls.yml
tracking-id
tmpdirs-serverless

View File

@ -30,7 +30,7 @@ Please follow these Issue guidelines for opening Issues:
* Make sure your Issue is for a *feature request*, *bug report*, or *a discussion about a relevant topic*. For everything else, please use our [Discourse Forum](http://forum.serverless.com)
### Code Style
We aim for clean, consistent code style. We're using ESlint to check for codestyle issues using the Airbnb preset. If ESlint issues are found our build will fail and we can't merge the PR.
We aim for clean, consistent code style. We're using ESlint to check for codestyle issues using the Airbnb preset. If ESlint issues are found our build will fail and we can't merge the PR. To help reduce the effort of creating contributions with this style, an [.editorconfig file](http://editorconfig.org/) is provided that your editor may use to override any conflicting global defaults and automate a subset of the style settings. You may need to enable EditorConfig's use by changing a setting or installing a plugin. Using it is not compulsory.
Please follow these Code Style guidelines when writing your unit tests:
* In the root of our repo, use this command to check for styling issues: `npm run lint`

View File

@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2015 Serverless, Inc. http://www.serverless.com
Copyright (c) 2016 Serverless, Inc. http://www.serverless.com
The following license applies to all parts of this software except as
documented below:

View File

@ -10,8 +10,6 @@ Serverless is an MIT open-source project, actively maintained by a full-time, ve
## Links
* [Guide to Serverless](./docs/01-guide/README.md)
* [Features](#features)
* [Documentation v.1](./docs/README.md) / [v.0](http://serverless.readme.io)
@ -54,6 +52,31 @@ Check out our in-depth [Guide to Serverless](./docs/01-guide/README.md) for more
* An ecosystem of serverless services and plugins.
* A passionate and welcoming community!
## <a name="v1-plugins"></a>Plugins (V1.0)
Use these plugins to overwrite or extend the Framework's functionality...
* [serverless-webpack](https://github.com/elastic-coders/serverless-webpack) - Bundle your lambdas with Webpack
* [serverless-alexa-plugin](https://github.com/rajington/serverless-alexa-plugin) - Support Alexa Lambda events
* [serverless-run-function](https://github.com/lithin/serverless-run-function-plugin) - Run functions locally
* [serverless-plugin-write-env-vars](https://github.com/silvermine/serverless-plugin-write-env-vars)
* [serverless-plugin-multiple-responses](https://github.com/silvermine/serverless-plugin-multiple-responses)
* [serverless-build](https://github.com/nfour/serverless-build-plugin)
* [serverless-scriptable](https://github.com/wei-xu-myob/serverless-scriptable-plugin)
* [serverless-plugin-stage-variables](https://github.com/svdgraaf/serverless-plugin-stage-variables)
## <a name="v1-services"></a>Services & Projects (V1.0)
Pre-written functions you can use instantly and example implementations...
* [serverless-examples](https://github.com/andymac4182/serverless_example)
* [serverless-npm-registry](https://github.com/craftship/yith)
* [serverless-pokego](https://github.com/jch254/pokego-serverless)
* [serverless-pocket-app](https://github.com/s0enke/weekly2pocket)
* [serverless-quotebot](https://github.com/pmuens/quotebot)
* [serverless-slackbot](https://github.com/conveyal/trevorbot)
* [serverless-garden-aid](https://github.com/garden-aid/web-bff)
## <a name="contributing"></a>Contributing
We love our contributors! Please read our [Contributing Document](CONTRIBUTING.md) to learn how you can start working on the Framework yourself.
@ -121,7 +144,7 @@ Below are projects and plugins relating to version 0.5 and below. Note that thes
You can read the v0.5.x documentation at [readme.io](https://serverless.readme.io/v0.5.0/docs).
## v0.5.x Projects
## Projects (v0.5.x)
Serverless Projects are shareable and installable. You can publish them to npm and install them via the Serverless Framework CLI by using `$ serverless project install <project-name>`
* [serverless-graphql](https://github.com/serverless/serverless-graphql) - Official Serverless boilerplate to kick start your project
* [serverless-starter](https://github.com/serverless/serverless-starter) - A simple boilerplate for new projects (JavaScript) with a few architectural options
@ -131,7 +154,7 @@ Serverless Projects are shareable and installable. You can publish them to npm
* [sc5-serverless-boilerplate](https://github.com/SC5/sc5-serverless-boilerplate) - A boilerplate for test driven development of REST endpoints
* [MoonMail] (https://github.com/microapps/MoonMail) - Build your own email marketing infrastructure using Lambda + SES
## v0.5.x Plugins
## Plugins (v0.5.x)
Serverless is composed of Plugins. A group of default Plugins ship with the Framework, and here are some others you can add to improve/help your workflow:
* [Meta Sync](https://github.com/serverless/serverless-meta-sync) - Securely sync your the variables in your project's `_meta/variables` across your team.
* [Offline](https://github.com/dherault/serverless-offline) - Emulate AWS Lambda and Api Gateway locally to speed up your development cycles.

View File

@ -1,7 +1,7 @@
version: '2'
services:
serverless-node:
image: node:5.11.1
image: node:latest
working_dir: /app
volumes:
- .:/app
@ -28,3 +28,7 @@ services:
image: qlik/gradle
volumes:
- ./tmp/serverless-integration-test-aws-java-gradle:/app
aws-scala-sbt:
image: hseeberger/scala-sbt
volumes:
- ./tmp/serverless-integration-test-aws-scala-sbt:/app

View File

@ -12,10 +12,10 @@ You can create a service based on a specific template that specifies which provi
To create a service with a `nodejs` runtime running on `aws` just pass the `aws-nodejs` template to the create command:
```
serverless create --template aws-nodejs
serverless create --template aws-nodejs --name my-special-service
```
This will create a service and generate `serverless.yml` and `handler.js` files in the current working directory.
This will create a service and generate `serverless.yml`, `handler.js` and `event.json` files in the current working directory and set the name of the service to `my-special-service` in `serverless.yml`.
You can also check out the [create command docs](../03-cli-reference/01-create.md) for all the details and options.

View File

@ -68,7 +68,7 @@ You've successfully executed the function through the HTTP endpoint!
Serverless provides more than just a HTTP event source. You can find the full list of all available event sources with
corresponding examples in the provider specific docs:
* [AWS event documentation](../02-providers/aws/events/README.md).
* [AWS event documentation](../02-providers/aws/events/).
## Conclusion

View File

@ -21,8 +21,8 @@ We've just removed the whole service from our provider with a simple `serverless
## What's next?
You can either dive deeper into our [Advanced Guides](./README.md#advanced-guides) or read through the provider specific documentation we provide:
You can either dive deeper into our [Advanced Guides](./#advanced-guides) or read through the provider specific documentation we provide:
* [AWS Documentation](../02-providers/aws/README.md)
* [AWS Documentation](../02-providers/aws/)
Have fun with building your Serverless services and if you have feedback on the please let us know in [our Forum](forum.serverless.com) or [open an Issue in our Github repository](https://github.com/serverless/serverless/issues/new) for any bugs you might encounter or if you have an idea for a new feature.

View File

@ -52,5 +52,3 @@ plugins:
```
In this case `plugin1` is loaded before `plugin2`.
[Next step > Removing your service](removing-a-service.md)

View File

@ -28,7 +28,7 @@ We're also using the term `normalizedName` or similar terms in this guide. This
|Lambda::Permission | <ul><li>**Schedule**: {normalizedFunctionName}LambdaPermissionEventsRuleSchedule{index} </li><li>**S3**: {normalizedFunctionName}LambdaPermissionS3</li><li>**APIG**: {normalizedFunctionName}LambdaPermissionApiGateway</li><li>**SNS**: {normalizedFunctionName}LambdaPermission{normalizedTopicName}</li> | <ul><li>**Schedule**: HelloLambdaPermissionEventsRuleSchedule1 </li><li>**S3**: HelloLambdaPermissionS3</li><li>**APIG**: HelloLambdaPermissionApiGateway</li><li>**SNS**: HelloLambdaPermissionSometopic</li> |
|Events::Rule | {normalizedFuntionName}EventsRuleSchedule{SequentialID} | HelloEventsRuleSchedule1 |
|ApiGateway::RestApi | ApiGatewayRestApi | ApiGatewayRestApi |
|ApiGateway::Resource | ApiGatewayResource{normalizedPath} | ApiGatewayResourceUsers |
|ApiGateway::Resource | ApiGatewayResource{normalizedPath} | <ul><li>ApiGatewayResourceUsers</li><li>ApiGatewayResourceUsers**Var** for paths containing a variable</li><li>ApiGatewayResource**Dash** if the path is just a `-`</li></ul> |
|ApiGateway::Method | ApiGatewayResource{normalizedPath}{normalizedMethod} | ApiGatewayResourceUsersGet |
|ApiGateway::Authorizer | {normalizedFunctionName}ApiGatewayAuthorizer | HelloApiGatewayAuthorizer |
|ApiGateway::Deployment | ApiGatewayDeployment{randomNumber} | ApiGatewayDeployment12356789 |

View File

@ -6,7 +6,7 @@ layout: Doc
# Serverless AWS Documentation
Check out the [Getting started guide](../../01-guide/README.md) and the [CLI reference](../../03-cli-reference/README.md) for an introduction to Serverless.
Check out the [Getting started guide](../../01-guide/) and the [CLI reference](../../03-cli-reference/) for an introduction to Serverless.
## Setup and configuration

View File

@ -71,12 +71,34 @@ functions:
path: whatever
request:
template:
text/xhtml: { "stage" : "$context.stage" }
application/json: { "httpMethod" : "$context.httpMethod" }
text/xhtml: '{ "stage" : "$context.stage" }'
application/json: '{ "httpMethod" : "$context.httpMethod" }'
```
**Note:** The templates are defined as plain text here. However you can also reference an external file with the help of the `${file(templatefile)}` syntax.
**Note 2:** In .yml, strings containing `:`, `{`, `}`, `[`, `]`, `,`, `&`, `*`, `#`, `?`, `|`, `-`, `<`, `>`, `=`, `!`, `%`, `@`, `` ` `` must be quoted.
If you want to map querystrings to the event object, you can use the `$input.params('hub.challenge')` syntax from API Gateway, as follows:
```yml
functions:
create:
handler: posts.create
events:
- http:
method: get
path: whatever
request:
template:
application/json: '{ "foo" : "$input.params(''bar'')" }'
```
**Note:** Notice when using single-quoted strings, any single quote `'` inside its contents must be doubled (`''`) to escape it.
You can then access the query string `https://example.com/dev/whatever?bar=123` by `event.foo` in the lambda function.
If you want to spread a string into multiple lines, you can use the `>` or `|` syntax, but the following strings have to be all indented with the same amount, [read more about `>` syntax](http://stackoverflow.com/questions/3790454/in-yaml-how-do-i-break-a-string-over-multiple-lines).
### Pass Through Behavior
API Gateway provides multiple ways to handle requests where the Content-Type header does not match any of the specified mapping templates. When this happens, the request payload will either be passed through the integration request *without transformation* or rejected with a `415 - Unsupported Media Type`, depending on the configuration.
@ -356,14 +378,14 @@ resources:
- RootResourceId
PathPart: serverless # the endpoint in your API that is set as proxy
RestApiId:
Ref: RestApiApigEvent
Ref: ApiGatewayRestApi
ProxyMethod:
ResourceId:
Ref: ProxyResource
RestApiId:
Ref: RestApiApigEvent
Type: AWS::ApiGateway::Method
Properties:
ResourceId:
Ref: ProxyResource
RestApiId:
Ref: ApiGatewayRestApi
HttpMethod: GET # the method of your proxy. Is it GET or POST or ... ?
MethodResponses:
- StatusCode: 200

View File

@ -32,6 +32,23 @@ functions:
event: s3:ObjectRemoved:*
```
## Setting filter rules
This will create a bucket `photos`. The `users` function is called whenever an image with `.jpg` extension is uploaded to folder `uploads` in the bucket. Check out the [AWS documentation](http://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html#notification-how-to-filtering) to learn more about all the different filter types that can be configured.
```yml
functions:
users:
handler: users.handler
rules:
- s3:
bucket: photos
event: s3:ObjectCreated:*
rules:
- prefix: uploads/
- suffix: .jpg
```
## Triggering separate functions from the same bucket
You're able to repeat the S3 event configuration in the same or separate functions so one bucket can call these functions. One caveat though is that you can't repeat the same configuration in both functions, e.g. the event type has to be different.

View File

@ -16,6 +16,7 @@ serverless create --template aws-nodejs
## Options
- `--template` or `-t` The name of your new service. **Required**.
- `--path` or `-p` The path where the service should be created.
- `--name` or `-n` the name of the service in `serverless.yml`.
## Provided lifecycle events
- `create:create`
@ -36,7 +37,7 @@ Most commonly used templates:
### Creating a new service
```
serverless create --template aws-nodejs
serverless create --template aws-nodejs --name my-special-service
```
This example will generate scaffolding for a service with `AWS` as a provider and `nodejs` as runtime. The scaffolding

View File

@ -24,7 +24,8 @@ serverless info
### AWS
On AWS the info plugin uses the `Outputs` section of the CloudFormation stack. Outputs will include Lambda function ARN's, a `ServiceEndpoint` for the API Gateway endpoint and user provided custom Outputs.
On AWS the info plugin uses the `Outputs` section of the CloudFormation stack and the AWS SDK to gather the necessary information.
See the example below for an example output.
**Example:**
@ -35,6 +36,8 @@ Service Information
service: my-serverless-service
stage: dev
region: us-east-1
api keys:
myKey: some123valid456api789key1011for1213api1415gateway
endpoints:
GET - https://dxaynpuzd4.execute-api.us-east-1.amazonaws.com/dev/users
functions:

View File

@ -8,15 +8,15 @@ layout: Doc
Welcome to the Serverless documentation.
- [Quick Start Guide](./01-guide/README.md)
- [Quick Start Guide](./01-guide/)
- [Core Concepts](#concepts)
- [CLI Reference](./03-cli-reference/README.md)
- [CLI Reference](./03-cli-reference/)
- [Providers](#providers)
- [Extending Serverless](./04-extending-serverless/README.md)
- [Extending Serverless](./04-extending-serverless/)
- [Contributing to Serverless](#contributing)
## Providers
- [AWS Integration Docs](./02-providers/aws/README.md)
- [AWS Integration Docs](./02-providers/aws/)
## Concepts
@ -38,7 +38,7 @@ Serverless is used to build event driven architecture. Basically everything whic
Events could be HTTP requests, events fired from a cloud storage (like a S3 bucket), scheduled events, etc.
- [AWS Events](./02-providers/aws/events/README.md)
- [AWS Events](./02-providers/aws/events/)
### Resources
@ -52,7 +52,7 @@ A *Serverless service* is a group of one or multiple functions and any resources
Here you can read how to develop your own Serverless plugins. We'll get into details on how to write custom plugins to extend the functionality of Serverless. Furthermore we'll look into the way how you can use your plugin knowledge to integrate your own provider into the Serverless framework.
- [Building plugins](./04-extending-serverless/README.md)
- [Building plugins](./04-extending-serverless/)
Connect with the community on [gitter](https://gitter.im/serverless/serverless) or in the [Forum](http://forum.serverless.com)

View File

@ -14,16 +14,7 @@ of our users to improve Serverless in future releases. However you can always [d
Our main goal is anonymity while tracking usage behavior. All the data is anonymized and won't reveal who you are or what
the project you're working on is / looks like.
Here's a list about all the information we track:
- Entered command(s)
- Operating system
- Loaded plugins
- Serverless version
- Number of functions inside your service
- Total number of events inside your service
- Event types and how often they were defined
- Provider of your service
- If the command was executed inside a service
Please take a look at the [`track()` method](../lib/classes/Utils.js) in the `Utils` class to see what (and how) we track.
## How tracking is implemented

View File

@ -69,10 +69,11 @@ class Service {
};
}
if (['aws', 'azure', 'google', 'ibm'].indexOf(serverlessFile.provider.name)) {
const providers = ['aws', 'azure', 'google', 'ibm'];
if (providers.indexOf(serverlessFile.provider.name) === -1) {
const errorMessage = [
`Provider "${serverlessFile.provider.name}" is not supported.`,
' Valid values for provider are: aws, azure, google, ibm.',
` Valid values for provider are: ${providers.join(', ')}.`,
' Please provide one of those values to the "provider" property in serverless.yml.',
].join('');
throw new SError(errorMessage);

View File

@ -7,6 +7,7 @@ const BbPromise = require('bluebird');
const fse = BbPromise.promisifyAll(require('fs-extra'));
const _ = require('lodash');
const fetch = require('node-fetch');
const uuid = require('uuid');
class Utils {
constructor(serverless) {
@ -76,7 +77,7 @@ class Utils {
if (filePath.endsWith('.json')) {
contents = JSON.parse(contents);
} else if (filePath.endsWith('.yml') || filePath.endsWith('.yaml')) {
contents = YAML.load(contents.toString());
contents = YAML.load(contents.toString(), { filename: filePath });
} else {
contents = contents.toString().trim();
}
@ -144,47 +145,138 @@ class Utils {
track(serverless) {
const writeKey = 'XXXX'; // TODO: Replace me before release
const loadedPlugins = serverless.pluginManager.plugins.map((plugin) => plugin.constructor.name);
const events = [];
let userId = uuid.v1();
if (serverless.service && serverless.service.functions) {
// create a new file with a uuid as the tracking id if not yet present
const trackingIdFilePath = path.join(serverless.config.serverlessPath, 'tracking-id');
if (!this.fileExistsSync(trackingIdFilePath)) {
fs.writeFileSync(trackingIdFilePath, userId);
} else {
userId = fs.readFileSync(trackingIdFilePath).toString();
}
// function related information retrieval
const numberOfFunctions = _.size(serverless.service.functions);
const memorySizeAndTimeoutPerFunction = [];
if (numberOfFunctions) {
_.forEach(serverless.service.functions, (func) => {
const memorySize = Number(func.memorySize)
|| Number(this.serverless.service.provider.memorySize)
|| 1024;
const timeout = Number(func.timeout)
|| Number(this.serverless.service.provider.timeout)
|| 6;
const memorySizeAndTimeoutObject = {
memorySize,
timeout,
};
memorySizeAndTimeoutPerFunction.push(memorySizeAndTimeoutObject);
});
}
// event related information retrieval
const numberOfEventsPerType = [];
const eventNamesPerFunction = [];
if (numberOfFunctions) {
_.forEach(serverless.service.functions, (func) => {
if (func.events) {
const funcEventsArray = [];
func.events.forEach((event) => {
const name = Object.keys(event)[0];
const alreadyPresentEvent = _.find(events, { name });
funcEventsArray.push(name);
const alreadyPresentEvent = _.find(numberOfEventsPerType, { name });
if (alreadyPresentEvent) {
alreadyPresentEvent.count++;
} else {
events.push({
numberOfEventsPerType.push({
name,
count: 1,
});
}
});
eventNamesPerFunction.push(funcEventsArray);
}
});
}
let hasCustomResourcesDefined = false;
// check if configuration in resources.Resources is defined
if ((serverless.service.resources &&
serverless.service.resources.Resources &&
Object.keys(serverless.service.resources.Resources).length)) {
hasCustomResourcesDefined = true;
}
// check if configuration in resources.Outputs is defined
if ((serverless.service.resources &&
serverless.service.resources.Outputs &&
Object.keys(serverless.service.resources.Outputs).length)) {
hasCustomResourcesDefined = true;
}
let hasCustomVariableSyntaxDefined = false;
const defaultVariableSyntax = '\\${([ :a-zA-Z0-9._,\\-\\/\\(\\)]+?)}';
// check if the variableSyntax in the defaults section is defined
if (serverless.service.defaults &&
serverless.service.defaults.variableSyntax &&
serverless.service.defaults.variableSyntax !== defaultVariableSyntax) {
hasCustomVariableSyntaxDefined = true;
}
// check if the variableSyntax in the provider section is defined
if (serverless.service.provider &&
serverless.service.provider.variableSyntax &&
serverless.service.provider.variableSyntax !== defaultVariableSyntax) {
hasCustomVariableSyntaxDefined = true;
}
const auth = `${writeKey}:`;
const data = {
userId: 'anonymousUser',
userId,
event: 'Serverless framework usage',
properties: {
commands: serverless.processedInput.commands,
operatingSystem: process.platform,
loadedPlugins,
serverlessVersion: serverless.version,
numberOfFunctions: _.size(serverless.service.functions),
events,
totalNumberOfEvents: events.length,
provider: serverless.service.provider,
isRunInsideService: (!!serverless.config.servicePath),
command: {
name: serverless.processedInput.commands.join(' '),
isRunInService: (!!serverless.config.servicePath),
},
service: {
numberOfCustomPlugins: _.size(serverless.service.plugins),
hasCustomResourcesDefined,
hasVariablesInCustomSectionDefined: (!!serverless.service.custom),
hasCustomVariableSyntaxDefined,
},
provider: {
name: serverless.service.provider.name,
runtime: serverless.service.provider.runtime,
stage: serverless.service.provider.stage,
region: serverless.service.provider.region,
},
functions: {
numberOfFunctions,
memorySizeAndTimeoutPerFunction,
},
events: {
numberOfEvents: numberOfEventsPerType.length,
numberOfEventsPerType,
eventNamesPerFunction,
},
general: {
userId,
timestamp: (new Date()).getTime(),
timezone: (new Date()).toString().match(/([A-Z]+[\+-][0-9]+.*)/)[1],
operatingSystem: process.platform,
serverlessVersion: serverless.version,
nodeJsVersion: process.version,
},
},
};
fetch('https://api.segment.io/v1/track', {
return fetch('https://api.segment.io/v1/track', {
headers: {
Authorization: `Basic ${new Buffer(auth).toString('base64')}`,
'content-type': 'application/json',

View File

@ -36,7 +36,8 @@ class AwsCompileApigEvents {
_.forEach(this.serverless.service.functions, functionObj => {
if (functionObj.events) {
functionObj.events.forEach(event => {
if (event.http) noEndpoints = false;
// Allow events with empty http event to validate function
if ({}.hasOwnProperty.call(event, 'http')) noEndpoints = false;
});
}
});

View File

@ -38,21 +38,6 @@ module.exports = {
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources,
newApiKeyObject);
// Add API Key to Outputs section
const newOutput = {
Description: apiKey,
Value: {
Ref: `ApiGatewayApiKey${apiKeyNumber}`,
},
};
const newOutputObject = {
[`ApiGatewayApiKey${apiKeyNumber}Value`]: newOutput,
};
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Outputs,
newOutputObject);
});
}

View File

@ -333,7 +333,8 @@ module.exports = {
{ StatusCode: 422, SelectionPattern: '.*\\[422\\].*' },
{ StatusCode: 500,
SelectionPattern:
'.*(Process\\s?exited\\s?before\\s?completing\\s?request|\\[500\\]).*' },
// eslint-disable-next-line max-len
'.*(Process\\s?exited\\s?before\\s?completing\\s?request|Task\\s?timed\\s?out\\s?|\\[500\\]).*' },
{ StatusCode: 502, SelectionPattern: '.*\\[502\\].*' },
{ StatusCode: 504, SelectionPattern: '.*\\[504\\].*' }
);

View File

@ -43,7 +43,12 @@ module.exports = {
});
});
const capitalizeAlphaNumericPath = (path) => _.capitalize(path.replace(/[^0-9A-Za-z]/g, ''));
const capitalizeAlphaNumericPath = (path) => _.upperFirst(
_.capitalize(path)
.replace(/-/g, 'Dash')
.replace(/\{(.*)\}/g, '$1Var')
.replace(/[^0-9A-Za-z]/g, '')
);
// ['users', 'users/create', 'users/create/something']
this.resourcePaths.forEach(path => {

View File

@ -8,15 +8,26 @@ module.exports = {
// validate that path and method exists for each http event in service
_.forEach(this.serverless.service.functions, (functionObject, functionName) => {
functionObject.events.forEach(event => {
if (event.http) {
if ({}.hasOwnProperty.call(event, 'http')) {
let method;
let path;
if (!event.http) {
const errorMessage = [
`Empty http event in function "${functionName}"`,
' in serverless.yml.',
' If you define an http event, make sure you pass a valid value for it,',
' either as string syntax, or object syntax.',
' Please check the docs for more options.',
].join('');
throw new this.serverless.classes.Error(errorMessage);
}
if (typeof event.http === 'object') {
method = event.http.method.toLowerCase();
method = event.http.method;
path = event.http.path;
} else if (typeof event.http === 'string') {
method = event.http.split(' ')[0].toLowerCase();
method = event.http.split(' ')[0];
path = event.http.split(' ')[1];
}
@ -43,11 +54,15 @@ module.exports = {
throw new this.serverless.classes
.Error(errorMessage);
}
method = method.toLowerCase();
if (['get', 'post', 'put', 'patch', 'options', 'head', 'delete'].indexOf(method) === -1) {
const allowedMethods = [
'get', 'post', 'put', 'patch', 'options', 'head', 'delete', 'any',
];
if (allowedMethods.indexOf(method) === -1) {
const errorMessage = [
`Invalid APIG method "${method}" in function "${functionName}".`,
' AWS supported methods are: get, post, put, patch, options, head, delete.',
` AWS supported methods are: ${allowedMethods.join(', ')}.`,
].join('');
throw new this.serverless.classes.Error(errorMessage);
}

View File

@ -82,20 +82,6 @@ describe('#compileApiKeys()', () => {
})
);
it('should add api keys cf output template', () => awsCompileApigEvents
.compileApiKeys().then(() => {
expect(
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
.Outputs.ApiGatewayApiKey1Value.Description
).to.equal('1234567890');
expect(
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
.Outputs.ApiGatewayApiKey1Value.Value.Ref
).to.equal('ApiGatewayApiKey1');
})
);
it('throw error if apiKey property is not an array', () => {
awsCompileApigEvents.serverless.service.provider.apiKeys = 2;
expect(() => awsCompileApigEvents.compileApiKeys()).to.throw(Error);

View File

@ -665,7 +665,9 @@ describe('#compileMethods()', () => {
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[6]
).to.deep.equal({ StatusCode: 500,
SelectionPattern: '.*(Process\\s?exited\\s?before\\s?completing\\s?request|\\[500\\]).*' });
SelectionPattern:
// eslint-disable-next-line max-len
'.*(Process\\s?exited\\s?before\\s?completing\\s?request|Task\\s?timed\\s?out\\s?|\\[500\\]).*' });
expect(
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[7]

View File

@ -21,6 +21,9 @@ describe('#compileResources()', () => {
method: 'POST',
},
},
{
http: 'GET bar/-',
},
{
http: 'GET bar/foo',
},
@ -57,8 +60,17 @@ describe('#compileResources()', () => {
it('should construct the correct resourcePaths array', () => awsCompileApigEvents
.compileResources().then(() => {
const expectedResourcePaths = ['foo/bar', 'foo', 'bar/foo', 'bar', 'bar/{id}',
'bar/{id}/foobar', 'bar/{foo_id}', 'bar/{foo_id}/foobar'];
const expectedResourcePaths = [
'foo/bar',
'foo',
'bar/-',
'bar',
'bar/foo',
'bar/{id}',
'bar/{id}/foobar',
'bar/{foo_id}',
'bar/{foo_id}/foobar',
];
expect(awsCompileApigEvents.resourcePaths).to.deep.equal(expectedResourcePaths);
})
);
@ -66,12 +78,13 @@ describe('#compileResources()', () => {
it('should construct the correct resourceLogicalIds object', () => awsCompileApigEvents
.compileResources().then(() => {
const expectedResourceLogicalIds = {
'bar/-': 'ApiGatewayResourceBarDash',
'foo/bar': 'ApiGatewayResourceFooBar',
foo: 'ApiGatewayResourceFoo',
'bar/{id}/foobar': 'ApiGatewayResourceBarIdFoobar',
'bar/{id}': 'ApiGatewayResourceBarId',
'bar/{foo_id}/foobar': 'ApiGatewayResourceBarFooidFoobar',
'bar/{foo_id}': 'ApiGatewayResourceBarFooid',
'bar/{id}/foobar': 'ApiGatewayResourceBarIdVarFoobar',
'bar/{id}': 'ApiGatewayResourceBarIdVar',
'bar/{foo_id}/foobar': 'ApiGatewayResourceBarFooidVarFoobar',
'bar/{foo_id}': 'ApiGatewayResourceBarFooidVar',
'bar/foo': 'ApiGatewayResourceBarFoo',
bar: 'ApiGatewayResourceBar',
};
@ -94,14 +107,14 @@ describe('#compileResources()', () => {
.Resources.ApiGatewayResourceBar.Properties.ParentId['Fn::GetAtt'][1])
.to.equal('RootResourceId');
expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayResourceBarId.Properties.ParentId.Ref)
.Resources.ApiGatewayResourceBarIdVar.Properties.ParentId.Ref)
.to.equal('ApiGatewayResourceBar');
expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayResourceBarFooid.Properties.ParentId.Ref)
.Resources.ApiGatewayResourceBarFooidVar.Properties.ParentId.Ref)
.to.equal('ApiGatewayResourceBar');
expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayResourceBarFooidFoobar.Properties.ParentId.Ref)
.to.equal('ApiGatewayResourceBarFooid');
.Resources.ApiGatewayResourceBarFooidVarFoobar.Properties.ParentId.Ref)
.to.equal('ApiGatewayResourceBarFooidVar');
})
);

View File

@ -30,6 +30,19 @@ describe('#validate()', () => {
};
});
it('should reject an empty http event', () => {
awsCompileApigEvents.serverless.service.functions = {
first: {
events: [
{
http: null,
},
],
},
};
expect(() => awsCompileApigEvents.validate()).to.throw(Error);
});
it('should validate the http events "path" property', () => {
awsCompileApigEvents.serverless.service.functions = {
first: {

View File

@ -58,3 +58,19 @@ functions:
bucket: confidential-information
event: s3:ObjectRemoved:*
```
We can also specify filter rules.
```yml
# serverless.yml
functions:
mail:
handler: mail.removal
events:
- s3:
bucket: confidential-information
event: s3:ObjectRemoved:*
rules:
- prefix: inbox/
- suffix: .eml
```

View File

@ -23,6 +23,7 @@ class AwsCompileS3Events {
if (event.s3) {
let bucketName;
let notificationEvent = 's3:ObjectCreated:*';
let filter = {};
if (typeof event.s3 === 'object') {
if (!event.s3.bucket) {
@ -38,6 +39,33 @@ class AwsCompileS3Events {
if (event.s3.event) {
notificationEvent = event.s3.event;
}
if (event.s3.rules) {
if (!_.isArray(event.s3.rules)) {
const errorMessage = [
`S3 filter rules of function ${functionName} is not an array`,
' The correct syntax is: rules: [{ Name: Value }]',
' Please check the docs for more info.',
].join('');
throw new this.serverless.classes
.Error(errorMessage);
}
const rules = [];
event.s3.rules.forEach(rule => {
if (!_.isPlainObject(rule)) {
const errorMessage = [
`S3 filter rule ${rule} of function ${functionName} is not an object`,
' The correct syntax is: { Name: Value }',
' Please check the docs for more info.',
].join('');
throw new this.serverless.classes
.Error(errorMessage);
}
const name = Object.keys(rule)[0];
const value = rule[name];
rules.push({ Name: name, Value: value });
});
filter = { Filter: { S3Key: { Rules: rules } } };
}
} else if (typeof event.s3 === 'string') {
bucketName = event.s3;
} else {
@ -56,7 +84,7 @@ class AwsCompileS3Events {
// check if the bucket already defined
// in another S3 event in the service
if (bucketsLambdaConfigurations[bucketName]) {
const newLambdaConfiguration = {
let newLambdaConfiguration = {
Event: notificationEvent,
Function: {
'Fn::GetAtt': [
@ -66,6 +94,11 @@ class AwsCompileS3Events {
},
};
// Assign 'filter' if not empty
newLambdaConfiguration = _.assign(
newLambdaConfiguration,
filter
);
bucketsLambdaConfigurations[bucketName]
.push(newLambdaConfiguration);
} else {
@ -80,6 +113,11 @@ class AwsCompileS3Events {
},
},
];
// Assign 'filter' if not empty
bucketsLambdaConfigurations[bucketName][0] = _.assign(
bucketsLambdaConfigurations[bucketName][0],
filter
);
}
s3EnabledFunctions.push(functionName);
}

View File

@ -51,6 +51,42 @@ describe('AwsCompileS3Events', () => {
expect(() => awsCompileS3Events.compileS3Events()).to.throw(Error);
});
it('should throw an error if the "rules" property is not an array', () => {
awsCompileS3Events.serverless.service.functions = {
first: {
events: [
{
s3: {
bucket: 'first-function-bucket',
event: 's3:ObjectCreated:Put',
rules: {},
},
},
],
},
};
expect(() => awsCompileS3Events.compileS3Events()).to.throw(Error);
});
it('should throw an error if the "rules" property is invalid', () => {
awsCompileS3Events.serverless.service.functions = {
first: {
events: [
{
s3: {
bucket: 'first-function-bucket',
event: 's3:ObjectCreated:Put',
rules: [[]],
},
},
],
},
};
expect(() => awsCompileS3Events.compileS3Events()).to.throw(Error);
});
it('should create corresponding resources when S3 events are given', () => {
awsCompileS3Events.serverless.service.functions = {
first: {
@ -62,6 +98,9 @@ describe('AwsCompileS3Events', () => {
s3: {
bucket: 'first-function-bucket-two',
event: 's3:ObjectCreated:Put',
rules: [
{ prefix: 'subfolder/' },
],
},
},
],
@ -79,6 +118,11 @@ describe('AwsCompileS3Events', () => {
expect(awsCompileS3Events.serverless.service.provider.compiledCloudFormationTemplate
.Resources.FirstLambdaPermissionS3.Type
).to.equal('AWS::Lambda::Permission');
expect(awsCompileS3Events.serverless.service.provider.compiledCloudFormationTemplate
.Resources.S3BucketFirstfunctionbuckettwo.Properties.NotificationConfiguration
.LambdaConfigurations[0].Filter).to.deep.equal({
S3Key: { Rules: [{ Name: 'prefix', Value: 'subfolder/' }] },
});
});
it('should create single bucket resource when the same bucket referenced repeatedly', () => {
@ -92,6 +136,9 @@ describe('AwsCompileS3Events', () => {
s3: {
bucket: 'first-function-bucket-one',
event: 's3:ObjectCreated:Put',
rules: [
{ prefix: 'subfolder/' },
],
},
},
],

View File

@ -10,7 +10,7 @@ module.exports = {
const params = {
StackName: stackName,
OnFailure: 'DELETE',
OnFailure: 'ROLLBACK',
Capabilities: [
'CAPABILITY_IAM',
],
@ -32,6 +32,16 @@ module.exports = {
createStack() {
const stackName = `${this.serverless.service.service}-${this.options.stage}`;
if (/^[^a-zA-Z].+|.*[^a-zA-Z0-9\-].*/.test(stackName) || stackName.length > 128) {
const errorMessage = [
`The stack service name "${stackName}" is not valid. `,
'A service name should only contain alphanumeric',
' (case sensitive) and hyphens. It should start',
' with an alphabetic character and shouldn\'t',
' exceed 128 characters.',
].join('');
throw new this.serverless.classes.Error(errorMessage);
}
return BbPromise.bind(this)
// always write the template to disk, whether we are deploying or not

View File

@ -1,12 +1,12 @@
'use strict';
const sinon = require('sinon');
const os = require('os');
const path = require('path');
const BbPromise = require('bluebird');
const expect = require('chai').expect;
const AwsDeploy = require('../index');
const Serverless = require('../../../../Serverless');
const testUtils = require('../../../../../tests/utils');
describe('uploadArtifacts', () => {
let serverless;
@ -61,7 +61,7 @@ describe('uploadArtifacts', () => {
});
it('should upload the .zip file to the S3 bucket', () => {
const tmpDirPath = path.join(os.tmpdir(), (new Date()).getTime().toString());
const tmpDirPath = testUtils.getTmpDirPath();
const artifactFilePath = path.join(tmpDirPath, 'artifact.zip');
serverless.utils.writeFileSync(artifactFilePath, 'artifact.zip file content');

View File

@ -51,12 +51,13 @@ class AwsInfo {
this.options.stage,
this.options.region)
.then((result) => {
let outputs;
if (result) {
const outputs = result.Stacks[0].Outputs;
outputs = result.Stacks[0].Outputs;
// Functions
info.functions = [];
info.apiKeys = [];
outputs.filter(x => x.OutputKey.match(/LambdaFunctionArn$/))
.forEach(x => {
const functionInfo = {};
@ -71,21 +72,23 @@ class AwsInfo {
info.endpoint = x.OutputValue;
});
// API Keys
outputs.filter(x => x.OutputKey.match(/^ApiGatewayApiKey/))
.forEach(x => {
const apiKeyInfo = {};
apiKeyInfo.name = x.Description;
apiKeyInfo.value = x.OutputValue;
info.apiKeys.push(apiKeyInfo);
});
// Resources
info.resources = [];
// API Keys
info.apiKeys = [];
}
return BbPromise.resolve(info);
// create a gatheredData object which can be passed around ("[call] by reference")
const gatheredData = {
outputs,
info,
};
return BbPromise.resolve(gatheredData);
})
.then((gatheredData) => this.getApiKeyValues(gatheredData))
.then((gatheredData) => BbPromise.resolve(gatheredData.info)) // resolve the info at the end
.catch((e) => {
let result;
@ -102,6 +105,38 @@ class AwsInfo {
});
}
getApiKeyValues(gatheredData) {
const info = gatheredData.info;
// check if the user has set api keys
const apiKeyNames = this.serverless.service.provider.apiKeys || [];
if (apiKeyNames.length) {
return this.sdk.request('APIGateway',
'getApiKeys',
{ includeValues: true },
this.options.stage,
this.options.region
).then((allApiKeys) => {
const items = allApiKeys.items;
if (items) {
// filter out the API keys only created for this stack
const filteredItems = items.filter((item) => _.includes(apiKeyNames, item.name));
// iterate over all apiKeys and push the API key info and update the info object
filteredItems.forEach((item) => {
const apiKeyInfo = {};
apiKeyInfo.name = item.name;
apiKeyInfo.value = item.value;
info.apiKeys.push(apiKeyInfo);
});
}
return BbPromise.resolve(gatheredData);
});
}
return BbPromise.resolve(gatheredData);
}
/**
* Display service information
*/

View File

@ -163,7 +163,7 @@ describe('AwsInfo', () => {
it('should gather with correct params', () => awsInfo.gather()
.then(() => {
expect(describeStackStub.calledOnce).to.equal(true);
expect(describeStackStub.called).to.equal(true);
expect(describeStackStub.args[0][0]).to.equal('CloudFormation');
expect(describeStackStub.args[0][1]).to.equal('describeStacks');
expect(describeStackStub.args[0][2].StackName)
@ -218,23 +218,6 @@ describe('AwsInfo', () => {
});
});
it('should get api keys', () => {
const expectedApiKeys = [
{
name: 'first',
value: 'xxx',
},
{
name: 'second',
value: 'yyy',
},
];
return awsInfo.gather().then((info) => {
expect(info.apiKeys).to.deep.equal(expectedApiKeys);
});
});
it("should provide only general info when stack doesn't exist (ValidationError)", () => {
awsInfo.sdk.request.restore();
@ -267,6 +250,85 @@ describe('AwsInfo', () => {
});
});
describe('#getApiKeyValues()', () => {
it('should return the api keys in the info object', () => {
// TODO: implement a pattern for stub restoring to get rid of this
awsInfo.sdk.request.restore();
// set the API Keys for the service
awsInfo.serverless.service.provider.apiKeys = ['foo', 'bar'];
const gatheredData = {
outputs: [],
info: {
apiKeys: [],
},
};
const apiKeyItems = {
items: [
{
id: '4711',
name: 'SomeRandomIdInUsersAccount',
value: 'ShouldNotBeConsidered',
},
{
id: '1234',
name: 'foo',
value: 'valueForKeyFoo',
},
{
id: '5678',
name: 'bar',
value: 'valueForKeyBar',
},
],
};
const gatheredDataAfterKeyLookup = {
info: {
apiKeys: [
{ name: 'foo', value: 'valueForKeyFoo' },
{ name: 'bar', value: 'valueForKeyBar' },
],
},
};
const getApiKeysStub = sinon
.stub(awsInfo.sdk, 'request')
.returns(BbPromise.resolve(apiKeyItems));
return awsInfo.getApiKeyValues(gatheredData).then((result) => {
expect(getApiKeysStub.calledOnce).to.equal(true);
expect(result.info.apiKeys).to.deep.equal(gatheredDataAfterKeyLookup.info.apiKeys);
awsInfo.sdk.request.restore();
});
});
it('should resolve with the passed-in data if no API key retrieval is necessary', () => {
awsInfo.serverless.service.provider.apiKeys = null;
const gatheredData = {
outputs: [],
info: {
apiKeys: [],
},
};
const getApiKeysStub = sinon
.stub(awsInfo.sdk, 'request')
.returns(BbPromise.resolve());
return awsInfo.getApiKeyValues(gatheredData).then((result) => {
expect(getApiKeysStub.calledOnce).to.equal(false);
expect(result).to.deep.equal(gatheredData);
awsInfo.sdk.request.restore();
});
});
});
describe('#display()', () => {
it('should format information message correctly', () => {
serverless.cli = new CLI(serverless);

View File

@ -93,6 +93,23 @@ describe('AwsInvoke', () => {
});
});
it('it should parse a yaml file if file path is provided', () => {
serverless.config.servicePath = testUtils.getTmpDirPath();
const yamlContent = 'testProp: testValue';
serverless.utils.writeFileSync(path
.join(serverless.config.servicePath, 'data.yml'), yamlContent);
awsInvoke.options.path = 'data.yml';
return awsInvoke.extendedValidate().then(() => {
expect(awsInvoke.options.data).to.deep.equal({
testProp: 'testValue',
});
awsInvoke.options.path = false;
serverless.config.servicePath = true;
});
});
it('it should throw error if service path is not set', () => {
serverless.config.servicePath = false;
expect(() => awsInvoke.extendedValidate()).to.throw(Error);

View File

@ -10,6 +10,7 @@ const validTemplates = [
'aws-python',
'aws-java-maven',
'aws-java-gradle',
'aws-scala-sbt',
];
const humanReadableTemplateList = `${validTemplates.slice(0, -1)
@ -36,6 +37,10 @@ class Create {
usage: 'The path where the service should be created (e.g. --path my-service)',
shortcut: 'p',
},
name: {
usage: 'Name for the service. Overwrites the default name of the created service.',
shortcut: 'n',
},
},
},
};
@ -57,8 +62,9 @@ class Create {
throw new this.serverless.classes.Error(errorMessage);
}
// store the path option for the service if given
// store the custom options for the service if given
const servicePath = this.options.path && this.options.path.length ? this.options.path : null;
const serviceName = this.options.name && this.options.name.length ? this.options.name : null;
// create (if not yet present) and chdir into the directory for the service
if (servicePath) {
@ -78,8 +84,8 @@ class Create {
'plugins', 'create', 'templates', this.options.template), this.serverless.config.servicePath);
// rename the service if the user has provided a path via options
if (servicePath) {
const serviceName = servicePath.split(path.sep).pop();
if (servicePath || serviceName) {
const newServiceName = serviceName || servicePath.split(path.sep).pop();
const serverlessYmlFilePath = path
.join(this.serverless.config.servicePath, 'serverless.yml');
@ -87,7 +93,7 @@ class Create {
.readFileSync(serverlessYmlFilePath).toString();
serverlessYmlFileContent = serverlessYmlFileContent
.replace(/service: .+/, `service: ${serviceName}`);
.replace(/service: .+/, `service: ${newServiceName}`);
fse.writeFileSync(serverlessYmlFilePath, serverlessYmlFileContent);
}

View File

@ -1,2 +1,5 @@
def hello(event, context):
return { "message": "Go Serverless v1.0! Your function executed successfully!", "event": event }
return {
"message": "Go Serverless v1.0! Your function executed successfully!",
"event": event
}

View File

@ -0,0 +1,21 @@
import sbt.Keys._
import sbt._
import sbtrelease.Version
name := "hello"
resolvers += Resolver.sonatypeRepo("public")
scalaVersion := "2.11.8"
releaseNextVersion := { ver => Version(ver).map(_.bumpMinor.string).getOrElse("Error") }
assemblyJarName in assembly := "hello.jar"
libraryDependencies ++= Seq(
"com.amazonaws" % "aws-lambda-java-events" % "1.1.0",
"com.amazonaws" % "aws-lambda-java-core" % "1.1.0"
)
scalacOptions ++= Seq(
"-unchecked",
"-deprecation",
"-feature",
"-Xfatal-warnings")

View File

@ -0,0 +1,5 @@
{
"key3": "value3",
"key2": "value2",
"key1": "value1"
}

View File

@ -0,0 +1,3 @@
resolvers += Resolver.sonatypeRepo("public")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.0")

View File

@ -0,0 +1 @@
addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.0")

View File

@ -0,0 +1,70 @@
# Welcome to Serverless!
#
# This file is the main config file for your service.
# It's very minimal at this point and uses default values.
# You can always add more config options for more control.
# We've included some commented out config examples here.
# Just uncomment any of them to get that config option.
#
# For full config options, check the docs:
# docs.serverless.com
#
# Happy Coding!
service: aws-scala-sbt # NOTE: update this with your service name
provider:
name: aws
runtime: java8
# you can overwrite defaults here
# stage: dev
# region: us-east-1
# you can add statements to the Lambda function's IAM Role here
# iamRoleStatements:
# - Effect: "Allow"
# Action:
# - "s3:ListBucket"
# Resource: { "Fn::Join" : ["", ["arn:aws:s3:::", { "Ref" : "ServerlessDeploymentBucket" } ] ] }
# - Effect: "Allow"
# Action:
# - "s3:PutObject"
# Resource:
# Fn::Join:
# - ""
# - - "arn:aws:s3:::"
# - "Ref" : "ServerlessDeploymentBucket"
# you can add packaging information here
package:
# include:
# - include-me.java
# exclude:
# - exclude-me.java
artifact: target/scala-2.11/hello.jar
functions:
hello:
handler: hello.Handler
# you can add any of the following events
# events:
# - http:
# path: users/create
# method: get
# - s3: ${env:bucket}
# - schedule: rate(10 minutes)
# - sns: greeter-topic
# you can add CloudFormation resource templates here
#resources:
# Resources:
# NewResource:
# Type: AWS::S3::Bucket
# Properties:
# BucketName: my-new-bucket
# Outputs:
# NewOutput:
# Description: "Description for the output"
# Value: "Some output value"

View File

@ -0,0 +1,11 @@
package hello
import com.amazonaws.services.lambda.runtime.{Context, RequestHandler}
class Handler extends RequestHandler[Request, Response] {
def handleRequest(input: Request, context: Context): Response = {
return new Response("Go Serverless v1.0! Your function executed successfully!", input)
}
}

View File

@ -0,0 +1,7 @@
package hello
import scala.beans.BeanProperty
class Request(@BeanProperty var key1: String, @BeanProperty var key2: String, @BeanProperty var key3: String) {
def this() = this("", "", "")
}

View File

@ -0,0 +1,5 @@
package hello
import scala.beans.BeanProperty
case class Response(@BeanProperty message: String, @BeanProperty request: Request)

View File

@ -48,6 +48,23 @@ describe('Create', () => {
expect(() => create.create()).to.throw(Error);
});
it('should overwrite the name for the service if user passed name', () => {
const cwd = process.cwd();
fse.mkdirsSync(tmpDir);
process.chdir(tmpDir);
create.options.template = 'aws-nodejs';
create.options.name = 'my_service';
return create.create().then(() =>
create.serverless.yamlParser.parse(
path.join(tmpDir, 'serverless.yml')
).then((obj) => {
expect(obj.service).to.equal('my_service');
process.chdir(cwd);
})
);
});
it('should set servicePath based on cwd', () => {
const cwd = process.cwd();
fse.mkdirsSync(tmpDir);
@ -157,6 +174,36 @@ describe('Create', () => {
});
});
it('should generate scaffolding for "aws-scala-sbt" template', () => {
const cwd = process.cwd();
fse.mkdirsSync(tmpDir);
process.chdir(tmpDir);
create.options.template = 'aws-scala-sbt';
return create.create().then(() => {
expect(create.serverless.utils.fileExistsSync(path.join(tmpDir, 'serverless.yml')))
.to.be.equal(true);
expect(create.serverless.utils.fileExistsSync(path.join(tmpDir, 'event.json')))
.to.be.equal(true);
expect(create.serverless.utils.fileExistsSync(path.join(tmpDir, 'build.sbt')))
.to.be.equal(true);
expect(create.serverless.utils.fileExistsSync(path.join(tmpDir, 'src', 'main', 'scala',
'hello', 'Handler.scala'
)))
.to.be.equal(true);
expect(create.serverless.utils.fileExistsSync(path.join(tmpDir, 'src', 'main', 'scala',
'hello', 'Request.scala'
)))
.to.be.equal(true);
expect(create.serverless.utils.fileExistsSync(path.join(tmpDir, 'src', 'main', 'scala',
'hello', 'Response.scala'
)))
.to.be.equal(true);
process.chdir(cwd);
});
});
// this test should live here because of process.cwd() which might cause trouble when using
// nested dirs like its done here
it('should create a renamed service in the directory if using the "path" option', () => {
@ -165,6 +212,7 @@ describe('Create', () => {
process.chdir(tmpDir);
create.options.path = 'my-new-service';
create.options.name = null;
// using the nodejs template (this test is completely be independent from the template)
create.options.template = 'aws-nodejs';
@ -187,5 +235,36 @@ describe('Create', () => {
process.chdir(cwd);
});
});
it('should create a custom renamed service in the directory if using ' +
'the "path" and "name" option', () => {
const cwd = process.cwd();
fse.mkdirsSync(tmpDir);
process.chdir(tmpDir);
create.options.path = 'my-new-service';
create.options.name = 'my-custom-new-service';
// using the nodejs template (this test is completely be independent from the template)
create.options.template = 'aws-nodejs';
return create.create().then(() => {
const serviceDir = path.join(tmpDir, create.options.path);
// check if files are created in the correct directory
expect(create.serverless.utils.fileExistsSync(
path.join(serviceDir, 'serverless.yml'))).to.be.equal(true);
expect(create.serverless.utils.fileExistsSync(
path.join(serviceDir, 'handler.js'))).to.be.equal(true);
// check if the service was renamed
const serverlessYmlfileContent = fse
.readFileSync(path.join(serviceDir, 'serverless.yml')).toString();
expect((/service: my-custom-new-service/).test(serverlessYmlfileContent)).to.equal(true);
process.chdir(cwd);
});
});
});
});

View File

@ -2,6 +2,7 @@
const expect = require('chai').expect;
const fs = require('fs');
const os = require('os');
const path = require('path');
const JsZip = require('jszip');
const _ = require('lodash');
@ -140,13 +141,19 @@ describe('#zipService()', () => {
}).then(unzippedData => {
const unzippedFileData = unzippedData.files;
// binary file is set with chmod of 777
expect(unzippedFileData['bin/some-binary'].unixPermissions)
.to.equal(Math.pow(2, 15) + 777);
if (os.platform() === 'win32') {
// chmod does not work right on windows. this is better than nothing?
expect(unzippedFileData['bin/some-binary'].unixPermissions)
.to.not.equal(unzippedFileData['bin/read-only'].unixPermissions);
} else {
// binary file is set with chmod of 777
expect(unzippedFileData['bin/some-binary'].unixPermissions)
.to.equal(Math.pow(2, 15) + 777);
// read only file is set with chmod of 444
expect(unzippedFileData['bin/read-only'].unixPermissions)
.to.equal(Math.pow(2, 15) + 444);
// read only file is set with chmod of 444
expect(unzippedFileData['bin/read-only'].unixPermissions)
.to.equal(Math.pow(2, 15) + 444);
}
});
});

View File

@ -43,7 +43,7 @@ class Tracking {
}
if (this.options.disable && !this.options.enable) {
fs.writeFileSync(path.join(serverlessPath, trackingFileName),
'Keep this file to enable tracking');
'Keep this file to disable tracking');
this.serverless.cli.log('Tracking successfully disabled');
}
}

68
npm-shrinkwrap.json generated
View File

@ -1,6 +1,6 @@
{
"name": "serverless",
"version": "1.0.0-beta.2",
"version": "1.0.0-rc.2",
"dependencies": {
"abbrev": {
"version": "1.0.9",
@ -114,6 +114,11 @@
"from": "async@>=1.5.2 <2.0.0",
"resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz"
},
"asynckit": {
"version": "0.4.0",
"from": "asynckit@>=0.4.0 <0.5.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz"
},
"aws-sdk": {
"version": "2.5.3",
"from": "aws-sdk@>=2.3.17 <3.0.0",
@ -136,7 +141,7 @@
},
"bl": {
"version": "1.1.2",
"from": "bl@>=1.0.0 <2.0.0",
"from": "bl@>=1.1.2 <1.2.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-1.1.2.tgz",
"dependencies": {
"readable-stream": {
@ -178,7 +183,7 @@
},
"builtin-modules": {
"version": "1.1.1",
"from": "builtin-modules@>=1.0.0 <2.0.0",
"from": "builtin-modules@>=1.1.1 <2.0.0",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz"
},
"caller-id": {
@ -213,7 +218,7 @@
},
"chalk": {
"version": "1.1.3",
"from": "chalk@>=1.1.1 <2.0.0",
"from": "chalk@>=1.1.0 <2.0.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz"
},
"circular-json": {
@ -327,12 +332,12 @@
},
"debug": {
"version": "2.2.0",
"from": "debug@>=2.0.0 <3.0.0",
"from": "debug@>=2.2.0 <3.0.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz"
},
"decamelize": {
"version": "1.2.0",
"from": "decamelize@>=1.1.1 <2.0.0",
"from": "decamelize@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz"
},
"deep-eql": {
@ -522,9 +527,9 @@
"resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz"
},
"file-entry-cache": {
"version": "1.3.1",
"from": "file-entry-cache@>=1.3.1 <2.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-1.3.1.tgz"
"version": "2.0.0",
"from": "file-entry-cache@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz"
},
"find-up": {
"version": "1.1.2",
@ -558,7 +563,7 @@
},
"fs-extra": {
"version": "0.26.7",
"from": "fs-extra@>=0.26.7 <0.27.0",
"from": "fs-extra@>=0.26.4 <0.27.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.26.7.tgz"
},
"fs.realpath": {
@ -637,7 +642,7 @@
},
"har-validator": {
"version": "2.0.6",
"from": "har-validator@>=2.0.6 <2.1.0",
"from": "har-validator@>=2.0.2 <2.1.0",
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz"
},
"has-ansi": {
@ -652,7 +657,7 @@
},
"hawk": {
"version": "3.1.3",
"from": "hawk@>=3.1.3 <3.2.0",
"from": "hawk@>=3.1.0 <3.2.0",
"resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz"
},
"hoek": {
@ -807,7 +812,7 @@
},
"js-yaml": {
"version": "3.6.1",
"from": "js-yaml@>=3.6.1 <4.0.0",
"from": "js-yaml@>=3.5.5 <4.0.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.6.1.tgz"
},
"jsbn": {
@ -890,11 +895,6 @@
"from": "lcid@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz"
},
"lcov-parse": {
"version": "0.0.6",
"from": "lcov-parse@0.0.6",
"resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-0.0.6.tgz"
},
"levn": {
"version": "0.3.0",
"from": "levn@>=0.3.0 <0.4.0",
@ -985,11 +985,6 @@
"from": "lodash.keys@>=3.0.0 <4.0.0",
"resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz"
},
"log-driver": {
"version": "1.2.4",
"from": "log-driver@1.2.4",
"resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.4.tgz"
},
"lolex": {
"version": "1.3.2",
"from": "lolex@1.3.2",
@ -1074,7 +1069,7 @@
},
"node-uuid": {
"version": "1.4.7",
"from": "node-uuid@>=1.4.7 <1.5.0",
"from": "node-uuid@>=1.4.2 <2.0.0",
"resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.7.tgz"
},
"nopt": {
@ -1099,12 +1094,12 @@
},
"oauth-sign": {
"version": "0.8.2",
"from": "oauth-sign@>=0.8.1 <0.9.0",
"from": "oauth-sign@>=0.8.0 <0.9.0",
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz"
},
"object-assign": {
"version": "4.1.0",
"from": "object-assign@>=4.1.0 <5.0.0",
"from": "object-assign@>=4.0.1 <5.0.0",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.0.tgz"
},
"once": {
@ -1353,7 +1348,7 @@
},
"semver-regex": {
"version": "1.0.0",
"from": "semver-regex@>=1.0.0 <2.0.0",
"from": "semver-regex@latest",
"resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-1.0.0.tgz"
},
"set-blocking": {
@ -1425,7 +1420,7 @@
},
"stack-trace": {
"version": "0.0.9",
"from": "stack-trace@>=0.0.7 <0.1.0",
"from": "stack-trace@>=0.0.0 <0.1.0",
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.9.tgz"
},
"string_decoder": {
@ -1592,16 +1587,6 @@
"from": "uglify-to-browserify@>=1.0.0 <1.1.0",
"resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz"
},
"underscore": {
"version": "1.7.0",
"from": "underscore@>=1.7.0 <1.8.0",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz"
},
"underscore.string": {
"version": "2.4.0",
"from": "underscore.string@>=2.4.0 <2.5.0",
"resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.4.0.tgz"
},
"uri-js": {
"version": "2.1.1",
"from": "uri-js@>=2.1.1 <3.0.0",
@ -1622,6 +1607,11 @@
"from": "util-deprecate@>=1.0.1 <1.1.0",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
},
"uuid": {
"version": "2.0.2",
"from": "uuid@latest",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.2.tgz"
},
"validate-npm-package-license": {
"version": "3.0.1",
"from": "validate-npm-package-license@>=3.0.1 <4.0.0",
@ -1649,7 +1639,7 @@
},
"wordwrap": {
"version": "1.0.0",
"from": "wordwrap@>=1.0.0 <1.1.0",
"from": "wordwrap@>=0.0.2",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz"
},
"wrap-ansi": {

View File

@ -1,6 +1,6 @@
{
"name": "serverless",
"version": "1.0.0-rc.1",
"version": "1.0.0-rc.2",
"engines": {
"node": ">=4.0"
},
@ -39,7 +39,7 @@
"serverless-run-python-handler": "./bin/serverless-run-python-handler"
},
"scripts": {
"test": "istanbul cover _mocha tests/all -- -R spec --recursive",
"test": "istanbul cover node_modules/mocha/bin/_mocha tests/all -- -R spec --recursive",
"lint": "eslint .",
"integration-test": "mocha tests/integration_test"
},
@ -77,6 +77,7 @@
"replaceall": "^0.1.6",
"semver-regex": "^1.0.0",
"shelljs": "^0.6.0",
"traverse": "^0.6.6"
"traverse": "^0.6.6",
"uuid": "^2.0.2"
}
}

View File

@ -6,13 +6,18 @@
const expect = require('chai').expect;
const CLI = require('../../lib/classes/CLI');
const os = require('os');
const fse = require('fs-extra');
const exec = require('child_process').exec;
const path = require('path');
const Serverless = require('../../lib/Serverless');
const testUtils = require('../../tests/utils');
describe('CLI', () => {
let cli;
let serverless;
beforeEach(() => {
beforeEach(function () { // eslint-disable-line prefer-arrow-callback
serverless = new Serverless({});
});
@ -262,4 +267,55 @@ describe('CLI', () => {
expect(inputToBeProcessed).to.deep.equal(expectedObject);
});
});
describe('integration tests', () => {
before(function () {
const tmpDir = testUtils.getTmpDirPath();
this.cwd = process.cwd();
fse.mkdirSync(tmpDir);
process.chdir(tmpDir);
serverless = new Serverless();
serverless.init();
// Cannot rely on shebang in severless.js to invoke script using NodeJS on Windows.
const execPrefix = os.platform() === 'win32' ? 'node ' : '';
this.serverlessExec = execPrefix + path.join(serverless.config.serverlessPath,
'..', 'bin', 'serverless');
});
after(function () { // eslint-disable-line prefer-arrow-callback
process.chdir(this.cwd);
});
it('prints general --help to stdout', function (done) {
this.timeout(10000);
exec(`${this.serverlessExec} --help`, (err, stdout) => {
if (err) {
done(err);
return;
}
expect(stdout).to.contain('contextual help');
done();
});
});
it('prints command --help to stdout', function (done) {
this.timeout(10000);
exec(`${this.serverlessExec} deploy --help`, (err, stdout) => {
if (err) {
done(err);
return;
}
expect(stdout).to.contain('deploy');
expect(stdout).to.contain('--stage');
done();
});
});
});
});

View File

@ -10,6 +10,7 @@ const fse = require('fs-extra');
const execSync = require('child_process').execSync;
const mockRequire = require('mock-require');
const testUtils = require('../../tests/utils');
const os = require('os');
describe('PluginManager', () => {
let pluginManager;
@ -187,7 +188,7 @@ describe('PluginManager', () => {
}
}
beforeEach(() => {
beforeEach(function () { // eslint-disable-line prefer-arrow-callback
serverless = new Serverless();
pluginManager = new PluginManager(serverless);
});
@ -348,7 +349,7 @@ describe('PluginManager', () => {
});
describe('#loadAllPlugins()', () => {
beforeEach(() => {
beforeEach(function () { // eslint-disable-line prefer-arrow-callback
mockRequire('ServicePluginMock1', ServicePluginMock1);
mockRequire('ServicePluginMock2', ServicePluginMock2);
});
@ -395,7 +396,7 @@ describe('PluginManager', () => {
expect(pluginManager.plugins[2]).to.be.instanceof(ServicePluginMock2);
});
afterEach(() => {
afterEach(function () { // eslint-disable-line prefer-arrow-callback
mockRequire.stop('ServicePluginMock1');
mockRequire.stop('ServicePluginMock2');
});
@ -410,7 +411,7 @@ describe('PluginManager', () => {
});
describe('#loadServicePlugins()', () => {
beforeEach(() => {
beforeEach(function () { // eslint-disable-line prefer-arrow-callback
mockRequire('ServicePluginMock1', ServicePluginMock1);
mockRequire('ServicePluginMock2', ServicePluginMock2);
});
@ -426,7 +427,7 @@ describe('PluginManager', () => {
expect(pluginManager.plugins).to.contain(servicePluginMock2);
});
afterEach(() => {
afterEach(function () { // eslint-disable-line prefer-arrow-callback
mockRequire.stop('ServicePluginMock1');
mockRequire.stop('ServicePluginMock2');
});
@ -442,7 +443,7 @@ describe('PluginManager', () => {
});
describe('#getEvents()', () => {
beforeEach(() => {
beforeEach(function () { // eslint-disable-line prefer-arrow-callback
const synchronousPluginMockInstance = new SynchronousPluginMock();
pluginManager.loadCommands(synchronousPluginMockInstance);
});
@ -480,7 +481,7 @@ describe('PluginManager', () => {
});
describe('#getPlugins()', () => {
beforeEach(() => {
beforeEach(function () { // eslint-disable-line prefer-arrow-callback
mockRequire('ServicePluginMock1', ServicePluginMock1);
mockRequire('ServicePluginMock2', ServicePluginMock2);
});
@ -493,7 +494,7 @@ describe('PluginManager', () => {
expect(pluginManager.getPlugins()[1]).to.be.instanceof(ServicePluginMock2);
});
afterEach(() => {
afterEach(function () { // eslint-disable-line prefer-arrow-callback
mockRequire.stop('ServicePluginMock1');
mockRequire.stop('ServicePluginMock2');
});
@ -691,7 +692,7 @@ describe('PluginManager', () => {
});
describe('when using a synchronous hook function', () => {
beforeEach(() => {
beforeEach(function () { // eslint-disable-line prefer-arrow-callback
pluginManager.addPlugin(SynchronousPluginMock);
});
@ -715,7 +716,7 @@ describe('PluginManager', () => {
});
describe('when using a promise based hook function', () => {
beforeEach(() => {
beforeEach(function () { // eslint-disable-line prefer-arrow-callback
pluginManager.addPlugin(PromisePluginMock);
});
@ -739,7 +740,7 @@ describe('PluginManager', () => {
});
describe('when using provider specific plugins', () => {
beforeEach(() => {
beforeEach(function () { // eslint-disable-line prefer-arrow-callback
pluginManager.setProvider('provider1');
pluginManager.addPlugin(Provider1PluginMock);
@ -762,11 +763,15 @@ describe('PluginManager', () => {
});
});
describe('Plugin/CLI integration', () => {
it('Plugin/CLI integration', function () {
this.timeout(10000);
const serverlessInstance = new Serverless();
serverlessInstance.init();
const serverlessExec = path.join(serverlessInstance.config.serverlessPath,
'..', 'bin', 'serverless');
// Cannot rely on shebang in severless.js to invoke script using NodeJS on Windows.
const execPrefix = os.platform() === 'win32' ? 'node ' : '';
const serverlessExec = execPrefix + path.join(serverlessInstance.config.serverlessPath,
'..', 'bin', 'serverless');
const tmpDir = testUtils.getTmpDirPath();
fse.mkdirSync(tmpDir);
const cwd = process.cwd();

View File

@ -1,12 +1,12 @@
'use strict';
const path = require('path');
const os = require('os');
const YAML = require('js-yaml');
const expect = require('chai').expect;
const Service = require('../../lib/classes/Service');
const Utils = require('../../lib/classes/Utils');
const Serverless = require('../../lib/Serverless');
const testUtils = require('../../tests/utils');
describe('Service', () => {
describe('#constructor()', () => {
@ -101,6 +101,11 @@ describe('Service', () => {
describe('#load()', () => {
let serviceInstance;
let tmpDirPath;
beforeEach(() => {
tmpDirPath = testUtils.getTmpDirPath();
});
it('should resolve if no servicePath is found', () => {
const serverless = new Serverless();
@ -111,7 +116,6 @@ describe('Service', () => {
it('should load from filesystem', () => {
const SUtils = new Utils();
const tmpDirPath = path.join(os.tmpdir(), (new Date()).getTime().toString());
const serverlessYml = {
service: 'new-service',
provider: 'aws',
@ -164,7 +168,6 @@ describe('Service', () => {
it('should make sure function name contains the default stage', () => {
const SUtils = new Utils();
const tmpDirPath = path.join(os.tmpdir(), (new Date()).getTime().toString());
const serverlessYml = {
service: 'new-service',
provider: 'aws',
@ -204,9 +207,39 @@ describe('Service', () => {
});
});
it('should support Serverless file with a non-aws provider', () => {
const SUtils = new Utils();
const serverlessYaml = {
service: 'my-service',
provider: 'ibm',
functions: {
functionA: {
name: 'customFunctionName',
},
},
};
SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yaml'),
YAML.dump(serverlessYaml));
const serverless = new Serverless({ servicePath: tmpDirPath });
serviceInstance = new Service(serverless);
return serviceInstance.load().then(() => {
const expectedFunc = {
functionA: {
name: 'customFunctionName',
events: [],
},
};
expect(serviceInstance.service).to.be.equal('my-service');
expect(serviceInstance.provider.name).to.deep.equal('ibm');
expect(serviceInstance.functions).to.deep.equal(expectedFunc);
});
});
it('should support Serverless file with a .yaml extension', () => {
const SUtils = new Utils();
const tmpDirPath = path.join(os.tmpdir(), (new Date()).getTime().toString());
const serverlessYaml = {
service: 'my-service',
provider: 'aws',
@ -238,7 +271,6 @@ describe('Service', () => {
it('should support Serverless file with a .yml extension', () => {
const SUtils = new Utils();
const tmpDirPath = path.join(os.tmpdir(), (new Date()).getTime().toString());
const serverlessYml = {
service: 'my-service',
provider: 'aws',
@ -268,7 +300,6 @@ describe('Service', () => {
it('should throw error if service property is missing', () => {
const SUtils = new Utils();
const tmpDirPath = path.join(os.tmpdir(), (new Date()).getTime().toString());
const serverlessYml = {
provider: 'aws',
functions: {},
@ -290,7 +321,6 @@ describe('Service', () => {
it('should throw error if provider property is missing', () => {
const SUtils = new Utils();
const tmpDirPath = path.join(os.tmpdir(), (new Date()).getTime().toString());
const serverlessYml = {
service: 'service-name',
functions: {},
@ -312,7 +342,6 @@ describe('Service', () => {
it('should throw error if functions property is missing', () => {
const SUtils = new Utils();
const tmpDirPath = path.join(os.tmpdir(), (new Date()).getTime().toString());
const serverlessYml = {
service: 'service-name',
provider: 'aws',
@ -334,7 +363,6 @@ describe('Service', () => {
it("should throw error if a function's event is not an array", () => {
const SUtils = new Utils();
const tmpDirPath = path.join(os.tmpdir(), (new Date()).getTime().toString());
const serverlessYml = {
service: 'service-name',
provider: 'aws',
@ -361,7 +389,6 @@ describe('Service', () => {
it('should throw error if provider property is invalid', () => {
const SUtils = new Utils();
const tmpDirPath = path.join(os.tmpdir(), (new Date()).getTime().toString());
const serverlessYml = {
service: 'service-name',
provider: 'invalid',

View File

@ -3,6 +3,8 @@
const path = require('path');
const os = require('os');
const expect = require('chai').expect;
const fse = require('fs-extra');
const fs = require('fs');
const Serverless = require('../../lib/Serverless');
const testUtils = require('../../tests/utils');
@ -107,6 +109,34 @@ describe('Utils', () => {
expect(obj.foo).to.equal('bar');
});
it('should read a filename extension .yml', () => {
const tmpFilePath = testUtils.getTmpFilePath('anything.yml');
serverless.utils.writeFileSync(tmpFilePath, { foo: 'bar' });
const obj = serverless.utils.readFileSync(tmpFilePath);
expect(obj.foo).to.equal('bar');
});
it('should read a filename extension .yaml', () => {
const tmpFilePath = testUtils.getTmpFilePath('anything.yaml');
serverless.utils.writeFileSync(tmpFilePath, { foo: 'bar' });
const obj = serverless.utils.readFileSync(tmpFilePath);
expect(obj.foo).to.equal('bar');
});
it('should throw YAMLException with filename if yml file is invalid format', () => {
const tmpFilePath = testUtils.getTmpFilePath('invalid.yml');
serverless.utils.writeFileSync(tmpFilePath, ': a');
expect(() => {
serverless.utils.readFileSync(tmpFilePath);
}).to.throw(new RegExp('YAMLException:.*invalid.yml'));
});
});
describe('#readFile()', () => {
@ -228,4 +258,61 @@ describe('Utils', () => {
process.chdir(testDir);
});
});
describe('#track()', () => {
let serverlessPath;
beforeEach(() => {
serverless.init();
// create a new tmpDir for the serverlessPath
const tmpDirPath = testUtils.getTmpDirPath();
fse.mkdirsSync(tmpDirPath);
serverlessPath = tmpDirPath;
serverless.config.serverlessPath = tmpDirPath;
// add some mock data to the serverless service object
serverless.service.functions = {
foo: {
memorySize: 47,
timeout: 11,
events: [
{
http: 'GET foo',
},
],
},
bar: {
events: [
{
http: 'GET foo',
s3: 'someBucketName',
},
],
},
};
});
it('should create a new file with a tracking id if not found', () => {
const trackingIdFilePath = path.join(serverlessPath, 'tracking-id');
return serverless.utils.track(serverless).then(() => {
expect(fs.readFileSync(trackingIdFilePath).toString().length).to.be.above(1);
});
});
it('should re-use an existing file which contains the tracking id if found', () => {
const trackingIdFilePath = path.join(serverlessPath, 'tracking-id');
const trackingId = 'some-tracking-id';
// create a new file with a tracking id
fse.ensureFileSync(trackingIdFilePath);
fs.writeFileSync(trackingIdFilePath, trackingId);
return serverless.utils.track(serverless).then(() => {
expect(fs.readFileSync(trackingIdFilePath).toString()).to.be.equal(trackingId);
});
});
});
});

View File

@ -19,7 +19,7 @@ serverless create --template $template
echo "Overwriting Service Name"
sed -i.bak s/${template}/sls-test-$template-$RANDOM/g $template_folder/serverless.yml
echo "Running Compose build for Teamplate"
echo "Running Compose build for Template"
docker-compose build $template
if [ ! -z "$2" ]

View File

@ -10,5 +10,6 @@ function integration-test {
integration-test aws-java-gradle build
integration-test aws-java-maven mvn package
integration-test aws-scala-sbt sbt assembly
integration-test aws-nodejs
integration-test aws-python

View File

@ -4,7 +4,8 @@ const os = require('os');
const path = require('path');
const crypto = require('crypto');
const getTmpDirPath = () => path.join(os.tmpdir(), crypto.randomBytes(8).toString('hex'));
const getTmpDirPath = () => path.join(os.tmpdir(),
'tmpdirs-serverless', 'serverless', crypto.randomBytes(8).toString('hex'));
const getTmpFilePath = (fileName) => path.join(getTmpDirPath(), fileName);