diff --git a/CHANGELOG.md b/CHANGELOG.md index ba8186065..995833626 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# 1.16.1 (26.06.2017) +- CI/CD fix for the Serverless Platform - #3829 + +## Meta +- [Comparison since last release](https://github.com/serverless/serverless/compare/v1.16.0...v1.16.1) + + # 1.16.0 (21.06.2017) - Added support for usage plans to APIG - #3819 - Optmizied packaging to exclude dev dependencies - #3737 diff --git a/README.md b/README.md index 94a9c1fca..535802fe5 100644 --- a/README.md +++ b/README.md @@ -312,6 +312,7 @@ These consultants use the Serverless Framework and can help you build your serve * [Craftship](https://craftship.io) * [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) ---- diff --git a/bin/serverless b/bin/serverless index 5adf6bfd4..fb63b7383 100755 --- a/bin/serverless +++ b/bin/serverless @@ -6,6 +6,8 @@ const autocomplete = require('../lib/utils/autocomplete'); const BbPromise = require('bluebird'); const logError = require('../lib/classes/Error').logError; +Error.stackTraceLimit = Infinity; + BbPromise.config({ longStackTraces: true, }); diff --git a/docker-compose.yml b/docker-compose.yml index 4649a8550..227462503 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -47,6 +47,10 @@ services: image: microsoft/dotnet:1.0.4-sdk volumes: - ./tmp/serverless-integration-test-aws-csharp:/app + aws-fsharp: + image: microsoft/dotnet:1.0.4-sdk + volumes: + - ./tmp/serverless-integration-test-aws-fsharp:/app google-nodejs: image: node:6.9.1 volumes: diff --git a/docs/providers/aws/cli-reference/create.md b/docs/providers/aws/cli-reference/create.md index 4cbca6f09..6d2a2296c 100644 --- a/docs/providers/aws/cli-reference/create.md +++ b/docs/providers/aws/cli-reference/create.md @@ -48,6 +48,7 @@ Most commonly used templates: - aws-java-gradle - aws-scala-sbt - aws-csharp +- aws-fsharp - plugin ## Examples diff --git a/docs/providers/aws/cli-reference/logs.md b/docs/providers/aws/cli-reference/logs.md index d57ecbd0a..f0ab9994f 100644 --- a/docs/providers/aws/cli-reference/logs.md +++ b/docs/providers/aws/cli-reference/logs.md @@ -16,6 +16,9 @@ Lets you watch the logs of a specific function. ```bash serverless logs -f hello + +# Optionally tail the logs with -t +serverless logs -f hello -t ``` ## Options diff --git a/docs/providers/aws/events/apigateway.md b/docs/providers/aws/events/apigateway.md index 81a118a32..79f7a7c0f 100644 --- a/docs/providers/aws/events/apigateway.md +++ b/docs/providers/aws/events/apigateway.md @@ -265,7 +265,12 @@ functions: arn: arn:aws:cognito-idp:us-east-1:xxx:userpool/us-east-1_ZZZ ``` -By default the `sub` claim will be exposed in `events.cognitoPoolClaims`, you can add extra claims like so: +If you are using the default `lambda-proxy` integration, your attributes will be +exposed at `event.requestContext.authorizer.claims`. + +If you want control more control over which attributes are exposed as claims you +can switch to `integration: lambda` and add the following configuration. The +claims will be exposed at `events.cognitoPoolClaims`. ```yml functions: @@ -283,16 +288,12 @@ functions: - nickname ``` -Note: Since claims must be explicitly listed to be exposed, you must use `integration: lambda` integration type to access any claims. - ### Catching Exceptions In Your Lambda Function In case an exception is thrown in your lambda function AWS will send an error message with `Process exited before completing request`. This will be caught by the regular expression for the 500 HTTP status and the 500 status will be returned. ### Setting API keys for your Rest API -**Note:** Due to a CloudFormation restriction you need to wire up API Keys and usage plans manually in the AWS console. - You can specify a list of API keys to be used by your service Rest API by adding an `apiKeys` array property to the `provider` object in `serverless.yml`. You'll also need to explicitly specify which endpoints are `private` and require one of the api keys to be included in the request by adding a `private` boolean property to the `http` event object you @@ -354,7 +355,6 @@ functions: url: true headers: foo: false - bar: true paths: bar: false ``` diff --git a/docs/providers/aws/examples/hello-world/fsharp/Handler.fs b/docs/providers/aws/examples/hello-world/fsharp/Handler.fs new file mode 100644 index 000000000..b9e1806fe --- /dev/null +++ b/docs/providers/aws/examples/hello-world/fsharp/Handler.fs @@ -0,0 +1,17 @@ +namespace AwsDotnetFsharp +open Amazon.Lambda.Core + +[)>] +do () + +type Request = { Key1 : string; Key2 : string; Key3 : string } +type Response = { Message : string; Request : Request } + +module Handler = + open System + open System.IO + open System.Text + + let hello(request:Request) = + { Message="Go Serverless v1.0! Your function executed successfully!" + Request=request } \ No newline at end of file diff --git a/docs/providers/aws/examples/hello-world/fsharp/README.md b/docs/providers/aws/examples/hello-world/fsharp/README.md new file mode 100644 index 000000000..7c7de6acb --- /dev/null +++ b/docs/providers/aws/examples/hello-world/fsharp/README.md @@ -0,0 +1,37 @@ + + +# Hello World F# Example + +## Prerequisites + +* Make sure `serverless` is installed. [See installation guide](../../../guide/installation.md). +* [.Net Core 1.0.1 SDK](https://www.microsoft.com/net/download/core) + * 1.1 isn't currently supported by AWS Lambda +* [NodeJS v4 or higher](https://nodejs.org/en/) +* An AWS Account + +## Build and Package + +From the root of this directory, run `build.cmd`, or `build.sh` if on Linux / Mac. + +This will produce your deployment package at `bin/release/netcoreapp1.0/deploy-package.zip`. + +## Deployment and Invocation + +Once packaged, you can follow [these instructions](https://github.com/serverless/serverless#quick-start) to deploy and remotely invoke the function on AWS Lambda. + +In short the commands you will need to run are (from the root of this directory): + +``` +serverless config credentials --provider aws --key {YourAwsAccessKey} --secret {YourAwsSecret} +serverless deploy -v +serverless invoke -f hello -l +serverless remove +``` + +By default this template deploys to us-east-1, you can change that in "serverless.yml" under the `region: us-east-1` key. \ No newline at end of file diff --git a/docs/providers/aws/examples/hello-world/fsharp/aws-fsharp.fsproj b/docs/providers/aws/examples/hello-world/fsharp/aws-fsharp.fsproj new file mode 100644 index 000000000..f3a1aafd4 --- /dev/null +++ b/docs/providers/aws/examples/hello-world/fsharp/aws-fsharp.fsproj @@ -0,0 +1,24 @@ + + + + netcoreapp1.0 + FsharpHandlers + aws-fsharp + + + + + + + + + + + + + + + + + + diff --git a/docs/providers/aws/examples/hello-world/fsharp/serverless.yml b/docs/providers/aws/examples/hello-world/fsharp/serverless.yml new file mode 100644 index 000000000..c90e1089a --- /dev/null +++ b/docs/providers/aws/examples/hello-world/fsharp/serverless.yml @@ -0,0 +1,87 @@ +# 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-fsharp # NOTE: update this with your service name + +# You can pin your service to only deploy with a specific Serverless version +# Check out our docs for more details +# frameworkVersion: "=X.X.X" + +provider: + name: aws + runtime: dotnetcore1.0 + +# 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 define service wide environment variables here +# environment: +# variable1: value1 + +# you can add packaging information here +package: + artifact: bin/release/netcoreapp1.0/deploy-package.zip +# exclude: +# - exclude-me.js +# - exclude-me-dir/** + +functions: + hello: + handler: FsharpHandlers::AwsDotnetFsharp.Handler::hello + +# The following are a few example events you can configure +# NOTE: Please make sure to change your handler code to work with those events +# Check the event documentation for details +# events: +# - http: +# path: users/create +# method: get +# - s3: ${env:BUCKET} +# - schedule: rate(10 minutes) +# - sns: greeter-topic +# - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000 +# - alexaSkill +# - iot: +# sql: "SELECT * FROM 'some_topic'" + +# Define function environment variables here +# environment: +# variable2: value2 + +# 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" diff --git a/docs/providers/aws/guide/services.md b/docs/providers/aws/guide/services.md index ee776f9e2..dd96c4c24 100644 --- a/docs/providers/aws/guide/services.md +++ b/docs/providers/aws/guide/services.md @@ -58,6 +58,7 @@ Here are the available runtimes for AWS Lambda: * aws-java-maven * aws-scala-sbt * aws-csharp +* aws-fsharp Check out the [create command docs](../cli-reference/create) for all the details and options. diff --git a/docs/providers/google/events/event.md b/docs/providers/google/events/event.md index a3e86159d..b96c5ec2e 100644 --- a/docs/providers/google/events/event.md +++ b/docs/providers/google/events/event.md @@ -26,7 +26,7 @@ functions: handler: pubSub events: - event: - eventType: providers/cloud.pubsub/eventTypes/topics.publish + eventType: providers/cloud.pubsub/eventTypes/topic.publish resource: projects/*/topics/my-topic ``` diff --git a/docs/providers/openwhisk/guide/web-actions.md b/docs/providers/openwhisk/guide/web-actions.md index 599c0ce43..86d366e8c 100644 --- a/docs/providers/openwhisk/guide/web-actions.md +++ b/docs/providers/openwhisk/guide/web-actions.md @@ -12,7 +12,7 @@ layout: Doc # OpenWhisk - Web Actions -Functions can be turned into ["*web actions*"](https://github.com/openwhisk/openwhisk/blob/master/docs/actions.md) which return HTTP content without use of an API Gateway. This feature is enabled by setting an annotation (`web-export`) in the configuration file. +Functions can be turned into ["*web actions*"](https://github.com/apache/incubator-openwhisk/blob/master/docs/actions.md) which return HTTP content without use of an API Gateway. This feature is enabled by setting an annotation (`web-export`) in the configuration file. ``` functions: @@ -73,4 +73,4 @@ Functions can access request parameters using the following environment variable Full details on this new feature are available in this [blog post](https://medium.com/openwhisk/serverless-http-handlers-with-openwhisk-90a986cc7cdd#.2x09176m8). -**\*IMPORTANT: [Web Actions](https://github.com/openwhisk/openwhisk/blob/master/docs/actions.md) is currently experimental and may be subject to breaking changes.*** +**\*IMPORTANT: [Web Actions](https://github.com/apache/incubator-openwhisk/blob/master/docs/actions.md) is currently experimental and may be subject to breaking changes.*** diff --git a/lib/classes/CLI.js b/lib/classes/CLI.js index 05698c36d..594d2279b 100644 --- a/lib/classes/CLI.js +++ b/lib/classes/CLI.js @@ -56,7 +56,11 @@ class CLI { if ((commands.length === 0) || (commands.length === 0 && (options.help || options.h)) || (commands.length === 1 && (commands.indexOf('help') > -1))) { - this.generateMainHelp(); + if (options.verbose || options.v) { + this.generateVerboseHelp(); + } else { + this.generateMainHelp(); + } return true; } @@ -68,18 +72,19 @@ class CLI { return false; } - displayCommandUsage(commandObject, command) { + displayCommandUsage(commandObject, command, indents) { const dotsLength = 30; // check if command has lifecycleEvents (can be executed) if (commandObject.lifecycleEvents) { const usage = commandObject.usage; const dots = _.repeat('.', dotsLength - command.length); - this.consoleLog(`${chalk.yellow(command)} ${chalk.dim(dots)} ${usage}`); + const indent = _.repeat(' ', indents || 0); + this.consoleLog(`${indent}${chalk.yellow(command)} ${chalk.dim(dots)} ${usage}`); } _.forEach(commandObject.commands, (subcommandObject, subcommand) => { - this.displayCommandUsage(subcommandObject, `${command} ${subcommand}`); + this.displayCommandUsage(subcommandObject, `${command} ${subcommand}`, indents); }); } @@ -120,13 +125,24 @@ class CLI { this.consoleLog(chalk.yellow.underline('Commands')); this.consoleLog(chalk.dim('* Serverless documentation: http://docs.serverless.com')); this.consoleLog(chalk.dim('* You can run commands with "serverless" or the shortcut "sls"')); + this.consoleLog(chalk.dim('* Pass "--verbose" to this command to get in-depth plugin info')); this.consoleLog(chalk.dim('* Pass "--help" after any for contextual help')); this.consoleLog(''); - _.forEach(this.loadedCommands, (details, command) => { - this.displayCommandUsage(details, command); - }); + if (this.loadedCommands) { + const commandKeys = Object.keys(this.loadedCommands); + const sortedCommandKeys = _.sortBy(commandKeys); + const sortedCommands = _.fromPairs( + _.map(sortedCommandKeys, key => [key, this.loadedCommands[key]]) + ); + + _.forEach(sortedCommands, (details, command) => { + this.displayCommandUsage(details, command); + }); + } else { + this.consoleLog('No commands found'); + } this.consoleLog(''); @@ -134,21 +150,84 @@ class CLI { this.consoleLog(chalk.yellow.underline('Plugins')); if (this.loadedPlugins.length) { - const sortedPlugins = _.sortBy( - this.loadedPlugins, - (plugin) => plugin.constructor.name - ); - + const sortedPlugins = _.sortBy(this.loadedPlugins, (plugin) => plugin.constructor.name); this.consoleLog(sortedPlugins.map((plugin) => plugin.constructor.name).join(', ')); } else { this.consoleLog('No plugins added yet'); } } + generateVerboseHelp() { + this.consoleLog(''); + this.consoleLog(chalk.yellow.underline('Commands by plugin')); + this.consoleLog(''); + + let pluginCommands = {}; + + // add commands to pluginCommands based on command's plugin + const addToPluginCommands = (cmd) => { + const pcmd = _.clone(cmd); + + // remove subcommand from clone + delete pcmd.commands; + + // check if a plugin entry is alreay present in pluginCommands. Use the + // existing one or create a new plugin entry. + if (_.has(pluginCommands, pcmd.pluginName)) { + pluginCommands[pcmd.pluginName] = pluginCommands[pcmd.pluginName].concat(pcmd); + } else { + pluginCommands[pcmd.pluginName] = [pcmd]; + } + + // check for subcommands + if ('commands' in cmd) { + _.forEach(cmd.commands, (d) => { + addToPluginCommands(d); + }); + } + }; + + // fill up pluginCommands with commands in loadedCommands + _.forEach(this.loadedCommands, (details) => { + addToPluginCommands(details); + }); + + // sort plugins alphabetically + pluginCommands = _(pluginCommands).toPairs().sortBy(0).fromPairs() + .value(); + + _.forEach(pluginCommands, (details, plugin) => { + this.consoleLog(plugin); + _.forEach(details, (cmd) => { + // display command usage with single(1) indent + this.displayCommandUsage(cmd, cmd.key.split(':').join(' '), 1); + }); + this.consoleLog(''); + }); + } + generateCommandsHelp(commandsArray) { - const command = this.serverless.pluginManager.getCommand(commandsArray); const commandName = commandsArray.join(' '); + // Get all the commands using getCommands() with filtered entrypoint + // commands and reduce to the required command. + const allCommands = this.serverless.pluginManager.getCommands(); + const command = _.reduce(commandsArray, (currentCmd, cmd) => { + if (currentCmd.commands && cmd in currentCmd.commands) { + return currentCmd.commands[cmd]; + } + return null; + }, { commands: allCommands }); + + // Throw error if command not found. + if (!command) { + const errorMessage = [ + `Serverless command "${commandName}" not found.`, + ' Run "serverless help" for a list of all available commands.', + ].join(''); + throw new this.serverless.classes.Error(errorMessage); + } + // print the name of the plugin this.consoleLog(chalk.yellow.underline(`Plugin: ${command.pluginName}`)); diff --git a/lib/classes/CLI.test.js b/lib/classes/CLI.test.js index 3a1d6b326..9925a75a1 100644 --- a/lib/classes/CLI.test.js +++ b/lib/classes/CLI.test.js @@ -1,10 +1,7 @@ 'use strict'; -/** - * Test: CLI Class - */ - const expect = require('chai').expect; +const sinon = require('sinon'); const CLI = require('../../lib/classes/CLI'); const os = require('os'); const fse = require('fs-extra'); @@ -273,6 +270,79 @@ describe('CLI', () => { }); }); + describe('#generateCommandsHelp()', () => { + let getCommandsStub; + let consoleLogStub; + let displayCommandUsageStub; + let displayCommandOptionsStub; + + const commands = { + package: { + usage: 'Packages a Serverless service', + lifecycleEvents: ['cleanup', 'initialize'], + options: {}, + key: 'package', + pluginName: 'Package', + }, + deploy: { + usage: 'Deploy a Serverless service', + lifecycleEvents: ['cleanup', 'initialize'], + options: {}, + key: 'deploy', + pluginName: 'Deploy', + commands: {}, + }, + }; + + beforeEach(() => { + cli = new CLI(serverless); + getCommandsStub = sinon.stub(cli.serverless.pluginManager, 'getCommands') + .returns(commands); + consoleLogStub = sinon.stub(cli, 'consoleLog').returns(); + displayCommandUsageStub = sinon.stub(cli, 'displayCommandUsage').returns(); + displayCommandOptionsStub = sinon.stub(cli, 'displayCommandOptions').returns(); + }); + + afterEach(() => { + cli.serverless.pluginManager.getCommands.restore(); + cli.consoleLog.restore(); + cli.displayCommandUsage.restore(); + cli.displayCommandOptions.restore(); + }); + + it('should gather and generate the commands help info if the command can be found', () => { + const commandsArray = ['package']; + cli.inputArray = commandsArray; + + cli.generateCommandsHelp(commandsArray); + + expect(getCommandsStub.calledOnce).to.equal(true); + expect(consoleLogStub.called).to.equal(true); + expect(displayCommandUsageStub.calledOnce).to.equal(true); + expect(displayCommandUsageStub.calledWithExactly( + commands.package, + 'package' + )).to.equal(true); + expect(displayCommandOptionsStub.calledOnce).to.equal(true); + expect(displayCommandOptionsStub.calledWithExactly( + commands.package + )).to.equal(true); + }); + + it('should throw an error if the command could not be found', () => { + const commandsArray = ['invalid-command']; + + cli.inputArray = commandsArray; + + expect(() => { cli.generateCommandsHelp(commandsArray); }) + .to.throw(Error, 'not found'); + expect(getCommandsStub.calledOnce).to.equal(true); + expect(consoleLogStub.called).to.equal(false); + expect(displayCommandUsageStub.calledOnce).to.equal(false); + expect(displayCommandOptionsStub.calledOnce).to.equal(false); + }); + }); + describe('#processInput()', () => { it('should only return the commands when only commands are given', () => { cli = new CLI(serverless, ['deploy', 'functions']); @@ -352,5 +422,17 @@ describe('CLI', () => { done(); }); }); + + it('should print help --verbose to stdout', (done) => { + exec(`${this.serverlessExec} help --verbose`, (err, stdout) => { + if (err) { + done(err); + return; + } + + expect(stdout).to.contain('Commands by plugin'); + done(); + }); + }); }); }); diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/validate.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/validate.js index 97f99637f..13ab37913 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/validate.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/validate.js @@ -150,7 +150,7 @@ module.exports = { }, getHttpPath(http, functionName) { - if (typeof http.path === 'string') { + if (http && typeof http.path === 'string') { return http.path.replace(/^\//, '').replace(/\/$/, ''); } const errorMessage = [ @@ -158,6 +158,7 @@ module.exports = { ' for http event 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 indentation of your config values if you use the object syntax.', ' Please check the docs for more options.', ].join(''); throw new this.serverless.classes.Error(errorMessage); @@ -247,7 +248,7 @@ module.exports = { if (integration === 'AWS_PROXY' && typeof arn === 'string' && arn.match(/^arn:aws:cognito-idp/) && authorizer.claims) { const errorMessage = [ - 'Cognito claims can\'t be retrieved when using lambda-proxy as the integration type', + 'Cognito claims can only be filtered when using the lambda integration type', ]; throw new this.serverless.classes.Error(errorMessage); } diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/validate.test.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/validate.test.js index 7ead5365f..6206c757b 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/validate.test.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/validate.test.js @@ -61,6 +61,25 @@ describe('#validate()', () => { expect(() => awsCompileApigEvents.validate()).to.throw(Error); }); + it('should throw a helpful error if http event type object doesn\'t have a path property', () => { + /** + * This can happen with surprising subtle syntax error such as when path is not + * indented under http in yml. + */ + awsCompileApigEvents.serverless.service.functions = { + first: { + events: [ + { + http: null, + }, + ], + }, + }; + + expect(() => awsCompileApigEvents.validate()).to + .throw(/invalid "path" property in function "first"/); + }); + it('should validate the http events "path" property', () => { awsCompileApigEvents.serverless.service.functions = { first: { diff --git a/lib/plugins/aws/provider/awsProvider.js b/lib/plugins/aws/provider/awsProvider.js index 24c2e3854..c7354fb43 100644 --- a/lib/plugins/aws/provider/awsProvider.js +++ b/lib/plugins/aws/provider/awsProvider.js @@ -4,8 +4,9 @@ const AWS = require('aws-sdk'); const BbPromise = require('bluebird'); const HttpsProxyAgent = require('https-proxy-agent'); const url = require('url'); +const chalk = require('chalk'); const _ = require('lodash'); - +const userStats = require('../../../utils/userStats'); const naming = require('../lib/naming.js'); const constants = { @@ -170,10 +171,11 @@ class AwsProvider { if (err.message === 'Missing credentials in config') { const errorMessage = [ 'AWS provider credentials not found.', - ' You can find more info on how to set up provider', - ' credentials in our docs here: https://git.io/vXsdd', + ' Learn how to set up AWS provider credentials', + ` in our docs here: ${chalk.green('https://git.io/vXsdd')}.`, ].join(''); err.message = errorMessage; + userStats.track('user_awsCredentialsNotFound'); } reject(new this.serverless.classes.Error(err.message, err.statusCode)); } else { diff --git a/lib/plugins/config/config.js b/lib/plugins/config/config.js index fc82bfa9a..600c92e2b 100644 --- a/lib/plugins/config/config.js +++ b/lib/plugins/config/config.js @@ -1,6 +1,7 @@ 'use strict'; const BbPromise = require('bluebird'); +const userStats = require('../../utils/userStats'); // class wide constants const validProviders = [ @@ -38,8 +39,8 @@ class Config { }; this.hooks = { - 'before:config:credentials:config': () => BbPromise.bind(this) - .then(this.validate), + 'before:config:credentials:config': () => BbPromise.bind(this).then(this.validate), + 'after:config:credentials:config': () => BbPromise.bind(this).then(this.track), }; } @@ -56,6 +57,18 @@ class Config { return BbPromise.resolve(); } + + track() { + const sls = this.serverless; + if (sls && sls.processedInput && sls.processedInput.options) { + const opts = sls.processedInput.options; + if (opts.provider === 'aws') { + userStats.track('user_awsCredentialsConfigured'); + } + // TODO add other providers here when supported + } + return BbPromise.resolve(); + } } module.exports = Config; diff --git a/lib/plugins/create/create.js b/lib/plugins/create/create.js index 7851a43a8..eb62cc843 100644 --- a/lib/plugins/create/create.js +++ b/lib/plugins/create/create.js @@ -16,6 +16,7 @@ const validTemplates = [ 'aws-java-gradle', 'aws-scala-sbt', 'aws-csharp', + 'aws-fsharp', 'azure-nodejs', 'openwhisk-nodejs', 'openwhisk-python', diff --git a/lib/plugins/create/create.test.js b/lib/plugins/create/create.test.js index eabe5a2d6..f77496b3d 100644 --- a/lib/plugins/create/create.test.js +++ b/lib/plugins/create/create.test.js @@ -121,6 +121,28 @@ describe('Create', () => { }); }); + it('should generate scaffolding for "aws-fsharp" template', () => { + process.chdir(tmpDir); + create.options.template = 'aws-fsharp'; + + 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, 'Handler.fs'))) + .to.be.equal(true); + expect(create.serverless.utils.fileExistsSync(path.join(tmpDir, '.gitignore'))) + .to.be.equal(true); + expect(create.serverless.utils.fileExistsSync(path.join(tmpDir, 'build.sh'))) + .to.be.equal(true); + expect(create.serverless.utils.fileExistsSync(path.join(tmpDir, 'build.cmd'))) + .to.be.equal(true); + expect(create.serverless.utils.fileExistsSync(path.join(tmpDir, 'aws-fsharp.fsproj'))) + .to.be.equal(true); + expect(create.serverless.utils.fileExistsSync(path.join(tmpDir, 'global.json'))) + .to.be.equal(true); + }); + }); + it('should generate scaffolding for "aws-python" template', () => { process.chdir(tmpDir); create.options.template = 'aws-python'; diff --git a/lib/plugins/create/templates/aws-csharp/serverless.yml b/lib/plugins/create/templates/aws-csharp/serverless.yml index 5d227388b..1498ed2a5 100644 --- a/lib/plugins/create/templates/aws-csharp/serverless.yml +++ b/lib/plugins/create/templates/aws-csharp/serverless.yml @@ -67,6 +67,7 @@ functions: # - schedule: rate(10 minutes) # - sns: greeter-topic # - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000 +# - alexaSkill # - iot: # sql: "SELECT * FROM 'some_topic'" # - cloudwatchEvent: diff --git a/lib/plugins/create/templates/aws-fsharp/Handler.fs b/lib/plugins/create/templates/aws-fsharp/Handler.fs new file mode 100644 index 000000000..e1eb3a208 --- /dev/null +++ b/lib/plugins/create/templates/aws-fsharp/Handler.fs @@ -0,0 +1,17 @@ +namespace AwsDotnetFsharp +open Amazon.Lambda.Core + +[)>] +do () + +type Request = { Key1 : string; Key2 : string; Key3 : string } +type Response = { Message : string; Request : Request } + +module Handler = + open System + open System.IO + open System.Text + + let hello(request:Request) = + { Message="Go Serverless v1.0! Your function executed successfully!" + Request=request } diff --git a/lib/plugins/create/templates/aws-fsharp/aws-fsharp.fsproj b/lib/plugins/create/templates/aws-fsharp/aws-fsharp.fsproj new file mode 100644 index 000000000..f3a1aafd4 --- /dev/null +++ b/lib/plugins/create/templates/aws-fsharp/aws-fsharp.fsproj @@ -0,0 +1,24 @@ + + + + netcoreapp1.0 + FsharpHandlers + aws-fsharp + + + + + + + + + + + + + + + + + + diff --git a/lib/plugins/create/templates/aws-fsharp/build.cmd b/lib/plugins/create/templates/aws-fsharp/build.cmd new file mode 100644 index 000000000..468f8503a --- /dev/null +++ b/lib/plugins/create/templates/aws-fsharp/build.cmd @@ -0,0 +1,2 @@ +dotnet restore +dotnet lambda package --configuration release --framework netcoreapp1.0 --output-package bin/release/netcoreapp1.0/deploy-package.zip diff --git a/lib/plugins/create/templates/aws-fsharp/build.sh b/lib/plugins/create/templates/aws-fsharp/build.sh new file mode 100644 index 000000000..892b0f289 --- /dev/null +++ b/lib/plugins/create/templates/aws-fsharp/build.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +#install zip +apt-get -qq update +apt-get -qq -y install zip + +dotnet restore + +#create deployment package +dotnet lambda package --configuration release --framework netcoreapp1.0 --output-package bin/release/netcoreapp1.0/deploy-package.zip diff --git a/lib/plugins/create/templates/aws-fsharp/gitignore b/lib/plugins/create/templates/aws-fsharp/gitignore new file mode 100644 index 000000000..4dc157e7f --- /dev/null +++ b/lib/plugins/create/templates/aws-fsharp/gitignore @@ -0,0 +1,246 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Microsoft Azure ApplicationInsights config file +ApplicationInsights.config + +# Windows Store app package directory +AppPackages/ +BundleArtifacts/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe + +# FAKE - F# Make +.fake/ +*.orig + +# macOS +.DS_Store + +# JetBrains Rider C# IDE +.idea* + +# Serverless directories +.serverless diff --git a/lib/plugins/create/templates/aws-fsharp/global.json b/lib/plugins/create/templates/aws-fsharp/global.json new file mode 100644 index 000000000..8af244a46 --- /dev/null +++ b/lib/plugins/create/templates/aws-fsharp/global.json @@ -0,0 +1,5 @@ +{ + "sdk": { + "version": "1.0.4" + } +} diff --git a/lib/plugins/create/templates/aws-fsharp/serverless.yml b/lib/plugins/create/templates/aws-fsharp/serverless.yml new file mode 100644 index 000000000..314962e28 --- /dev/null +++ b/lib/plugins/create/templates/aws-fsharp/serverless.yml @@ -0,0 +1,101 @@ +# 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-fsharp # NOTE: update this with your service name + +# You can pin your service to only deploy with a specific Serverless version +# Check out our docs for more details +# frameworkVersion: "=X.X.X" + +provider: + name: aws + runtime: dotnetcore1.0 + +# 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 define service wide environment variables here +# environment: +# variable1: value1 + +# you can add packaging information here +package: + artifact: bin/release/netcoreapp1.0/deploy-package.zip +# exclude: +# - exclude-me.js +# - exclude-me-dir/** + +functions: + hello: + handler: FsharpHandlers::AwsDotnetFsharp.Handler::hello + +# The following are a few example events you can configure +# NOTE: Please make sure to change your handler code to work with those events +# Check the event documentation for details +# events: +# - http: +# path: users/create +# method: get +# - s3: ${env:BUCKET} +# - schedule: rate(10 minutes) +# - sns: greeter-topic +# - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000 +# - alexaSkill +# - iot: +# sql: "SELECT * FROM 'some_topic'" +# - cloudwatchEvent: +# event: +# source: +# - "aws.ec2" +# detail-type: +# - "EC2 Instance State-change Notification" +# detail: +# state: +# - pending +# - cloudwatchLog: '/aws/lambda/hello' +# - cognitoUserPool: +# pool: MyUserPool +# trigger: PreSignUp + +# Define function environment variables here +# environment: +# variable2: value2 + +# 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" diff --git a/lib/plugins/deploy/deploy.js b/lib/plugins/deploy/deploy.js index 8c26dd55d..bdb917acd 100644 --- a/lib/plugins/deploy/deploy.js +++ b/lib/plugins/deploy/deploy.js @@ -97,13 +97,23 @@ class Deploy { return BbPromise.resolve(); }), - 'after:deploy:deploy': () => BbPromise.bind(this) - .then(this.track), + 'after:deploy:deploy': () => BbPromise.bind(this).then(this.track), }; } track() { - userStats.track('service_deployed'); + const sls = this.serverless; + let serviceInfo = {}; + if (sls && sls.service && sls.service.provider && sls.service.provider.name) { + serviceInfo = { + provider: sls.service.provider.name, + runtime: sls.service.provider.runtime, + }; + } + userStats.track('service_deployed', { + data: serviceInfo, + }); + return BbPromise.resolve(); } } diff --git a/lib/plugins/info/info.js b/lib/plugins/info/info.js index e5889cce7..5d4499397 100644 --- a/lib/plugins/info/info.js +++ b/lib/plugins/info/info.js @@ -1,5 +1,8 @@ 'use strict'; +const BbPromise = require('bluebird'); +const userStats = require('../../utils/userStats'); + class Info { constructor(serverless) { this.serverless = serverless; @@ -26,6 +29,15 @@ class Info { }, }, }; + + this.hooks = { + 'after:info:info': () => BbPromise.bind(this).then(this.track), + }; + } + + track() { + userStats.track('service_infoViewed'); + return BbPromise.resolve(); } } diff --git a/lib/plugins/install/install.js b/lib/plugins/install/install.js index 7fb095067..f845bece0 100644 --- a/lib/plugins/install/install.js +++ b/lib/plugins/install/install.js @@ -6,6 +6,7 @@ const URL = require('url'); const download = require('download'); const fse = require('fs-extra'); const os = require('os'); +const userStats = require('../../utils/userStats'); class Install { constructor(serverless, options) { @@ -151,6 +152,11 @@ class Install { return this.renameService(dirName, servicePath); }).then(() => { let message = `Successfully installed "${serviceName}"`; + userStats.track('service_installed', { + data: { // will be updated with core analtyics lib + url: this.options.url, + }, + }); if (renamed) message = `${message} as "${dirName}"`; that.serverless.cli.log(message); diff --git a/lib/plugins/invoke/invoke.js b/lib/plugins/invoke/invoke.js index c5ecdfee5..f513fef23 100644 --- a/lib/plugins/invoke/invoke.js +++ b/lib/plugins/invoke/invoke.js @@ -2,6 +2,7 @@ const BbPromise = require('bluebird'); const _ = require('lodash'); +const userStats = require('../../utils/userStats'); class Invoke { constructor(serverless) { @@ -72,11 +73,22 @@ class Invoke { }; this.hooks = { - 'invoke:local:loadEnvVars': () => BbPromise.bind(this) - .then(this.loadEnvVarsForLocal), + 'invoke:local:loadEnvVars': () => BbPromise.bind(this).then(this.loadEnvVarsForLocal), + 'after:invoke:invoke': () => BbPromise.bind(this).then(this.trackInvoke), + 'after:invoke:local:invoke': () => BbPromise.bind(this).then(this.trackInvokeLocal), }; } + trackInvoke() { + userStats.track('service_invoked'); + return BbPromise.resolve(); + } + + trackInvokeLocal() { + userStats.track('service_invokedLocally'); + return BbPromise.resolve(); + } + /** * Set environment variables for "invoke local" that are provider independent. */ diff --git a/lib/plugins/login/login.js b/lib/plugins/login/login.js index 902164475..56476c955 100644 --- a/lib/plugins/login/login.js +++ b/lib/plugins/login/login.js @@ -7,7 +7,7 @@ const fetch = require('node-fetch'); const jwtDecode = require('jwt-decode'); const chalk = require('chalk'); const openBrowser = require('../../utils/openBrowser'); -const getFrameworkId = require('../../utils/getFrameworkId'); +const configUtils = require('../../utils/config'); const clearConsole = require('../../utils/clearConsole'); const userStats = require('../../utils/userStats'); const setConfig = require('../../utils/config').set; @@ -49,13 +49,15 @@ class Login { // Generate the verifier, and the corresponding challenge const verifier = base64url(crypto.randomBytes(32)); const verifierChallenge = base64url(crypto.createHash('sha256').update(verifier).digest()); - const frameworkId = getFrameworkId(); + const configuration = configUtils.getConfig(); + const frameworkId = configuration.frameworkId; // eslint-disable-next-line prefer-template const version = this.serverless.version; const state = `id%3D${frameworkId}%26version%3D${version}%26platform%3D${process.platform}`; // refresh token docs https://auth0.com/docs/tokens/preview/refresh-token#get-a-refresh-token + const scope = 'openid%20nickname%20email%20name%20login_count%20created_at%20tracking_id%20offline_access'; // eslint-disable-line const authorizeUrl = - `${config.AUTH0_URL}/authorize?response_type=code&scope=openid%20profile%20offline_access` + + `${config.AUTH0_URL}/authorize?response_type=code&scope=${scope}` + `&client_id=${config.AUTH0_CLIENT_ID}&redirect_uri=${config.AUTH0_CALLBACK_URL}` + `&code_challenge=${verifierChallenge}&code_challenge_method=S256&state=${state}`; @@ -91,9 +93,8 @@ class Login { .then((platformResponse) => { const decoded = jwtDecode(platformResponse.id_token); this.serverless.cli.log('You are now logged in'); - // because platform only support github - const id = decoded.original_user_id || decoded.sub; + const id = decoded.tracking_id || decoded.sub; /* For future use segment.identify({ @@ -134,6 +135,7 @@ class Login { email: decoded.email, // unix timestamp created_at: Math.round(+new Date(createdAt) / 1000), + trackingDisabled: configuration.trackingDisabled, force: true, }).then(() => { userStats.track('user_loggedIn', { diff --git a/lib/plugins/logs/logs.js b/lib/plugins/logs/logs.js index 3cb9b4db0..1888f7227 100644 --- a/lib/plugins/logs/logs.js +++ b/lib/plugins/logs/logs.js @@ -1,5 +1,8 @@ 'use strict'; +const BbPromise = require('bluebird'); +const userStats = require('../../utils/userStats'); + class Logs { constructor(serverless) { this.serverless = serverless; @@ -41,6 +44,20 @@ class Logs { }, }, }; + + this.hooks = { + 'after:logs:logs': () => BbPromise.bind(this).then(this.track), + }; + } + + track() { + const sls = this.serverless; + if (sls && sls.processedInput && sls.processedInput.options) { + const opts = sls.processedInput.options; + const type = (opts.tail) ? 'service_logsTailed' : 'service_logsViewed'; + userStats.track(type); + } + return BbPromise.resolve(); } } diff --git a/lib/plugins/metrics/metrics.js b/lib/plugins/metrics/metrics.js index 393dc8b86..c089724c2 100644 --- a/lib/plugins/metrics/metrics.js +++ b/lib/plugins/metrics/metrics.js @@ -1,5 +1,8 @@ 'use strict'; +const BbPromise = require('bluebird'); +const userStats = require('../../utils/userStats'); + class Metrics { constructor(serverless, options) { this.serverless = serverless; @@ -33,6 +36,16 @@ class Metrics { }, }, }; + + this.hooks = { + 'after:metrics:metrics': () => BbPromise.bind(this).then(this.track), + }; + } + + track() { + // todo would like to see time frame via --startTime + userStats.track('service_metricsViewed'); + return BbPromise.resolve(); } } diff --git a/lib/plugins/platform/platform.js b/lib/plugins/platform/platform.js index a05a2e0e9..e3d7a690b 100644 --- a/lib/plugins/platform/platform.js +++ b/lib/plugins/platform/platform.js @@ -1,5 +1,7 @@ 'use strict'; +/* eslint-disable no-console */ + const path = require('path'); const fs = require('fs'); const gql = require('graphql-tag'); @@ -7,6 +9,7 @@ const jwtDecode = require('jwt-decode'); const BbPromise = require('bluebird'); const fsExtra = require('../../utils/fs/fse'); const fetch = require('node-fetch'); +const chalk = require('chalk'); const configUtils = require('../../utils/config'); const functionInfoUtils = require('../../utils/functionInfoUtils'); const createApolloClient = require('../../utils/createApolloClient'); @@ -61,10 +64,16 @@ class Platform { if (this.provider) { this.hooks = { 'after:deploy:deploy': this.publishService.bind(this), + 'after:remove:remove': this.removeService.bind(this), }; } } + removeService() { + // TODO implement platform removal here + return BbPromise.resolve(); + } + publishServiceRequest(service, client) { return client .mutate({ @@ -81,6 +90,10 @@ class Platform { } getAuthToken() { + if (process.env.SERVERLESS_TOKEN) { + return process.env.SERVERLESS_TOKEN; + } + const userConfig = configUtils.getConfig(); const currentId = userConfig.userId; const globalConfig = configUtils.getGlobalConfig(); @@ -96,6 +109,13 @@ class Platform { // NOTE publishService is an opt-in feature and no warning is needed return BbPromise.resolve(); } + + if (authToken && authToken.length > 8000) { + this.serverless.cli.log('Your serverless login has expired'); + this.serverless.cli.log('Please run `serverless login` again'); + return BbPromise.resolve(); + } + this.serverless.cli.log('Publish service to Serverless Platform...'); const clientWithAuth = createApolloClient(config.GRAPHQL_ENDPOINT_URL, authToken); @@ -153,19 +173,23 @@ class Platform { const readmePath = path.join(this.serverless.config.servicePath, 'README.md'); const serviceDataWithReadme = addReadme(serviceData, readmePath); - return this.publishServiceRequest(serviceDataWithReadme, clientWithAuth) - .then(() => { - const username = jwtDecode(authToken).nickname; - const serviceName = this.serverless.service.service; - const url = `${config.PLATFORM_FRONTEND_BASE_URL}services/${username}/${serviceName}`; - this.serverless.cli.log(`Your service is available at ${url}`); - }) - .catch(error => { - this.serverless.cli.log( - "Couldn't publish this deploy information to the Serverless Platform due: \n", - error - ); - }); + return new BbPromise((resolve, reject) => { + this.publishServiceRequest(serviceDataWithReadme, clientWithAuth) + .then(() => { + const username = jwtDecode(authToken).nickname; + const serviceName = this.serverless.service.service; + const url = `${config.PLATFORM_FRONTEND_BASE_URL}services/${username}/${serviceName}`; + console.log('Service successfully published! Your service details are available at:'); + console.log(chalk.green(url)); + resolve(); + }) + .catch(error => { + this.serverless.cli.log( + "Couldn't publish this deploy information to the Serverless Platform." + ); + reject(error); + }); + }); }) ); } diff --git a/lib/plugins/platform/platform.test.js b/lib/plugins/platform/platform.test.js index 162733ae7..2f9c4a2fc 100644 --- a/lib/plugins/platform/platform.test.js +++ b/lib/plugins/platform/platform.test.js @@ -1,7 +1,10 @@ 'use strict'; +/* eslint-disable no-console */ + const expect = require('chai').expect; const sinon = require('sinon'); +const chalk = require('chalk'); const Platform = require('./platform'); const Serverless = require('../../Serverless'); const AwsProvider = require('../aws/provider/awsProvider'); @@ -51,6 +54,7 @@ describe('Platform', () => { ], }); publishServiceRequestStub = sinon.stub(platform, 'publishServiceRequest').resolves(); + sinon.spy(console, 'log'); }); afterEach(() => { @@ -58,6 +62,7 @@ describe('Platform', () => { getAccountIdStub.restore(); endpointsRequestStub.restore(); publishServiceRequestStub.restore(); + console.log.restore(); }); it('should send a minimal service request to the platform', () => { @@ -66,7 +71,6 @@ describe('Platform', () => { name: 'new-service-2', }; platform.serverless.config.servicePath = '/path/to/service'; - sinon.spy(platform.serverless.cli, 'log'); return platform.publishService().then(() => { expect(getAuthTokenStub.calledOnce).to.be.equal(true); @@ -75,9 +79,10 @@ describe('Platform', () => { expect(publishServiceRequestStub.calledOnce).to.be.equal(true); const expected = { name: 'new-service-2', stage: undefined, functions: [] }; expect(publishServiceRequestStub.getCall(0).args[0]).to.deep.equal(expected); - const expectedLog = - 'Your service is available at https://platform.serverless.com/services/johndoe/new-service-2'; - expect(platform.serverless.cli.log.calledWithExactly(expectedLog)).to.be.equal(true); + const url = chalk.green('https://platform.serverless.com/services/johndoe/new-service-2'); + const successLog = 'Service successfully published! Your service details are available at:'; + expect(console.log.calledWithExactly(successLog)).to.be.equal(true); + expect(console.log.calledWithExactly(url)).to.be.equal(true); }); }); @@ -104,8 +109,6 @@ describe('Platform', () => { }, }; - sinon.spy(platform.serverless.cli, 'log'); - return platform.publishService().then(() => { expect(getAuthTokenStub.calledOnce).to.be.equal(true); expect(getAccountIdStub.calledOnce).to.be.equal(true); @@ -138,9 +141,10 @@ describe('Platform', () => { license: 'MIT', }; expect(publishServiceRequestStub.getCall(0).args[0]).to.deep.equal(expected); - const expectedLog = - 'Your service is available at https://platform.serverless.com/services/johndoe/new-service-2'; - expect(platform.serverless.cli.log.calledWithExactly(expectedLog)).to.be.equal(true); + const url = chalk.green('https://platform.serverless.com/services/johndoe/new-service-2'); + const successLog = 'Service successfully published! Your service details are available at:'; + expect(console.log.calledWithExactly(successLog)).to.be.equal(true); + expect(console.log.calledWithExactly(url)).to.be.equal(true); }); }); }); diff --git a/lib/plugins/remove/remove.js b/lib/plugins/remove/remove.js index e7d85cfc4..1f098b38a 100644 --- a/lib/plugins/remove/remove.js +++ b/lib/plugins/remove/remove.js @@ -1,5 +1,8 @@ 'use strict'; +const BbPromise = require('bluebird'); +const userStats = require('../../utils/userStats'); + class Remove { constructor(serverless) { this.serverless = serverless; @@ -26,6 +29,15 @@ class Remove { }, }, }; + + this.hooks = { + 'after:remove:remove': () => BbPromise.bind(this).then(this.track), + }; + } + + track() { + userStats.track('service_removed'); + return BbPromise.resolve(); } } diff --git a/lib/plugins/rollback/index.js b/lib/plugins/rollback/index.js index 4adbcccdc..d7e4a52ad 100644 --- a/lib/plugins/rollback/index.js +++ b/lib/plugins/rollback/index.js @@ -1,5 +1,8 @@ 'use strict'; +const BbPromise = require('bluebird'); +const userStats = require('../../utils/userStats'); + class Rollback { constructor(serverless) { this.serverless = serverless; @@ -52,6 +55,15 @@ class Rollback { }, }, }; + + this.hooks = { + 'after:rollback:rollback': () => BbPromise.bind(this).then(this.track), + }; + } + + track() { + userStats.track('service_rolledBack'); + return BbPromise.resolve(); } } diff --git a/package-lock.json b/package-lock.json index 1804b5917..b81c4dfcc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "serverless", - "version": "1.16.0", + "version": "1.16.1", "lockfileVersion": 1, "dependencies": { "@types/async": { diff --git a/package.json b/package.json index 5f7309141..cbe4f0e77 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "serverless", - "version": "1.16.0", + "version": "1.16.1", "engines": { "node": ">=4.0" }, diff --git a/scripts/postinstall.js b/scripts/postinstall.js index a684be441..a32f64078 100644 --- a/scripts/postinstall.js +++ b/scripts/postinstall.js @@ -1,5 +1,7 @@ 'use strict'; +const path = require('path'); + /* eslint-disable no-console */ /* eslint-disable no-use-before-define */ @@ -20,13 +22,17 @@ try { function setupAutocomplete() { return new Promise((resolve, reject) => { + const indexRegex = new RegExp(path.join(path.sep, 'index.js')); + const tabtabPath = require.resolve('tabtab').replace(indexRegex, ''); + const tabtabCliPath = path.join(tabtabPath, 'src', 'cli.js'); + try { - execSync('node ./node_modules/tabtab/src/cli.js install --name serverless --auto'); - execSync('node ./node_modules/tabtab/src/cli.js install --name sls --auto'); + execSync(`node ${tabtabCliPath} install --name serverless --auto`); + execSync(`node ${tabtabCliPath} install --name sls --auto`); return resolve(); } catch (error) { - execSync('node ./node_modules/tabtab/src/cli.js install --name serverless --stdout'); - execSync('node ./node_modules/tabtab/src/cli.js install --name sls --stdout'); + execSync(`node ${tabtabCliPath} install --name serverless --stdout`); + execSync(`node ${tabtabCliPath} install --name sls --stdout`); console.log('Could not auto-install serverless autocomplete script.'); console.log('Please copy / paste the script above into your shell.'); return reject(error); diff --git a/tests/templates/test_all_templates b/tests/templates/test_all_templates index 76a345eee..b539c35d1 100755 --- a/tests/templates/test_all_templates +++ b/tests/templates/test_all_templates @@ -9,6 +9,7 @@ function integration-test { } integration-test aws-csharp 'apt-get -qq update && apt-get -qq -y install zip && dotnet restore && dotnet lambda package --configuration release --framework netcoreapp1.0 --output-package bin/release/netcoreapp1.0/deploy-package.zip' +integration-test aws-fsharp 'apt-get -qq update && apt-get -qq -y install zip && dotnet restore && dotnet lambda package --configuration release --framework netcoreapp1.0 --output-package bin/release/netcoreapp1.0/deploy-package.zip' integration-test aws-groovy-gradle ./gradlew build integration-test aws-java-gradle ./gradlew build integration-test aws-java-maven mvn package