diff --git a/.gitignore b/.gitignore index 4699a968f..4d98e9617 100755 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,6 @@ tracking-config.json # Misc jest + +# VIM +*.swp diff --git a/.travis.yml b/.travis.yml index 81590be86..abb99c87c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,13 +23,6 @@ matrix: - SLS_IGNORE_WARNING=* - secure: Ia2nYzOeYvTE6qOP7DBKX3BO7s/U7TXdsvB2nlc3kOPFi//IbTVD0/cLKCAE5XqTzrrliHINSVsFcJNSfjCwmDSRmgoIGrHj5CJkWpkI6FEPageo3mdqFQYEc8CZeAjsPBNaHe6Ewzg0Ev/sjTByLSJYVqokzDCF1QostSxx1Ss6SGt1zjxeP/Hp4yOJn52VAm9IHAKYn7Y62nMAFTaaTPUQHvW0mJj6m2Z8TWyPU+2Bx6mliO65gTPFGs+PdHGwHtmSF/4IcUO504x+HjDuwzW2itomLXZmIOFfGDcFYadKWzVMAfJzoRWOcVKF4jXdMoSCOviWpHGtK35E7K956MTXkroVoWCS7V0knQDovbRZj8c8td8mS4tdprUA+TzgZoHet2atWNtMuTh79rdmwoAO+IAWJegYj62Tdfy3ycESzY+KxSaV8kysG9sR3PRFoWjZerA7MhLZEzQMORXDGjJlgwLaZfYVqjlsGe5p5etFBUTd0WbFgSwOKLoA2U/fm7WzqItkjs3UWaHuvFVvwYixGxjEVmVczS6wa2cdGpHtVD9H7km4fPEzljHqQ26v0P5e8eylgqLF2IB6mL7UqGFrAtrMvAgN/M3gnq4dTs/wq1AJIOxEP7YW7kc0NAldk8vUz6t5GzCPNcuukxAku91Awnh0twxgUywatgJLZPY= - secure: Dgaa5XIsA5Vbw/CYQLUAuVVsDX26C8+f1XYGwsbNmFQKbKvM8iy9lGrHlfrT3jftJkJH6re8tP1RjyZjjzLe25KPk4Tps7grNteCyiIIEDsC2aHhiXHD6zNHsItpxYusaFfyQinFWnK4CAYKWb9ZNIwHIDUIB4vq807QGAhYsnoj1Lg/ajWvtEKBwYjEzDz9OjB91lw7lpCnHtmKKw5A+TNIVGpDDZ/jRBqETsPaePtiXC9UTHZQyM3gFoeVXiJw9KSU/gjIx9REihCaWWPbnuQSeIONGGlVWY9V4DTZIsJr9/uwDcbioeXDD3G1ezGtNPPRSNTtq08QlUtE4mEtKea/+ObpllKZCeZGn6AJhMn+uqMIP95FFlqBB55YzRcLZY+Igi/qm/9LJ9RinAhxRVXiwzeQ+BdVA6jshAAzr+7wklux6lZAa0xGw9pgTv7MI4RP2LJ/LMP1ppFsnv9n/qt93Ax1VEwEu3xHZe3VTYL9tbXOPTZutf6fKjUrW7wSSuy637queESjYnnPKSb1vZcPxjSFlyh+GJvxu/3PurF9aqfiBdiorIBre+pQS4lakLtoft5nsbA+4iYUwrXR58qUPVUqQ7a0A0hedOWlp6g9ixLa6nugUP5aobJzR71T8l/IjqpnY2EEd/iINEb0XfUiZtB5zHaqFWejBtmWwCI= - - node_js: '6.2' - env: - - INTEGRATION_TEST=true - - INTEGRATION_TEST_SUITE=complex - - SLS_IGNORE_WARNING=* - - secure: Ia2nYzOeYvTE6qOP7DBKX3BO7s/U7TXdsvB2nlc3kOPFi//IbTVD0/cLKCAE5XqTzrrliHINSVsFcJNSfjCwmDSRmgoIGrHj5CJkWpkI6FEPageo3mdqFQYEc8CZeAjsPBNaHe6Ewzg0Ev/sjTByLSJYVqokzDCF1QostSxx1Ss6SGt1zjxeP/Hp4yOJn52VAm9IHAKYn7Y62nMAFTaaTPUQHvW0mJj6m2Z8TWyPU+2Bx6mliO65gTPFGs+PdHGwHtmSF/4IcUO504x+HjDuwzW2itomLXZmIOFfGDcFYadKWzVMAfJzoRWOcVKF4jXdMoSCOviWpHGtK35E7K956MTXkroVoWCS7V0knQDovbRZj8c8td8mS4tdprUA+TzgZoHet2atWNtMuTh79rdmwoAO+IAWJegYj62Tdfy3ycESzY+KxSaV8kysG9sR3PRFoWjZerA7MhLZEzQMORXDGjJlgwLaZfYVqjlsGe5p5etFBUTd0WbFgSwOKLoA2U/fm7WzqItkjs3UWaHuvFVvwYixGxjEVmVczS6wa2cdGpHtVD9H7km4fPEzljHqQ26v0P5e8eylgqLF2IB6mL7UqGFrAtrMvAgN/M3gnq4dTs/wq1AJIOxEP7YW7kc0NAldk8vUz6t5GzCPNcuukxAku91Awnh0twxgUywatgJLZPY= - - secure: Dgaa5XIsA5Vbw/CYQLUAuVVsDX26C8+f1XYGwsbNmFQKbKvM8iy9lGrHlfrT3jftJkJH6re8tP1RjyZjjzLe25KPk4Tps7grNteCyiIIEDsC2aHhiXHD6zNHsItpxYusaFfyQinFWnK4CAYKWb9ZNIwHIDUIB4vq807QGAhYsnoj1Lg/ajWvtEKBwYjEzDz9OjB91lw7lpCnHtmKKw5A+TNIVGpDDZ/jRBqETsPaePtiXC9UTHZQyM3gFoeVXiJw9KSU/gjIx9REihCaWWPbnuQSeIONGGlVWY9V4DTZIsJr9/uwDcbioeXDD3G1ezGtNPPRSNTtq08QlUtE4mEtKea/+ObpllKZCeZGn6AJhMn+uqMIP95FFlqBB55YzRcLZY+Igi/qm/9LJ9RinAhxRVXiwzeQ+BdVA6jshAAzr+7wklux6lZAa0xGw9pgTv7MI4RP2LJ/LMP1ppFsnv9n/qt93Ax1VEwEu3xHZe3VTYL9tbXOPTZutf6fKjUrW7wSSuy637queESjYnnPKSb1vZcPxjSFlyh+GJvxu/3PurF9aqfiBdiorIBre+pQS4lakLtoft5nsbA+4iYUwrXR58qUPVUqQ7a0A0hedOWlp6g9ixLa6nugUP5aobJzR71T8l/IjqpnY2EEd/iINEb0XfUiZtB5zHaqFWejBtmWwCI= - node_js: '6.2' env: - DISABLE_TESTS=true @@ -44,7 +37,6 @@ script: - if [[ -z "$INTEGRATION_TEST" && -z "$DISABLE_TESTS" ]]; then npm test; fi - if [[ ! -z "$DISABLE_TESTS" && ! -z "$LINTING" && -z "$INTEGRATION_TEST" ]]; then npm run lint; fi - if [[ ! -z "$INTEGRATION_TEST" && ! -z ${AWS_ACCESS_KEY_ID+x} && "$INTEGRATION_TEST_SUITE" == "simple" ]]; then npm run simple-integration-test; fi - - if [[ ! -z "$INTEGRATION_TEST" && ! -z ${AWS_ACCESS_KEY_ID+x} && "$INTEGRATION_TEST_SUITE" == "complex" && "$TRAVIS_BRANCH" == "master" && "$TRAVIS_PULL_REQUEST" == "false" ]]; then npm run complex-integration-test; fi after_success: - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage diff --git a/bin/serverless b/bin/serverless index d87b19226..bb59dae19 100755 --- a/bin/serverless +++ b/bin/serverless @@ -10,9 +10,12 @@ const initializeErrorReporter = require('../lib/utils/sentry').initializeErrorRe Error.stackTraceLimit = Infinity; -BbPromise.config({ - longStackTraces: true, -}); +if (process.env.SLS_DEBUG) { + // For performance reasons enabled only in SLS_DEBUG mode + BbPromise.config({ + longStackTraces: true, + }); +} process.on('unhandledRejection', (e) => { logError(e); diff --git a/docker-compose.yml b/docker-compose.yml index 9d9703d0f..4c2bbf7d2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -79,6 +79,10 @@ services: image: python2.7 volumes: - ./tmp/serverless-integration-test-spotinst-python:/app + spotinst-ruby: + image: ruby2.4.1 + volumes: + - ./tmp/serverless-integration-test-spotinst-ruby:/app webtasks-nodejs: image: node:6.10.3 volumes: diff --git a/docs/README.md b/docs/README.md index b7698d63b..448002d85 100644 --- a/docs/README.md +++ b/docs/README.md @@ -122,6 +122,7 @@ Already using AWS or another cloud provider? Read on.
  • Guide
  • CLI Reference
  • Events
  • +
  • Examples
  • diff --git a/docs/providers/aws/README.md b/docs/providers/aws/README.md index 3ae4e335b..8639edb2d 100644 --- a/docs/providers/aws/README.md +++ b/docs/providers/aws/README.md @@ -72,6 +72,7 @@ If you have questions, join the [chat in gitter](https://gitter.im/serverless/se
  • Plugin Search
  • Plugin Install
  • Plugin Uninstall
  • +
  • Print
  • Serverless Stats
  • @@ -91,6 +92,7 @@ If you have questions, join the [chat in gitter](https://gitter.im/serverless/se
  • Schedule
  • SNS
  • Alexa Skill
  • +
  • Alexa Smart Home
  • IoT
  • CloudWatch Event
  • CloudWatch Log
  • diff --git a/docs/providers/aws/cli-reference/create.md b/docs/providers/aws/cli-reference/create.md index e88221851..9555e0903 100644 --- a/docs/providers/aws/cli-reference/create.md +++ b/docs/providers/aws/cli-reference/create.md @@ -26,8 +26,15 @@ serverless create --template aws-nodejs serverless create --template aws-nodejs --path myService ``` +**Create service in new folder using a custom template:** + +```bash +serverless create --template-url https://github.com/serverless/serverless/tree/master/lib/plugins/create/templates/aws-nodejs --path myService +``` + ## Options -- `--template` or `-t` The name of one of the available templates. **Required**. +- `--template` or `-t` The name of one of the available templates. **Required if --template-url is not present**. +- `--template-url` or `-u` The name of one of the available templates. **Required if --template is not present**. - `--path` or `-p` The path where the service should be created. - `--name` or `-n` the name of the service in `serverless.yml`. diff --git a/docs/providers/aws/cli-reference/print.md b/docs/providers/aws/cli-reference/print.md new file mode 100644 index 000000000..53bcb602f --- /dev/null +++ b/docs/providers/aws/cli-reference/print.md @@ -0,0 +1,78 @@ + + + +### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/cli-reference/print) + + +# Print + +Print your `serverless.yml` config file with all variables resolved. + +If you're using [Serverless Variables](https://serverless.com/framework/docs/providers/aws/guide/variables/) +in your `serverless.yml`, it can be difficult to know if your syntax is correct +or if the variables are resolving as you expect. + +With this command, it will print the fully-resolved config to your console. + +```bash +serverless print +``` + +## Options + +- None + +## Examples: + +Assuming you have the following config file: + +```yml +service: my-service + +custom: + bucketName: test + +provider: + name: aws + runtime: nodejs6.10 + stage: ${opt:stage, "dev"} + +functions: + hello: + handler: handler.hello + +resources: + Resources: + MyBucket: + Type: AWS::S3::Bucket + Properties: + BucketName: ${self:custom.bucketName} +``` + +Using `sls print` will resolve the variables in `provider.stage` and `BucketName`. + +```bash +$ sls print +service: my-service +custom: + bucketName: test +provider: + name: aws + runtime: nodejs6.10 + stage: dev # <-- Resolved +functions: + hello: + handler: handler.hello +resources: + Resources: + MyBucket: + Type: 'AWS::S3::Bucket' + Properties: + BucketName: test # <-- Resolved +``` diff --git a/docs/providers/aws/cli-reference/rollback.md b/docs/providers/aws/cli-reference/rollback.md index 3839566d4..de4a88eec 100644 --- a/docs/providers/aws/cli-reference/rollback.md +++ b/docs/providers/aws/cli-reference/rollback.md @@ -19,6 +19,8 @@ Rollback the Serverless service to a specific deployment. serverless rollback --timestamp timestamp ``` +If `timestamp` is not specified, Framework will show your existing deployments. + ## Options - `--timestamp` or `-t` The deployment you want to rollback to. - `--verbose` or `-v` Shows any Stack Output. diff --git a/docs/providers/aws/cli-reference/slstats.md b/docs/providers/aws/cli-reference/slstats.md index 682765069..c62ac7f89 100644 --- a/docs/providers/aws/cli-reference/slstats.md +++ b/docs/providers/aws/cli-reference/slstats.md @@ -1,7 +1,7 @@ diff --git a/docs/providers/aws/events/alexa-smart-home.md b/docs/providers/aws/events/alexa-smart-home.md new file mode 100644 index 000000000..f6557597f --- /dev/null +++ b/docs/providers/aws/events/alexa-smart-home.md @@ -0,0 +1,46 @@ + + + +### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/events/alexa-smart-home) + + +# Alexa Smart Home + +## Event definition + +This will enable your Lambda function to be called by an Alexa Smart Home Skill. +`amzn1.ask.skill.xx-xx-xx-xx` is an application ID for Alexa Smart Home. You need to sign up [Amazon Developer Console](https://developer.amazon.com/) and get your application ID. +After deploying, add your deployed Lambda function ARN to which this event is attached to the Service Endpoint under Configuration on Amazon Developer Console. + +Please see [Steps to Create a Smart Home Skill](https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/steps-to-create-a-smart-home-skill) for more info. + +```yml +functions: + mySkill: + handler: mySkill.handler + events: + - alexaSmartHome: amzn1.ask.skill.xx-xx-xx-xx +``` + +## Enabling / Disabling + +**Note:** `alexaSmartHome` events are enabled by default. + +This will create and attach a alexaSmartHome event for the `mySkill` function which is disabled. If enabled it will call +the `mySkill` function by an Alexa Smart Home Skill. + +```yaml +functions: + mySkill: + handler: mySkill.handler + events: + - alexaSmartHome: + appId: amzn1.ask.skill.xx-xx-xx-xx + enabled: false +``` diff --git a/docs/providers/aws/events/apigateway.md b/docs/providers/aws/events/apigateway.md index ff68cfbc3..aa593e447 100644 --- a/docs/providers/aws/events/apigateway.md +++ b/docs/providers/aws/events/apigateway.md @@ -277,6 +277,7 @@ functions: resultTtlInSeconds: 0 identitySource: method.request.header.Authorization identityValidationExpression: someRegex + type: token authorizerFunc: handler: handler.authorizerFunc ``` @@ -313,6 +314,24 @@ functions: identityValidationExpression: someRegex ``` +You can also use the Request Type Authorizer by setting the `type` property. In this case, your `identitySource` could contain multiple entries for you policy cache. The default `type` is 'token'. + +```yml +functions: + create: + handler: posts.create + events: + - http: + path: posts/create + method: post + authorizer: + arn: xxx:xxx:Lambda-Name + resultTtlInSeconds: 0 + identitySource: method.request.header.Authorization, context.identity.sourceIp + identityValidationExpression: someRegex + type: request +``` + You can also configure an existing Cognito User Pool as the authorizer, as shown in the following example: diff --git a/docs/providers/aws/guide/functions.md b/docs/providers/aws/guide/functions.md index 59d20a5e0..de2538c87 100644 --- a/docs/providers/aws/guide/functions.md +++ b/docs/providers/aws/guide/functions.md @@ -139,7 +139,7 @@ provider: - Effect: "Allow" Action: - "s3:ListBucket" - # You can put CloudFormation syntax in here. No one will judge you. + # You can put CloudFormation syntax in here. No one will judge you. # Remember, this all gets translated to CloudFormation. Resource: { "Fn::Join" : ["", ["arn:aws:s3:::", { "Ref" : "ServerlessDeploymentBucket"} ] ] } - Effect: "Allow" @@ -226,6 +226,11 @@ Then, when you run `serverless deploy`, VPC configuration will be deployed along The Lambda function execution role must have permissions to create, describe and delete [Elastic Network Interfaces](http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_ElasticNetworkInterfaces.html) (ENI). When VPC configuration is provided the default AWS `AWSLambdaVPCAccessExecutionRole` will be associated with your Lambda execution role. In case custom roles are provided be sure to include the proper [ManagedPolicyArns](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-role.html#cfn-iam-role-managepolicyarns). For more information please check [configuring a Lambda Function for Amazon VPC Access](http://docs.aws.amazon.com/lambda/latest/dg/vpc.html) +**VPC Lambda Internet Access** + +By default, when a Lambda function is executed inside a VPC, it looses internet access and some resources inside AWS may become unavailable. In order for S3 resources and DynamoDB resources to be available for your Lambda function running inside the VPC, a VPC end point needs to be created. For more information please check [VPC Endpoint for Amazon S3](https://aws.amazon.com/blogs/aws/new-vpc-endpoint-for-amazon-s3/). +In order for other services such as Kinesis streams to be made available, a NAT Gateway needs to be configured inside the subnets that are being used to run the Lambda, for the VPC used to execute the Lambda. For more information, please check [Enable Outgoing Internet Access within VPC](https://medium.com/@philippholly/aws-lambda-enable-outgoing-internet-access-within-vpc-8dd250e11e12) + ## Environment Variables You can add environment variable configuration to a specific function in `serverless.yml` by adding an `environment` object property in the function configuration. This object should contain a key/value collection of strings: @@ -305,7 +310,7 @@ These versions are not cleaned up by serverless, so make sure you use a plugin o ## Dead Letter Queue (DLQ) -When AWS lambda functions fail, they are [retried](http://docs.aws.amazon.com/lambda/latest/dg/retries-on-errors.html). If the retries also fail, AWS has a feature to send information about the failed request to a SNS topic or SNS queue, called the [Dead Letter Queue](http://docs.aws.amazon.com/lambda/latest/dg/dlq.html), which you can use to track and diagnose and react to lambda failures. +When AWS lambda functions fail, they are [retried](http://docs.aws.amazon.com/lambda/latest/dg/retries-on-errors.html). If the retries also fail, AWS has a feature to send information about the failed request to a SNS topic or SQS queue, called the [Dead Letter Queue](http://docs.aws.amazon.com/lambda/latest/dg/dlq.html), which you can use to track and diagnose and react to lambda failures. You can setup a dead letter queue for your serverless functions with the help of a SNS topic and the `onError` config parameter. diff --git a/docs/providers/aws/guide/resources.md b/docs/providers/aws/guide/resources.md index cbddc1290..0fc8bb3be 100644 --- a/docs/providers/aws/guide/resources.md +++ b/docs/providers/aws/guide/resources.md @@ -70,7 +70,7 @@ We're also using the term `normalizedName` or similar terms in this guide. This |Lambda::Function | {normalizedFunctionName}LambdaFunction | HelloLambdaFunction | |Lambda::Version | {normalizedFunctionName}LambdaVersion{sha256} | HelloLambdaVersionr3pgoTvv1xT4E4NiCL6JG02fl6vIyi7OS1aW0FwAI | |Logs::LogGroup | {normalizedFunctionName}LogGroup | HelloLogGroup | -|Lambda::Permission | | | +|Lambda::Permission | | | |Events::Rule | | | |AWS::Logs::SubscriptionFilter | {normalizedFuntionName}LogsSubscriptionFilterCloudWatchLog{SequentialID} | HelloLogsSubscriptionFilterCloudWatchLog1 | |AWS::IoT::TopicRule | {normalizedFuntionName}IotTopicRule{SequentialID} | HelloIotTopicRule1 | @@ -111,6 +111,7 @@ functions: resources: Resources: WriteDashPostLogGroup: + Type: AWS::Logs::LogGroup Properties: RetentionInDays: "30" ``` diff --git a/docs/providers/aws/guide/serverless.yml.md b/docs/providers/aws/guide/serverless.yml.md index 2b699727a..e52f887c6 100644 --- a/docs/providers/aws/guide/serverless.yml.md +++ b/docs/providers/aws/guide/serverless.yml.md @@ -30,7 +30,7 @@ provider: region: us-east-1 # Overwrite the default region used. Default is us-east-1 profile: production # The default profile to use with this service memorySize: 512 # Overwrite the default memory size. Default is 1024 - timeout: 10 # The default is 6 seconds + timeout: 10 # The default is 6 seconds. Note: API Gateway current maximum is 30 seconds deploymentBucket: name: com.serverless.${self:provider.region}.deploys # Deployment bucket name. Default is generated by the framework serverSideEncryption: AES256 # when using server-side encryption @@ -55,13 +55,13 @@ provider: key: value iamRoleStatements: # IAM role statements so that services can be accessed in the AWS account - Effect: 'Allow' - Action: - - 's3:ListBucket' - Resource: - Fn::Join: - - '' - - - 'arn:aws:s3:::' - - Ref: ServerlessDeploymentBucket + Action: + - 's3:ListBucket' + Resource: + Fn::Join: + - '' + - - 'arn:aws:s3:::' + - Ref: ServerlessDeploymentBucket stackPolicy: # Optional CF stack policy. The example below allows updates to all resources except deleting/replacing EC2 instances (use with caution!) - Effect: Allow Principal: "*" @@ -150,6 +150,9 @@ functions: startingPosition: LATEST enabled: false - alexaSkill + - alexaSmartHome: + appId: amzn1.ask.skill.xx-xx-xx-xx + enabled: true - iot: name: myIoTEvent description: An IoT event diff --git a/docs/providers/aws/guide/variables.md b/docs/providers/aws/guide/variables.md index 54e704ad1..b4db4067b 100644 --- a/docs/providers/aws/guide/variables.md +++ b/docs/providers/aws/guide/variables.md @@ -12,22 +12,63 @@ layout: Doc # Variables -The Serverless framework provides a powerful variable system which allows you to add dynamic data into your `serverless.yml`. With Serverless Variables, you'll be able to do the following: +Variables allow users to dynamically replace config values in `serverless.yml` config. -- Reference & load variables from environment variables -- Reference & load variables from CLI options -- Reference & load variables from CloudFormation stack outputs -- Recursively reference properties of any type from the same `serverless.yml` file -- Recursively reference properties of any type from other YAML/JSON files -- Recursively reference properties exported from JS files, synchronously or asynchronously -- Recursively nest variable references within each other for ultimate flexibility -- Combine multiple variable references to overwrite each other -- Define your own variable syntax if it conflicts with CF syntax -- Reference & load variables from S3 -- Reference & load variables from SSM +They are especially useful when providing secrets for your service to use and when you are working with multiple stages. + +## Syntax + +To use variables, you will need to reference values enclosed in `${}` brackets. + +```yml +# serverless.yml file +yamlKeyXYZ: ${variableSource} # see list of current variable sources below +# this is an example of providing a default value as the second parameter +otherYamlKey: ${variableSource, defaultValue} +``` + +You can define your own variable syntax (regex) if it conflicts with CloudFormation's syntax. **Note:** You can only use variables in `serverless.yml` property **values**, not property keys. So you can't use variables to generate dynamic logical IDs in the custom resources section for example. +## Current variable sources: + +- [environment variables](https://serverless.com/framework/docs/providers/aws/guide/variables#referencing-environment-variables) +- [CLI options](https://serverless.com/framework/docs/providers/aws/guide/variables#referencing-cli-options) +- [other properties defined in `serverless.yml`](https://serverless.com/framework/docs/providers/aws/guide/variables#reference-properties-in-serverlessyml) +- [external YAML/JSON files](https://serverless.com/framework/docs/providers/aws/guide/variables#reference-variables-in-other-files) +- [variables from S3](https://serverless.com/framework/docs/providers/aws/guide/variables#referencing-s3-objects) +- [variables from AWS SSM Parameter Store](https://serverless.com/framework/docs/providers/aws/guide/variables#reference-variables-using-the-ssm-parameter-store) +- [CloudFormation stack outputs](https://serverless.com/framework/docs/providers/aws/guide/variables#reference-cloudformation-outputs) +- [properties exported from Javascript files (sync or async)](https://serverless.com/framework/docs/providers/aws/guide/variables#reference-variables-in-javascript-files) + +## Recursively reference properties + +You can also **Recursively reference properties** with the variable system. This means you can combine multiple values and variable sources for a lot of flexibility. + +For example: + +```yml +provider: + name: aws + stage: ${opt:stage, 'dev'} + environment: + MY_SECRET: ${file(./config.${self:provider.stage}.json):CREDS} +``` + +If `sls deploy --stage qa` is ran, the option `stage=qa` is used inside the `${file(./config.${self:provider.stage}.json):CREDS}` variable and it will resolve the `config.qa.json` file and use the `CREDS` key defined. + +**How that works:** + +1. stage is set to `qa` from the option supplied to the `sls deploy --stage qa` command +2. `${self:provider.stage}` resolves to `qa` and is used in `${file(./config.${self:provider.stage}.json):CREDS}` +3. `${file(./config.qa.json):CREDS}` is found & the `CREDS` value is read +4. `MY_SECRET` value is set + +Likewise, if `sls deploy --stage prod` is ran the `config.prod.json` file would be found and used. + +If no `--stage` flag is provided, the second parameter defined in `${opt:stage, 'dev'}` a.k.a `dev` will be used and result in `${file(./config.dev.json):CREDS}`. + ## Reference Properties In serverless.yml To self-reference properties in `serverless.yml`, use the `${self:someProperty}` syntax in your `serverless.yml`. `someProperty` can contain the empty string for a top-level self-reference or a dotted attribute reference to any depth of attribute, so you can go as shallow or deep in the object tree as you want. @@ -205,7 +246,7 @@ In your `serverless.yml`, depending on the type of your source file, either have functions: hello: handler: handler.hello - events: ${file(./myCustomFile.yml):myevents + events: ${file(./myCustomFile.yml):myevents} ``` or for a JSON reference file use this sytax: @@ -214,7 +255,7 @@ or for a JSON reference file use this sytax: functions: hello: handler: handler.hello - events: ${file(./myCustomFile.json):myevents + events: ${file(./myCustomFile.json):myevents} ``` ## Reference Variables in Javascript Files diff --git a/docs/providers/azure/README.md b/docs/providers/azure/README.md index d0f7300f7..e7ad1f63a 100644 --- a/docs/providers/azure/README.md +++ b/docs/providers/azure/README.md @@ -61,6 +61,7 @@ If you have questions, join the [chat in gitter](https://gitter.im/serverless/se
  • Plugin Search
  • Plugin Install
  • Plugin Uninstall
  • +
  • Print
  • diff --git a/docs/providers/azure/cli-reference/create.md b/docs/providers/azure/cli-reference/create.md index 742af639f..998a09cef 100644 --- a/docs/providers/azure/cli-reference/create.md +++ b/docs/providers/azure/cli-reference/create.md @@ -28,7 +28,8 @@ serverless create --template azure-nodejs --path myService ``` ## Options -- `--template` or `-t` The name of one of the available templates. **Required**. +- `--template` or `-t` The name of one of the available templates. **Required if --template-url is not present**. +- `--template-url` or `-u` The name of one of the available templates. **Required if --template is not present**. - `--path` or `-p` The path where the service should be created. - `--name` or `-n` the name of the service in `serverless.yml`. @@ -68,3 +69,9 @@ Serverless will use the already present directory. Additionally Serverless will rename the service according to the path you provide. In this example the service will be renamed to `my-new-service`. + +### Create service in new folder using a custom template + +```bash +serverless create --template-url https://github.com/serverless/serverless/tree/master/lib/plugins/create/templates/azure-nodejs --path myService +``` diff --git a/docs/providers/azure/cli-reference/print.md b/docs/providers/azure/cli-reference/print.md new file mode 100644 index 000000000..5a4c65077 --- /dev/null +++ b/docs/providers/azure/cli-reference/print.md @@ -0,0 +1,69 @@ + + + +### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/azure/cli-reference/print) + + +# Print + +Print your `serverless.yml` config file with all variables resolved. + +If you're using [Serverless Variables](https://serverless.com/framework/docs/providers/azure/guide/variables/) +in your `serverless.yml`, it can be difficult to know if your syntax is correct +or if the variables are resolving as you expect. + +With this command, it will print the fully-resolved config to your console. + +```bash +serverless print +``` + +## Options + +- None + +## Examples: + +Assuming you have the following config file: + +```yml +service: new-service +provider: azure +custom: + globalSchedule: cron(0 * * * *) + +functions: + hello: + handler: handler.hello + events: + - timer: ${self:custom.globalSchedule} + world: + handler: handler.world + events: + - timer: ${self:custom.globalSchedule} +``` + +Using `sls print` will resolve the variables in the `timer` blocks. + +```bash +service: new-service +provider: azure +custom: + globalSchedule: cron(0 * * * *) + +functions: + hello: + handler: handler.hello + events: + - timer: cron(0 * * * *) # <-- Resolved + world: + handler: handler.world + events: + - timer: cron(0 * * * *) # <-- Resolved +``` diff --git a/docs/providers/azure/events/timer.md b/docs/providers/azure/events/timer.md index ff44f911a..929d0bc11 100644 --- a/docs/providers/azure/events/timer.md +++ b/docs/providers/azure/events/timer.md @@ -33,7 +33,7 @@ functions: events: - timer: x-azure-settings: - name: item #, default - "myQueueItem", specifies which name it's available on `context.bindings` + name: timerObj #, default - "myTimer", specifies which name it's available on `context.bindings` schedule: 0 */5 * * * * #, cron expression to run on ``` diff --git a/docs/providers/google/README.md b/docs/providers/google/README.md index 5010a5b41..e355f2ec8 100644 --- a/docs/providers/google/README.md +++ b/docs/providers/google/README.md @@ -62,6 +62,7 @@ If you have questions, join the [chat in gitter](https://gitter.im/serverless/se
  • Plugin Search
  • Plugin Install
  • Plugin Uninstall
  • +
  • Print
  • diff --git a/docs/providers/google/cli-reference/create.md b/docs/providers/google/cli-reference/create.md index 834c10f6d..232c84bdc 100644 --- a/docs/providers/google/cli-reference/create.md +++ b/docs/providers/google/cli-reference/create.md @@ -28,7 +28,8 @@ serverless create --template google-nodejs --path my-service ## Options -- `--template` or `-t` The name of one of the available templates. **Required**. +- `--template` or `-t` The name of one of the available templates. **Required if --template-url is not present**. +- `--template-url` or `-u` The name of one of the available templates. **Required if --template is not present**. - `--path` or `-p` The path where the service should be created. - `--name` or `-n` the name of the service in `serverless.yml`. @@ -59,3 +60,9 @@ serverless create --template google-nodejs --path my-new-service This example will generate scaffolding for a service with `google` as a provider and `nodejs` as runtime. The scaffolding will be generated in the `my-new-service` directory. This directory will be created if not present. Otherwise Serverless will use the already present directory. Additionally Serverless will rename the service according to the path you provide. In this example the service will be renamed to `my-new-service`. + +### Create service in new folder using a custom template + +```bash +serverless create --template-url https://github.com/serverless/serverless/tree/master/lib/plugins/create/templates/google-nodejs --path myService +``` diff --git a/docs/providers/google/cli-reference/print.md b/docs/providers/google/cli-reference/print.md new file mode 100644 index 000000000..c71b0410a --- /dev/null +++ b/docs/providers/google/cli-reference/print.md @@ -0,0 +1,80 @@ + + + +### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/google/cli-reference/print) + + +# Print + +Print your `serverless.yml` config file with all variables resolved. + +If you're using [Serverless Variables](https://serverless.com/framework/docs/providers/google/guide/variables/) +in your `serverless.yml`, it can be difficult to know if your syntax is correct +or if the variables are resolving as you expect. + +With this command, it will print the fully-resolved config to your console. + +```bash +serverless print +``` + +## Options + +- None + +## Examples: + +Assuming you have the following config file: + +```yml +service: new-service +provider: google + +custom: + resource: projects/*/topics/my-topic + +functions: + first: + handler: firstPubSub + events: + - event: + eventType: providers/cloud.pubsub/eventTypes/topics.publish + resource: ${self:custom.resource} + second: + handler: secondPubSub + events: + - event: + eventType: providers/cloud.pubsub/eventTypes/topics.publish + resource: ${self:custom.resource} +``` + +Using `sls print` will resolve the variables in the `resource` blocks: + +```bash +$ sls print +service: new-service +provider: google + +custom: + resource: projects/*/topics/my-topic + +functions: + first: + handler: firstPubSub + events: + - event: + eventType: providers/cloud.pubsub/eventTypes/topics.publish + resource: projects/*/topics/my-topic # <-- Resolved. + second: + handler: secondPubSub + events: + - event: + eventType: providers/cloud.pubsub/eventTypes/topics.publish + resource: projects/*/topics/my-topic # <-- Resolved. +``` diff --git a/docs/providers/google/guide/credentials.md b/docs/providers/google/guide/credentials.md index 60b858614..fa46773b9 100644 --- a/docs/providers/google/guide/credentials.md +++ b/docs/providers/google/guide/credentials.md @@ -28,7 +28,7 @@ If necessary, a more detailed guide on creating a Billing Account can be found < A Google Cloud Project is required to use Google Cloud Functions. Here's how to create one: -1. Go to the Google Cloud Console Console. +1. Go to the Google Cloud Console. 2. There is a dropdown near the top left of the screen (near the search bar that lists your projects). Click it and select "Create Project". 3. Enter a Project name and select the Billing Account you created in the steps above (or any Billing Account with a valid credit card attached). 3. Click on "Create" to start the creation process. diff --git a/docs/providers/kubeless/README.md b/docs/providers/kubeless/README.md index 082347329..b9bba665a 100644 --- a/docs/providers/kubeless/README.md +++ b/docs/providers/kubeless/README.md @@ -68,6 +68,7 @@ If you have questions, join the [chat in gitter](https://gitter.im/serverless/se diff --git a/docs/providers/kubeless/cli-reference/create.md b/docs/providers/kubeless/cli-reference/create.md index a331f2801..7911ef281 100644 --- a/docs/providers/kubeless/cli-reference/create.md +++ b/docs/providers/kubeless/cli-reference/create.md @@ -35,7 +35,8 @@ serverless create --template kubeless-nodejs --path my-service ``` ## Options -- `--template` or `-t` The name of one of the available templates. **Required**. +- `--template` or `-t` The name of one of the available templates. **Required if --template-url is not present**. +- `--template-url` or `-u` The name of one of the available templates. **Required if --template is not present**. - `--path` or `-p` The path where the service should be created. - `--name` or `-n` the name of the service in `serverless.yml`. @@ -71,4 +72,4 @@ serverless create --template kubeless-python --path my-new-service This example will generate scaffolding for a service with `kubeless` as a provider and `python2.7` as runtime. The scaffolding will be generated in the `my-new-service` directory. This directory will be created if not present. Otherwise Serverless will use the already present directory. -Additionally Serverless will rename the service according to the path you provide. In this example the service will be renamed to `my-new-service`. \ No newline at end of file +Additionally Serverless will rename the service according to the path you provide. In this example the service will be renamed to `my-new-service`. diff --git a/docs/providers/kubeless/cli-reference/remove.md b/docs/providers/kubeless/cli-reference/remove.md index 2b6992a49..ea1bd14b6 100644 --- a/docs/providers/kubeless/cli-reference/remove.md +++ b/docs/providers/kubeless/cli-reference/remove.md @@ -20,5 +20,8 @@ serverless remove It will remove the Kubeless Function objects from your Kubernetes cluster, the Kubernetes Deployments and the Kubernetes Services associated with the Serverless service. +## Options +- `--verbose` or `-v` Shows additional information during the removal. + ## Provided lifecycle events - `remove:remove` diff --git a/docs/providers/kubeless/events/http.md b/docs/providers/kubeless/events/http.md index a6440fe42..8196f46f2 100644 --- a/docs/providers/kubeless/events/http.md +++ b/docs/providers/kubeless/events/http.md @@ -84,10 +84,6 @@ If the events HTTP definitions contain a `path` attribute, when deploying this S ``` kubectl get ingress -NAME HOSTS ADDRESS PORTS AGE -ingress-create * 192.168.99.100 80 2m -ingress-delete * 192.168.99.100 80 2m -ingress-read-all * 192.168.99.100 80 2m -ingress-read-one * 192.168.99.100 80 2m -ingress-update * 192.168.99.100 80 2m +NAME HOSTS ADDRESS PORTS AGE +ingress-1506350705094 192.168.99.100.nip.io 80 28s ``` \ No newline at end of file diff --git a/docs/providers/kubeless/events/scheduler.md b/docs/providers/kubeless/events/scheduler.md new file mode 100644 index 000000000..387bcfe63 --- /dev/null +++ b/docs/providers/kubeless/events/scheduler.md @@ -0,0 +1,34 @@ + + + +### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/kubeless/events/schedule) + + +# Kubeless Scheduled Events + +Kubeless functions can be triggered following a certain schedule. The schedule can be specified events section of the `serverless.yml` following the Cron notation: + +``` +service: clock + +provider: + name: kubeless + runtime: nodejs6 + +plugins: + - serverless-kubeless + +functions: + clock: + handler: handler.printClock + events: + - schedule: "* * * * *" +``` + +When deploying this `serverless.yml` file, Kubeless will create a Kubernetes cron job that will trigger the function `printClock` every minute. diff --git a/docs/providers/kubeless/guide/deploying.md b/docs/providers/kubeless/guide/deploying.md index 9c8288c1d..cdb4883d9 100644 --- a/docs/providers/kubeless/guide/deploying.md +++ b/docs/providers/kubeless/guide/deploying.md @@ -76,7 +76,7 @@ Kubeless will create a [Kubernetes Deployment](https://kubernetes.io/docs/concep ## Deploy Function -This deployment method updates a single function. It performs the platform API call to deploy your package without the other resources. It is much faster than redeploying your whole service each time. +This deployment method updates or deploys a single function. It performs the platform API call to deploy your package without the other resources. It is much faster than redeploying your whole service each time. ```bash serverless deploy function --function myFunction diff --git a/docs/providers/kubeless/guide/installation.md b/docs/providers/kubeless/guide/installation.md index c1a1a181a..483a87516 100644 --- a/docs/providers/kubeless/guide/installation.md +++ b/docs/providers/kubeless/guide/installation.md @@ -26,7 +26,7 @@ Go to the official [Node.js website](https://nodejs.org), download and follow th **Note:** Serverless runs on Node v4 or higher. -You can verify that Node.js is installed successfully by runnning `node --version` in your terminal. You should see the corresponding Node version number printed out. +You can verify that Node.js is installed successfully by running `node --version` in your terminal. You should see the corresponding Node version number printed out. ## Installing the Serverless Framework diff --git a/docs/providers/kubeless/guide/intro.md b/docs/providers/kubeless/guide/intro.md index 521658292..a6227c941 100644 --- a/docs/providers/kubeless/guide/intro.md +++ b/docs/providers/kubeless/guide/intro.md @@ -24,7 +24,7 @@ Here are the Serverless Framework's main concepts and how they pertain to Kubele ### Functions -A Function is an [Kubeless Function](http://kubeless.io/). It's an independent unit of deployment, like a microservice. It's merely code, deployed in the cloud, that is most often written to perform a single job such as: +A Function is a [Kubeless Function](http://kubeless.io/). It's an independent unit of deployment, like a microservice. It's merely code, deployed in the cloud, that is most often written to perform a single job such as: * *Saving a user to the database* * *Processing a file in a database* diff --git a/docs/providers/openwhisk/README.md b/docs/providers/openwhisk/README.md index e09360f45..73330b2b5 100644 --- a/docs/providers/openwhisk/README.md +++ b/docs/providers/openwhisk/README.md @@ -67,6 +67,7 @@ If you have questions, join the [chat in gitter](https://gitter.im/serverless/se
  • Plugin Search
  • Plugin Install
  • Plugin Uninstall
  • +
  • Print
  • Serverless Stats
  • diff --git a/docs/providers/openwhisk/cli-reference/create.md b/docs/providers/openwhisk/cli-reference/create.md index 12af9bba5..5c5201e47 100644 --- a/docs/providers/openwhisk/cli-reference/create.md +++ b/docs/providers/openwhisk/cli-reference/create.md @@ -27,7 +27,8 @@ serverless create --template openwhisk-nodejs --path myService ``` ## Options -- `--template` or `-t` The name of one of the available templates. **Required**. +- `--template` or `-t` The name of one of the available templates. **Required if --template-url is not present**. +- `--template-url` or `-u` The name of one of the available templates. **Required if --template is not present**. - `--path` or `-p` The path where the service should be created. - `--name` or `-n` the name of the service in `serverless.yml`. @@ -68,6 +69,12 @@ This example will generate scaffolding for a service with `openwhisk` as a provi Additionally Serverless will rename the service according to the path you provide. In this example the service will be renamed to `my-new-service`. +### Create service in new folder using a custom template + +```bash +serverless create --template-url https://github.com/serverless/serverless/tree/master/lib/plugins/create/templates/openwhisk-nodejs --path myService +``` + ### Creating a new plugin ``` diff --git a/docs/providers/openwhisk/cli-reference/print.md b/docs/providers/openwhisk/cli-reference/print.md new file mode 100644 index 000000000..e10ced465 --- /dev/null +++ b/docs/providers/openwhisk/cli-reference/print.md @@ -0,0 +1,70 @@ + + + +### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/openwhisk/cli-reference/print) + + +# Print + +Print your `serverless.yml` config file with all variables resolved. + +If you're using [Serverless Variables](https://serverless.com/framework/docs/providers/openwhisk/guide/variables/) +in your `serverless.yml`, it can be difficult to know if your syntax is correct +or if the variables are resolving as you expect. + +With this command, it will print the fully-resolved config to your console. + +```bash +serverless print +``` + +## Options + +- None + +## Examples: + +Assuming you have the following config file: + +```yml +service: new-service +provider: openwhisk +custom: + globalSchedule: cron(0 * * * *) + +functions: + hello: + handler: handler.hello + events: + - schedule: ${self:custom.globalSchedule} + world: + handler: handler.world + events: + - schedule: ${self:custom.globalSchedule} +``` + +Using `sls print` will resolve the variables in the `schedule` blocks. + +```bash +$ sls print +service: new-service +provider: openwhisk +custom: + globalSchedule: cron(0 * * * *) + +functions: + hello: + handler: handler.hello + events: + - schedule: cron(0 * * * *) # <-- Resolved + world: + handler: handler.world + events: + - schedule: cron(0 * * * *) # <-- Resolved +``` diff --git a/docs/providers/openwhisk/cli-reference/slstats.md b/docs/providers/openwhisk/cli-reference/slstats.md index 78de33a6d..e07997b14 100644 --- a/docs/providers/openwhisk/cli-reference/slstats.md +++ b/docs/providers/openwhisk/cli-reference/slstats.md @@ -1,7 +1,7 @@ diff --git a/docs/providers/openwhisk/events/apigateway.md b/docs/providers/openwhisk/events/apigateway.md index 4583ab870..911926147 100644 --- a/docs/providers/openwhisk/events/apigateway.md +++ b/docs/providers/openwhisk/events/apigateway.md @@ -111,8 +111,15 @@ functions: - http: path: posts/create method: post + resp: json ``` +HTTP event configuration supports the following parameters. + +- `method` - HTTP method (mandatory). +- `path` - URI path for API gateway (mandatory). +- `resp` - controls [web action content type](https://github.com/apache/incubator-openwhisk/blob/master/docs/webactions.md#additional-features), values include: `json`, `html`, `http`, `svg`or `text` (optional, defaults to `json`). + ### CORS Support **Note:** All HTTP endpoints defined in this manner have cross-site requests diff --git a/docs/providers/openwhisk/guide/quick-start.md b/docs/providers/openwhisk/guide/quick-start.md index 79afc26e1..a62bf6419 100644 --- a/docs/providers/openwhisk/guide/quick-start.md +++ b/docs/providers/openwhisk/guide/quick-start.md @@ -13,7 +13,7 @@ layout: Doc 1. Node.js `v6.5.0` or later. 2. Serverless CLI `v1.9.0` or later. You can run `npm install -g serverless` to install it. -3. An IBM Bluemix account. If you don't already have one, you can sign up for an [account](https://aws.amazon.com/s/dm/optimization/server-side-test/free-tier/free_np/) and then follow the instructions for getting access to [OpenWhisk on Bluemix](https://console.ng.bluemix.net/openwhisk/). +3. An IBM Bluemix account. If you don't already have one, you can sign up for an [account](https://console.bluemix.net/registration/) and then follow the instructions for getting access to [OpenWhisk on Bluemix](https://console.ng.bluemix.net/openwhisk/). 4. **Set-up your [Provider Credentials](./credentials.md)**. 5. Install Framework & Dependencies *Due to an [outstanding issue](https://github.com/serverless/serverless/issues/2895) with provider plugins, the [OpenWhisk provider](https://github.com/serverless/serverless-openwhisk) must be installed as a global module.* diff --git a/docs/providers/spotinst/README.md b/docs/providers/spotinst/README.md index 0672b9785..648396c47 100755 --- a/docs/providers/spotinst/README.md +++ b/docs/providers/spotinst/README.md @@ -14,28 +14,74 @@ Welcome to the Serverless Spotinst documentation! If you have questions, join the [chat in gitter](https://gitter.im/serverless/serverless) or [post over on the forums](https://forum.serverless.com/) -## Getting Started + diff --git a/docs/providers/spotinst/cli-reference/create.md b/docs/providers/spotinst/cli-reference/create.md index 28d7af7a1..0009dea57 100755 --- a/docs/providers/spotinst/cli-reference/create.md +++ b/docs/providers/spotinst/cli-reference/create.md @@ -27,7 +27,8 @@ serverless create -t spotinst-nodejs -p myService ``` ## Options -- `--template` or `-t` The name of one of the available templates. **Required**. +- `--template` or `-t` The name of one of the available templates. **Required if --template-url is not present**. +- `--template-url` or `-u` The name of one of the available templates. **Required if --template is not present**. - `--path` or `-p` The path where the service should be created. - `--name` or `-n` the name of the service in `serverless.yml`. diff --git a/docs/providers/spotinst/cli-reference/deploy-function.md b/docs/providers/spotinst/cli-reference/deploy-function.md index b8dc2a867..5d350188b 100644 --- a/docs/providers/spotinst/cli-reference/deploy-function.md +++ b/docs/providers/spotinst/cli-reference/deploy-function.md @@ -33,6 +33,10 @@ functions: handler: handler.main memory: 128 timeout: 30 + access: private +# cron: # Setup scheduled trigger with cron expression +# active: true +# value: '* * * * *' # extend the framework using plugins listed here: # https://github.com/serverless/plugins diff --git a/docs/providers/spotinst/events/http.md b/docs/providers/spotinst/events/http.md index 2454a6ee3..4ac098b08 100755 --- a/docs/providers/spotinst/events/http.md +++ b/docs/providers/spotinst/events/http.md @@ -12,18 +12,8 @@ layout: Doc # HTTP -Spotinst Functions can be triggered by an HTTP endpoint. To create HTTP endpoints as event sources for your Spotinst Functions, use the `http` event syntax. +Spotinst Functions are automatically given an HTTP endpoint when they are created. This means that you do not need to specify the event type when writing your function. After you deploy your function for the first time a unique URL is generated based on the application ID, environment where your application is launched, and the function ID. Here is a sample of how the URL is created -This setup specifies that the `first` function should be run when someone accesses the Functions API endpoint via a `GET` request. You can get the URL for the endpoint by running the `serverless info` command after deploying your service. +`https://{app id}{environment id}.spotinst.io/{function id}` -Here's an example: - -```yml -# serverless.yml - -functions: - first: - handler: http - events: - - http: path -``` +For information on your application ID, environment ID and function ID please checkout your Spotinst Functions dashboard on the [Spotinst website](https://console.spotinst.com/#/dashboard) \ No newline at end of file diff --git a/docs/providers/spotinst/events/schedule.md b/docs/providers/spotinst/events/schedule.md index 19621cbeb..bf8cf48b8 100755 --- a/docs/providers/spotinst/events/schedule.md +++ b/docs/providers/spotinst/events/schedule.md @@ -12,14 +12,38 @@ layout: Doc # Schedule -You can trigger the functions by using a scheduled event. This will execute the function according to the cron expressions you specify +You can trigger the functions by using a scheduled event. This will execute the function according to the cron expressions you specify. -You can either use the `rate` or `cron` syntax. +You can use `cron` syntax. + +The following example is a function configuration in the serverless.yml file that are scheduled to trigger the function crawl every day at 6:30 PM. ```yml functions: crawl: - handler: crawl - events: - - schedule: cron(0 12 * * ? *) + handler: handler.crawl + cron: # Setup scheduled trigger with cron expression + active: true + value: '30 18 * * *' ``` + + +## Active Status + +You also have the option to set your functions active status as either true or false + +**Note** `schedule` events active status are set to true by default + +This example will create and attach a schedule event for the function `crawl` which is active status is set to `false`. If the status is changed to true the `crawl` function will be called every Monday at 6:00 PM. + +```yml +functions: + crawl: + handler: handler.crawl + cron: # Setup scheduled trigger with cron expression + active: false + value: '* 18 * * 1' + +``` + +**Note** When creating a `cron` trigger the `value` is the crontab expression. For help on crontab check out the [documentation](http://www.adminschoice.com/crontab-quick-reference) diff --git a/docs/providers/spotinst/examples/README.md b/docs/providers/spotinst/examples/README.md new file mode 100644 index 000000000..126965022 --- /dev/null +++ b/docs/providers/spotinst/examples/README.md @@ -0,0 +1,18 @@ + + + +### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/examples/hello-world/) + + +# Hello World Serverless Example + +Pick your language of choice: + +* [JavaScript](./node) +* [Python](./python) +* [Ruby](./ruby) diff --git a/docs/providers/spotinst/examples/node/README.md b/docs/providers/spotinst/examples/node/README.md new file mode 100644 index 000000000..a238f7512 --- /dev/null +++ b/docs/providers/spotinst/examples/node/README.md @@ -0,0 +1,48 @@ + + + +### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/spotinst/) + + +# Hello World JavaScript Example + +Make sure `serverless` is installed. + +## 1. Create a service +`serverless create --template spotinst-nodejs --path serviceName` `serviceName` is going to be a new directory there the python template will be loaded. Once the download is complete change into that directory. Next you will need to install the Spotinst Serverless Functions plugin by running `npm install` in the root directory + +## 2. Deploy +`serverless deploy` + +## 3. Invoke deployed function +`serverless invoke --function hello` + +In your terminal window you should see the response + +```bash +{ +Deploy functions: + hello: created +Service Information + service: spotinst-python + functions: + hello +} +``` + +Congrats you have just deployed and ran your Hello World function! + +## Short Hand Guide + +`sls` is short hand for serverless cli commands + +`-f` is short hand for `--function` + +`-t` is short hand for `--template` + +`-p` is short hang for `--path` \ No newline at end of file diff --git a/docs/providers/spotinst/examples/python/README.md b/docs/providers/spotinst/examples/python/README.md new file mode 100644 index 000000000..fe3c8e1ee --- /dev/null +++ b/docs/providers/spotinst/examples/python/README.md @@ -0,0 +1,49 @@ + + + +### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/spotinst/) + + +# Hello World Python Example + +Make sure `serverless` is installed. + +## 1. Create a service +`serverless create --template spotinst-python --path serviceName` `serviceName` is going to be a new directory there the python template will be loaded. Once the download is complete change into that directory. Next you will need to install the Spotinst Serverless Functions plugin by running `npm install` in the root directory + + +## 2. Deploy +`serverless deploy` + +## 3. Invoke deployed function +`serverless invoke --function hello` + +In your terminal window you should see the response + +```bash +{ +Deploy functions: + hello: created +Service Information + service: spotinst-python + functions: + hello +} +``` + +Congrats you have just deployed and ran your Hello World function! + +## Short Hand Guide + +`sls` is short hand for serverless cli commands + +`-f` is short hand for `--function` + +`-t` is short hand for `--template` + +`-p` is short hang for `--path` \ No newline at end of file diff --git a/docs/providers/spotinst/examples/ruby/README.md b/docs/providers/spotinst/examples/ruby/README.md new file mode 100644 index 000000000..39789b49b --- /dev/null +++ b/docs/providers/spotinst/examples/ruby/README.md @@ -0,0 +1,49 @@ + + + +### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/spotinst/) + + +# Hello World Ruby Example + +Make sure `serverless` is installed. + +## 1. Create a service +`serverless create --template spotinst-ruby --path serviceName` `serviceName` is going to be a new directory there the python template will be loaded. Once the download is complete change into that directory. Next you will need to install the Spotinst Serverless Functions plugin by running `npm install` in the root directory + + +## 2. Deploy +`serverless deploy` + +## 3. Invoke deployed function +`serverless invoke --function hello` + +In your terminal window you should see the response + +```bash +{ +Deploy functions: + hello: created +Service Information + service: spotinst-ruby + functions: + hello +} +``` + +Congrats you have just deployed and ran your Hello World function! + +## Short Hand Guide + +`sls` is short hand for serverless cli commands + +`-f` is short hand for `--function` + +`-t` is short hand for `--template` + +`-p` is short hang for `--path` \ No newline at end of file diff --git a/docs/providers/spotinst/guide/create-token.md b/docs/providers/spotinst/guide/create-token.md new file mode 100644 index 000000000..9fbaf1d9b --- /dev/null +++ b/docs/providers/spotinst/guide/create-token.md @@ -0,0 +1,72 @@ + + + +### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/spotinst/guide/credentials) + + +# Spotinst Functions - Create Token + +The Serverless Framework needs access to your Spotinst account so that it can create and manage resources on your behalf. To do this you will need either a permanent or tempary token that is linked to your account + +## Create a Permanent Token + +You can generate a Permanent Token from the [Spotinst Console](https://console.spotinst.com/#/settings/tokens/permanent). + +> `WARNING`: Do not share your personal access token or your application secret with anyone outside your organization. Please contact our support if you’re concerned your token has been compromised. + +## Temporary Access Token +You can also generate a the temporary access token, which is only valid for 2 hours (7200 seconds). + +You can generate a temporary token from the [Spotinst Console](https://console.spotinst.com/#/settings/tokens/temporary). Or, using the below command: + +```bash +$ curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'username=&password=&grant_type=password&client_id=&client_secret=' https://oauth.spotinst.io/token +``` + +Replace the following parameters, more info can be found [here](https://console.spotinst.com/#/settings/tokens/temporary) + - `` + - `` + - `` + - `` + +The request will return two tokens: +```json +{ + "request": { + "id": "a2285a3f-4950-4874-a931-1ee1cdf33012", + "url": "/token", + "method": "POST", + "timestamp": "2017-08-30T22:00:34.610Z" + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "kind": "spotinst:oauth2:token", + "items": [ + { + "accessToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzcG90aW5zdCIsInVpZCI6LTgsIm9pZCI6NjA2MDc5ODYxOTExLCJyb2xlIjoyLCJleHAiOjE1MDQxMzc2MzQsImlhdCI6MTUwNDEzMDQzNH0.xyax", + "tokenType": "bearer", + "expiresIn": 7199 + } + ], + "count": 1 + } +} +``` + +* *accessToken* - Use this token when making calls to Spotinst API +* *refreshToken* - Use this token in order to refresh the temporary token. This will return a new token that is valid for additional 2 hours: + + +```bash +$ curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'refresh_token=&grant_type=refresh_token&client_id=&client_secret=' https://api.spotinst.io/token +``` + diff --git a/docs/providers/spotinst/guide/credentials.md b/docs/providers/spotinst/guide/credentials.md index 6266c551c..c42e86a5e 100644 --- a/docs/providers/spotinst/guide/credentials.md +++ b/docs/providers/spotinst/guide/credentials.md @@ -1,7 +1,7 @@ @@ -12,60 +12,32 @@ layout: Doc # Spotinst Functions - Credentials -The Serverless Framework needs access to your Spotinst account so that it can create and manage resources on your behalf. +The Serverless Framework needs access to your Spotinst account so that it can create and manage resources on your behalf. Please make sure you have created and saved your Permanent or Temporary Token before continuing. -## Create a Permanent Token +## Configure Credentials -You can generate a Permanent Token from the [Spotinst Console](https://console.spotinst.com/#/settings/tokens/permanent). +You will need to have your account ID number and your account token ready. Your account ID can be found on the Spotinst console and your token can be generated by following the [Create Token Guide](./create-token.md). -> `WARNING`: Do not share your personal access token or your application secret with anyone outside your organization. Please contact our support if you’re concerned your token has been compromised. +In order to run the config credentials command from the terminal you will need to start a new Spotinst project and install the plugin. First you will need to run the create a new project using the Spotinst template. To do this run: -## Temporary Access Token -You can also generate a the temporary access token, which is only valid for 2 hours (7200 seconds). - -You can generate a temporary token from the [Spotinst Console](https://console.spotinst.com/#/settings/tokens/temporary). Or, using the below command: - -```bash -$ curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'username=&password=&grant_type=password&client_id=&client_secret=' https://oauth.spotinst.io/token +``` +serverless create -t spotinst-nodejs -p new-function ``` -Replace the following parameters, more info can be found [here](https://console.spotinst.com/#/settings/tokens/temporary) - - `` - - `` - - `` - - `` +Then navigate to the directory that was just created `new-function` and install the Spotinst Serverless Plugin by running the `npm install` command. -The request will return two tokens: -```json -{ - "request": { - "id": "a2285a3f-4950-4874-a931-1ee1cdf33012", - "url": "/token", - "method": "POST", - "timestamp": "2017-08-30T22:00:34.610Z" - }, - "response": { - "status": { - "code": 200, - "message": "OK" - }, - "kind": "spotinst:oauth2:token", - "items": [ - { - "accessToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzcG90aW5zdCIsInVpZCI6LTgsIm9pZCI6NjA2MDc5ODYxOTExLCJyb2xlIjoyLCJleHAiOjE1MDQxMzc2MzQsImlhdCI6MTUwNDEzMDQzNH0.xyax", - "tokenType": "bearer", - "expiresIn": 7199 - } - ], - "count": 1 - } -} +Once this has completed you will be able to configure your credentials by running + +``` +serverless config credentials -p spotinst -k {your account number} -t {your token} ``` -* *accessToken* - Use this token when making calls to Spotinst API -* *refreshToken* - Use this token in order to refresh the temporary token. This will return a new token that is valid for additional 2 hours: +This will create a ~/.spotinst/credentials file the file should look like this when if done correctly: +``` +default: + token: {your token} + account: {your account number} +``` -```bash -$ curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'refresh_token=&grant_type=refresh_token&client_id=&client_secret=' https://api.spotinst.io/token -``` \ No newline at end of file +After this is set up properly you will be able to deploy functions from your computer and monitor them on the Spotinst Console. diff --git a/docs/providers/spotinst/guide/intro.md b/docs/providers/spotinst/guide/intro.md index 6ff12b465..767d2ed62 100644 --- a/docs/providers/spotinst/guide/intro.md +++ b/docs/providers/spotinst/guide/intro.md @@ -92,6 +92,10 @@ functions: handler: handler.main memory: 128 timeout: 30 +# access: private +# cron: # Setup scheduled trigger with cron expression +# active: true +# value: '* * * * *' ``` When you deploy with the Framework by running `serverless deploy`, everything in `serverless.yml` is deployed at once. @@ -105,4 +109,4 @@ You can overwrite or extend the functionality of the Framework using **Plugins** plugins: - serverless-spotinst-functions -``` \ No newline at end of file +``` diff --git a/docs/providers/spotinst/guide/quick-start.md b/docs/providers/spotinst/guide/quick-start.md index 00a17c253..d54d4dc0f 100644 --- a/docs/providers/spotinst/guide/quick-start.md +++ b/docs/providers/spotinst/guide/quick-start.md @@ -1,7 +1,7 @@ diff --git a/docs/providers/spotinst/guide/serverless.yml.md b/docs/providers/spotinst/guide/serverless.yml.md new file mode 100644 index 000000000..5c60b26b2 --- /dev/null +++ b/docs/providers/spotinst/guide/serverless.yml.md @@ -0,0 +1,51 @@ + +# Serverless.yml Reference + +This is an outline of a `serverless.yml` file with descriptions of the properties for reference + +```yml +# serverless.yml + +# The service can be whatever you choose. You can have multiple functions +# under one service + +service: your-service + +# The provider is Spotinst and the Environment ID can be found on the +# Spotinst Console under Functions + +provider: + name: spotinst + spotinst: + environment: #{Your Environment ID} + +# Here is where you will list your functions for this service. Each Function is +# required to have a name, runtime, handler, memory and timeout. The runtime is +# the language that you want to run your function with, the handler tells which +# file and function to run, memory is the amount of memory needed to run your +# function, timeout is the time the function will take to run, if it goes over +# this time it will terminate itself. Access is default set to private so if you +# want to be able to run the function by HTTPS request this needs to be set to +# public. For information on cron functions read the post here. + +functions: + function-name: + runtime: nodejs4.8 + handler: handler.main + memory: 128 + timeout: 30 +# access: public +# cron: +# active: false +# value: '*/1 * * * *' + + +plugins: + - serverless-spotinst-functions +``` diff --git a/docs/providers/webtasks/cli-reference/create.md b/docs/providers/webtasks/cli-reference/create.md index 64633059f..56da10709 100755 --- a/docs/providers/webtasks/cli-reference/create.md +++ b/docs/providers/webtasks/cli-reference/create.md @@ -28,7 +28,8 @@ serverless create --template webtasks-nodejs --path my-service ## Options -- `--template` or `-t` The name of one of the available templates. **Required**. +- `--template` or `-t` The name of one of the available templates. **Required if --template-url is not present**. +- `--template-url` or `-u` The name of one of the available templates. **Required if --template is not present**. - `--path` or `-p` The path where the service should be created. - `--name` or `-n` the name of the service in `serverless.yml`. diff --git a/docs/providers/webtasks/guide/intro.md b/docs/providers/webtasks/guide/intro.md index 6174e3f9e..3dce6f97b 100644 --- a/docs/providers/webtasks/guide/intro.md +++ b/docs/providers/webtasks/guide/intro.md @@ -14,7 +14,7 @@ layout: Doc The Serverless Framework helps you develop and deploy serverless applications using Auth0 Webtasks. The Serverless CLI offers structure, automation and best practices out-of-the-box. And with Auth0 Webtasks it's simple and easy to deploy code in just seconds. -**Note:** A local profile is required to use Auth0 Webtasks with the Serverless Framework. Follow the steps in the [Quick Start](../quick-start.md) to get setup in less than a minute. +**Note:** A local profile is required to use Auth0 Webtasks with the Serverless Framework. Follow the steps in the [Quick Start](quick-start.md) to get setup in less than a minute. ## Core Concepts diff --git a/lib/classes/PluginManager.js b/lib/classes/PluginManager.js index 75fd3d89e..7cf4f75da 100644 --- a/lib/classes/PluginManager.js +++ b/lib/classes/PluginManager.js @@ -90,17 +90,23 @@ class PluginManager { this.addPlugin(Plugin); } catch (error) { - // Rethrow the original error in case we're in debug mode. - if (process.env.SLS_DEBUG) { - throw error; + let errorMessage; + if (error && error.code === 'MODULE_NOT_FOUND' && error.message.endsWith(`'${plugin}'`)) { + // Plugin not installed + errorMessage = [ + `Serverless plugin "${plugin}" not found.`, + ' Make sure it\'s installed and listed in the "plugins" section', + ' of your serverless config file.', + ].join(''); + } else { + // Plugin initialization error + // Rethrow the original error in case we're in debug mode. + if (process.env.SLS_DEBUG) { + throw error; + } + errorMessage = + `Serverless plugin "${plugin}" initialization errored: ${error.message}`; } - - const errorMessage = [ - `Serverless plugin "${plugin}" not found.`, - ' Make sure it\'s installed and listed in the "plugins" section', - ' of your serverless config file.', - ].join(''); - if (!this.cliOptions.help) { throw new this.serverless.classes.Error(errorMessage); } diff --git a/lib/classes/Utils.js b/lib/classes/Utils.js index 7c01b147c..e73a5d374 100644 --- a/lib/classes/Utils.js +++ b/lib/classes/Utils.js @@ -8,8 +8,10 @@ const fse = BbPromise.promisifyAll(require('fs-extra')); const _ = require('lodash'); const fileExistsSync = require('../utils/fs/fileExistsSync'); const writeFileSync = require('../utils/fs/writeFileSync'); +const copyDirContentsSync = require('../utils/fs/copyDirContentsSync'); const readFileSync = require('../utils/fs/readFileSync'); const walkDirSync = require('../utils/fs/walkDirSync'); +const dirExistsSync = require('../utils/fs/dirExistsSync'); const isDockerContainer = require('is-docker'); const version = require('../../package.json').version; const segment = require('../utils/segment'); @@ -25,12 +27,7 @@ class Utils { } dirExistsSync(dirPath) { - try { - const stats = fse.statSync(dirPath); - return stats.isDirectory(); - } catch (e) { - return false; - } + return dirExistsSync(dirPath); } fileExistsSync(filePath) { @@ -92,12 +89,7 @@ class Utils { } copyDirContentsSync(srcDir, destDir) { - const fullFilesPaths = this.walkDirSync(srcDir); - - fullFilesPaths.forEach(fullFilePath => { - const relativeFilePath = fullFilePath.replace(srcDir, ''); - fse.copySync(fullFilePath, path.join(destDir, relativeFilePath)); - }); + return copyDirContentsSync(srcDir, destDir); } generateShortId(length) { diff --git a/lib/plugins/Plugins.json b/lib/plugins/Plugins.json index e6cd67bdb..161303d0d 100644 --- a/lib/plugins/Plugins.json +++ b/lib/plugins/Plugins.json @@ -12,6 +12,7 @@ "./login/login.js", "./logout/logout.js", "./metrics/metrics.js", + "./print/print.js", "./remove/remove.js", "./rollback/index.js", "./slstats/slstats.js", @@ -40,6 +41,7 @@ "./aws/package/compile/events/sns/index.js", "./aws/package/compile/events/stream/index.js", "./aws/package/compile/events/alexaSkill/index.js", + "./aws/package/compile/events/alexaSmartHome/index.js", "./aws/package/compile/events/iot/index.js", "./aws/package/compile/events/cloudWatchEvent/index.js", "./aws/package/compile/events/cloudWatchLog/index.js", diff --git a/lib/plugins/aws/deploy/lib/createStack.test.js b/lib/plugins/aws/deploy/lib/createStack.test.js index 898b80618..084625628 100644 --- a/lib/plugins/aws/deploy/lib/createStack.test.js +++ b/lib/plugins/aws/deploy/lib/createStack.test.js @@ -11,6 +11,7 @@ const testUtils = require('../../../../../tests/utils'); describe('createStack', () => { let awsDeploy; + let sandbox; const tmpDirPath = testUtils.getTmpDirPath(); const serverlessYmlPath = path.join(tmpDirPath, 'serverless.yml'); @@ -38,13 +39,21 @@ describe('createStack', () => { awsDeploy.serverless.cli = new serverless.classes.CLI(); }); + beforeEach(() => { + sandbox = sinon.sandbox.create(); + }); + + afterEach(() => { + sandbox.restore(); + }); + describe('#create()', () => { it('should include custom stack tags', () => { awsDeploy.serverless.service.provider.stackTags = { STAGE: 'overridden', tag1: 'value1' }; - const createStackStub = sinon + const createStackStub = sandbox .stub(awsDeploy.provider, 'request').resolves(); - sinon.stub(awsDeploy, 'monitorStack').resolves(); + sandbox.stub(awsDeploy, 'monitorStack').resolves(); return awsDeploy.create().then(() => { expect(createStackStub.args[0][2].Tags) @@ -52,33 +61,29 @@ describe('createStack', () => { { Key: 'STAGE', Value: 'overridden' }, { Key: 'tag1', Value: 'value1' }, ]); - awsDeploy.provider.request.restore(); - awsDeploy.monitorStack.restore(); }); }); it('should use CloudFormation service role ARN if it is specified', () => { awsDeploy.serverless.service.provider.cfnRole = 'arn:aws:iam::123456789012:role/myrole'; - const createStackStub = sinon + const createStackStub = sandbox .stub(awsDeploy.provider, 'request').resolves(); - sinon.stub(awsDeploy, 'monitorStack').resolves(); + sandbox.stub(awsDeploy, 'monitorStack').resolves(); return awsDeploy.create().then(() => { expect(createStackStub.args[0][2].RoleARN) .to.equal('arn:aws:iam::123456789012:role/myrole'); - awsDeploy.provider.request.restore(); - awsDeploy.monitorStack.restore(); }); }); }); describe('#createStack()', () => { it('should resolve if stack already created', () => { - const createStub = sinon + const createStub = sandbox .stub(awsDeploy, 'create').resolves(); - sinon.stub(awsDeploy.provider, 'request').resolves(); + sandbox.stub(awsDeploy.provider, 'request').resolves(); return awsDeploy.createStack().then(() => { expect(createStub.called).to.be.equal(false); @@ -87,7 +92,7 @@ describe('createStack', () => { it('should set the createLater flag and resolve if deployment bucket is provided', () => { awsDeploy.serverless.service.provider.deploymentBucket = 'serverless'; - sinon.stub(awsDeploy.provider, 'request') + sandbox.stub(awsDeploy.provider, 'request') .returns(BbPromise.reject({ message: 'does not exist' })); return awsDeploy.createStack().then(() => { @@ -100,7 +105,7 @@ describe('createStack', () => { message: 'Something went wrong.', }; - sinon.stub(awsDeploy.provider, 'request').rejects(errorMock); + sandbox.stub(awsDeploy.provider, 'request').rejects(errorMock); const createStub = sinon .stub(awsDeploy, 'create').resolves(); @@ -117,7 +122,7 @@ describe('createStack', () => { message: 'does not exist', }; - sinon.stub(awsDeploy.provider, 'request').rejects(errorMock); + sandbox.stub(awsDeploy.provider, 'request').rejects(errorMock); const createStub = sinon .stub(awsDeploy, 'create').resolves(); diff --git a/lib/plugins/aws/deploy/lib/extendedValidate.js b/lib/plugins/aws/deploy/lib/extendedValidate.js index 0c670d128..2a1bf15e3 100644 --- a/lib/plugins/aws/deploy/lib/extendedValidate.js +++ b/lib/plugins/aws/deploy/lib/extendedValidate.js @@ -29,6 +29,29 @@ module.exports = { this.serverless.service.package.artifact = path .join(this.serverless.config.servicePath, '.serverless', state.package.artifact); } + + // Check function's attached to API Gateway timeout + if (!_.isEmpty(this.serverless.service.functions)) { + this.serverless.service.getAllFunctions().forEach(functionName => { + const functionObject = this.serverless.service.getFunction(functionName); + + // Check if function timeout is greater than API Gateway timeout + if (functionObject.timeout > 30 && functionObject.events) { + functionObject.events.forEach(event => { + if (Object.keys(event)[0] === 'http') { + const warnMessage = [ + `WARNING: Function ${functionName} has timeout of ${functionObject.timeout} `, + 'seconds, however, it\'s attached to API Gateway so it\'s automatically ', + 'limited to 30 seconds.', + ].join(''); + + this.serverless.cli.log(warnMessage); + } + }); + } + }); + } + if (!_.isEmpty(this.serverless.service.functions) && this.serverless.service.package.individually) { // artifact file validation (multiple function artifacts) diff --git a/lib/plugins/aws/deploy/lib/extendedValidate.test.js b/lib/plugins/aws/deploy/lib/extendedValidate.test.js index 5e0d010b1..2e453a038 100644 --- a/lib/plugins/aws/deploy/lib/extendedValidate.test.js +++ b/lib/plugins/aws/deploy/lib/extendedValidate.test.js @@ -42,7 +42,9 @@ describe('extendedValidate', () => { }; awsDeploy = new AwsDeploy(serverless, options); awsDeploy.serverless.service.service = `service-${(new Date()).getTime().toString()}`; - awsDeploy.serverless.cli = new serverless.classes.CLI(); + awsDeploy.serverless.cli = { + log: sinon.spy(), + }; }); describe('extendedValidate()', () => { @@ -148,5 +150,30 @@ describe('extendedValidate', () => { delete awsDeploy.serverless.service.package.artifact; }); }); + + it('should warn if function\'s timeout is greater than 30 and it\'s attached to APIGW', () => { + stateFileMock.service.functions = { + first: { + timeout: 31, + package: { + artifact: 'artifact.zip', + }, + events: [{ + http: {}, + }], + }, + }; + awsDeploy.serverless.service.package.individually = true; + fileExistsSyncStub.returns(true); + readFileSyncStub.returns(stateFileMock); + + return awsDeploy.extendedValidate().then(() => { + const msg = [ + 'WARNING: Function first has timeout of 31 seconds, however, it\'s ', + 'attached to API Gateway so it\'s automatically limited to 30 seconds.', + ].join(''); + expect(awsDeploy.serverless.cli.log.firstCall.calledWithExactly(msg)).to.be.equal(true); + }); + }); }); }); diff --git a/lib/plugins/aws/deployFunction/index.js b/lib/plugins/aws/deployFunction/index.js index 3b098f8b3..4d347b8d9 100644 --- a/lib/plugins/aws/deployFunction/index.js +++ b/lib/plugins/aws/deployFunction/index.js @@ -73,6 +73,47 @@ class AwsDeployFunction { }); } + normalizeArnRole(role) { + if (typeof role === 'string') { + if (role.indexOf(':') === -1) { + const roleResource = this.serverless.service.resources.Resources[role]; + + if (roleResource.Type !== 'AWS::IAM::Role') { + throw new Error('Provided resource is not IAM Role.'); + } + + const roleProperties = roleResource.Properties; + const compiledFullRoleName = `${roleProperties.Path || '/'}${roleProperties.RoleName}`; + + return this.provider.getAccountId().then((accountId) => + `arn:aws:iam::${accountId}:role${compiledFullRoleName}` + ); + } + + return BbPromise.resolve(role); + } + + return this.provider.request( + 'IAM', + 'getRole', + { + RoleName: role['Fn::GetAtt'][0], + }, + this.options.stage, this.options.region + ).then((data) => data.Arn); + } + + callUpdateFunctionConfiguration(params) { + return this.provider.request( + 'Lambda', + 'updateFunctionConfiguration', + params, + this.options.stage, this.options.region + ).then(() => { + this.serverless.cli.log(`Successfully updated function: ${this.options.function}`); + }); + } + updateFunctionConfiguration() { const functionObj = this.options.functionObj; const serviceObj = this.serverless.service.serviceObject; @@ -97,12 +138,6 @@ class AwsDeployFunction { params.MemorySize = providerObj.memorySize; } - if ('role' in functionObj) { - params.Role = functionObj.role; - } else if ('role' in providerObj) { - params.Role = providerObj.role; - } - if ('timeout' in functionObj) { params.Timeout = functionObj.timeout; } else if ('timeout' in providerObj) { @@ -148,14 +183,21 @@ class AwsDeployFunction { params.VpcConfig.SubnetIds = providerObj.vpc.subnetIds; } - return this.provider.request( - 'Lambda', - 'updateFunctionConfiguration', - params, - this.options.stage, this.options.region - ).then(() => { - this.serverless.cli.log(`Successfully updated function: ${this.options.function}`); - }); + if ('role' in functionObj) { + return this.normalizeArnRole(functionObj.role).then(roleArn => { + params.Role = roleArn; + + return this.callUpdateFunctionConfiguration(params); + }); + } else if ('role' in providerObj) { + return this.normalizeArnRole(providerObj.role).then(roleArn => { + params.Role = roleArn; + + return this.callUpdateFunctionConfiguration(params); + }); + } + + return this.callUpdateFunctionConfiguration(params); } deployFunction() { diff --git a/lib/plugins/aws/deployFunction/index.test.js b/lib/plugins/aws/deployFunction/index.test.js index e51eebcc0..5e6def27f 100644 --- a/lib/plugins/aws/deployFunction/index.test.js +++ b/lib/plugins/aws/deployFunction/index.test.js @@ -116,8 +116,73 @@ describe('AwsDeployFunction', () => { }); }); + describe('#normalizeArnRole', () => { + let getAccountIdStub; + let getRoleStub; + + beforeEach(() => { + getAccountIdStub = sinon + .stub(awsDeployFunction.provider, 'getAccountId') + .resolves('123456789012'); + getRoleStub = sinon + .stub(awsDeployFunction.provider, 'request') + .resolves({ Arn: 'arn:aws:iam::123456789012:role/role_2' }); + + serverless.service.resources = { + Resources: { + MyCustomRole: { + Type: 'AWS::IAM::Role', + Properties: { + RoleName: 'role_123', + }, + }, + }, + }; + }); + + afterEach(() => { + awsDeployFunction.provider.getAccountId.restore(); + awsDeployFunction.provider.request.restore(); + serverless.service.resources = undefined; + }); + + it('should return unmodified ARN if ARN was provided', () => { + const arn = 'arn:aws:iam::123456789012:role/role'; + + return awsDeployFunction.normalizeArnRole(arn).then((result) => { + expect(getAccountIdStub.calledOnce).to.be.equal(false); + expect(result).to.be.equal(arn); + }); + }); + + it('should return compiled ARN if role name was provided', () => { + const roleName = 'MyCustomRole'; + + return awsDeployFunction.normalizeArnRole(roleName).then((result) => { + expect(getAccountIdStub.calledOnce).to.be.equal(true); + expect(result).to.be.equal('arn:aws:iam::123456789012:role/role_123'); + }); + }); + + it('should return compiled ARN if object role was provided', () => { + const roleObj = { + 'Fn::GetAtt': [ + 'role_2', + 'Arn', + ], + }; + + return awsDeployFunction.normalizeArnRole(roleObj).then((result) => { + expect(getRoleStub.calledOnce).to.be.equal(true); + expect(getAccountIdStub.calledOnce).to.be.equal(false); + expect(result).to.be.equal('arn:aws:iam::123456789012:role/role_2'); + }); + }); + }); + describe('#updateFunctionConfiguration', () => { let updateFunctionConfigurationStub; + let normalizeArnRoleStub; const options = { stage: 'dev', region: 'us-east-1', @@ -131,10 +196,15 @@ describe('AwsDeployFunction', () => { updateFunctionConfigurationStub = sinon .stub(awsDeployFunction.provider, 'request') .resolves(); + + normalizeArnRoleStub = sinon + .stub(awsDeployFunction, 'normalizeArnRole') + .resolves('arn:aws:us-east-1:123456789012:role/role'); }); afterEach(() => { awsDeployFunction.provider.request.restore(); + awsDeployFunction.normalizeArnRole.restore(); awsDeployFunction.serverless.service.provider.timeout = undefined; awsDeployFunction.serverless.service.provider.memorySize = undefined; awsDeployFunction.serverless.service.provider.role = undefined; @@ -162,6 +232,8 @@ describe('AwsDeployFunction', () => { awsDeployFunction.options = options; return awsDeployFunction.updateFunctionConfiguration().then(() => { + expect(normalizeArnRoleStub.calledOnce).to.be.equal(true); + expect(normalizeArnRoleStub.calledWithExactly('arn:aws:iam::123456789012:role/Admin')); expect(updateFunctionConfigurationStub.calledOnce).to.be.equal(true); expect(updateFunctionConfigurationStub.calledWithExactly( 'Lambda', @@ -179,7 +251,7 @@ describe('AwsDeployFunction', () => { FunctionName: 'first', KMSKeyArn: 'arn:aws:kms:us-east-1:123456789012', MemorySize: 128, - Role: 'arn:aws:iam::123456789012:role/Admin', + Role: 'arn:aws:us-east-1:123456789012:role/role', Timeout: 3, VpcConfig: { SecurityGroupIds: ['1'], @@ -247,6 +319,8 @@ describe('AwsDeployFunction', () => { awsDeployFunction.options = options; return awsDeployFunction.updateFunctionConfiguration().then(() => { + expect(normalizeArnRoleStub.calledOnce).to.be.equal(true); + expect(normalizeArnRoleStub.calledWithExactly('role')); expect(updateFunctionConfigurationStub.calledOnce).to.be.equal(true); expect(updateFunctionConfigurationStub.calledWithExactly( 'Lambda', @@ -260,7 +334,7 @@ describe('AwsDeployFunction', () => { }, Timeout: 12, MemorySize: 512, - Role: 'role', + Role: 'arn:aws:us-east-1:123456789012:role/role', }, awsDeployFunction.options.stage, awsDeployFunction.options.region diff --git a/lib/plugins/aws/invokeLocal/invoke.py b/lib/plugins/aws/invokeLocal/invoke.py index 3d9482473..1078d4b8a 100755 --- a/lib/plugins/aws/invokeLocal/invoke.py +++ b/lib/plugins/aws/invokeLocal/invoke.py @@ -58,6 +58,8 @@ if __name__ == '__main__': handler = getattr(module, args.handler_name) input = json.load(sys.stdin) + if sys.platform != 'win32': + sys.stdin = open('/dev/tty') context = FakeLambdaContext(**input.get('context', {})) result = handler(input['event'], context) sys.stdout.write(json.dumps(result, indent=4)) diff --git a/lib/plugins/aws/lib/naming.js b/lib/plugins/aws/lib/naming.js index 9fe809d77..f2c81c776 100644 --- a/lib/plugins/aws/lib/naming.js +++ b/lib/plugins/aws/lib/naming.js @@ -285,6 +285,10 @@ module.exports = { getLambdaAlexaSkillPermissionLogicalId(functionName) { return `${this.getNormalizedFunctionName(functionName)}LambdaPermissionAlexaSkill`; }, + getLambdaAlexaSmartHomePermissionLogicalId(functionName, alexaSmartHomeIndex) { + return `${this.getNormalizedFunctionName(functionName)}LambdaPermissionAlexaSmartHome${ + alexaSmartHomeIndex}`; + }, getLambdaCloudWatchLogPermissionLogicalId(functionName, logsIndex) { return `${this.getNormalizedFunctionName(functionName) }LambdaPermissionLogsSubscriptionFilterCloudWatchLog${logsIndex}`; diff --git a/lib/plugins/aws/lib/naming.test.js b/lib/plugins/aws/lib/naming.test.js index d028eb967..1b4ab23b0 100644 --- a/lib/plugins/aws/lib/naming.test.js +++ b/lib/plugins/aws/lib/naming.test.js @@ -469,6 +469,14 @@ describe('#naming()', () => { }); }); + describe('#getLambdaAlexaSmartHomePermissionLogicalId()', () => { + it('should normalize the function name and append the standard suffix', + () => { + expect(sdk.naming.getLambdaAlexaSmartHomePermissionLogicalId('functionName', 0)) + .to.equal('FunctionNameLambdaPermissionAlexaSmartHome0'); + }); + }); + describe('#getLambdaSnsSubscriptionLogicalId()', () => { it('should normalize the function name and append the standard suffix', () => { expect(sdk.naming.getLambdaSnsSubscriptionLogicalId('functionName', 'topicName')) diff --git a/lib/plugins/aws/package/compile/events/alexaSmartHome/index.js b/lib/plugins/aws/package/compile/events/alexaSmartHome/index.js new file mode 100644 index 000000000..97d34abf7 --- /dev/null +++ b/lib/plugins/aws/package/compile/events/alexaSmartHome/index.js @@ -0,0 +1,87 @@ +'use strict'; + +const _ = require('lodash'); + +class AwsCompileAlexaSmartHomeEvents { + constructor(serverless) { + this.serverless = serverless; + this.provider = this.serverless.getProvider('aws'); + + this.hooks = { + 'package:compileEvents': this.compileAlexaSmartHomeEvents.bind(this), + }; + } + + compileAlexaSmartHomeEvents() { + this.serverless.service.getAllFunctions().forEach((functionName) => { + const functionObj = this.serverless.service.getFunction(functionName); + let alexaSmartHomeNumberInFunction = 0; + + if (functionObj.events) { + functionObj.events.forEach(event => { + if (event.alexaSmartHome) { + alexaSmartHomeNumberInFunction++; + let EventSourceToken; + let Action; + + if (typeof event.alexaSmartHome === 'object') { + if (!event.alexaSmartHome.appId) { + const errorMessage = [ + `Missing "appId" property for alexaSmartHome event in function ${functionName}`, + ' The correct syntax is: appId: amzn1.ask.skill.xxxx-xxxx', + ' OR an object with "appId" property.', + ' Please check the docs for more info.', + ].join(''); + throw new this.serverless.classes + .Error(errorMessage); + } + EventSourceToken = event.alexaSmartHome.appId; + Action = event.alexaSmartHome.enabled !== false ? + 'lambda:InvokeFunction' : 'lambda:DisableInvokeFunction'; + } else if (typeof event.alexaSmartHome === 'string') { + EventSourceToken = event.alexaSmartHome; + Action = 'lambda:InvokeFunction'; + } else { + const errorMessage = [ + `Alexa Smart Home event of function "${functionName}" is not an object or string.`, + ' The correct syntax is: alexaSmartHome.', + ' Please check the docs for more info.', + ].join(''); + throw new this.serverless.classes.Error(errorMessage); + } + const lambdaLogicalId = this.provider.naming + .getLambdaLogicalId(functionName); + + const permissionTemplate = { + Type: 'AWS::Lambda::Permission', + Properties: { + FunctionName: { + 'Fn::GetAtt': [ + lambdaLogicalId, + 'Arn', + ], + }, + Action: Action.replace(/\\n|\\r/g, ''), + Principal: 'alexa-connectedhome.amazon.com', + EventSourceToken: EventSourceToken.replace(/\\n|\\r/g, ''), + }, + }; + + const lambdaPermissionLogicalId = this.provider.naming + .getLambdaAlexaSmartHomePermissionLogicalId(functionName, + alexaSmartHomeNumberInFunction); + + const permissionCloudFormationResource = { + [lambdaPermissionLogicalId]: permissionTemplate, + }; + + _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, + permissionCloudFormationResource); + } + }); + } + }); + } +} + +module.exports = AwsCompileAlexaSmartHomeEvents; diff --git a/lib/plugins/aws/package/compile/events/alexaSmartHome/index.test.js b/lib/plugins/aws/package/compile/events/alexaSmartHome/index.test.js new file mode 100644 index 000000000..4c0692846 --- /dev/null +++ b/lib/plugins/aws/package/compile/events/alexaSmartHome/index.test.js @@ -0,0 +1,221 @@ +'use strict'; + +const expect = require('chai').expect; +const AwsProvider = require('../../../../provider/awsProvider'); +const AwsCompileAlexaSmartHomeEvents = require('./index'); +const Serverless = require('../../../../../../Serverless'); + +describe('AwsCompileAlexaSmartHomeEvents', () => { + let serverless; + let awsCompileAlexaSmartHomeEvents; + + beforeEach(() => { + serverless = new Serverless(); + serverless.service.provider.compiledCloudFormationTemplate = { Resources: {} }; + serverless.setProvider('aws', new AwsProvider(serverless)); + awsCompileAlexaSmartHomeEvents = new AwsCompileAlexaSmartHomeEvents(serverless); + awsCompileAlexaSmartHomeEvents.serverless.service.service = 'new-service'; + }); + + describe('#constructor()', () => { + it('should set the provider variable to an instance of AwsProvider', () => + expect(awsCompileAlexaSmartHomeEvents.provider).to.be.instanceof(AwsProvider)); + }); + + describe('#compileAlexaSmartHomeEvents()', () => { + it('should throw an error if alexaSmartHome event type is not a string or an object', () => { + awsCompileAlexaSmartHomeEvents.serverless.service.functions = { + first: { + events: [ + { + alexaSmartHome: 42, + }, + ], + }, + }; + + expect(() => awsCompileAlexaSmartHomeEvents.compileAlexaSmartHomeEvents()).to.throw(Error); + }); + + it('should throw an error if the "appId" property is not given', () => { + awsCompileAlexaSmartHomeEvents.serverless.service.functions = { + first: { + events: [ + { + alexaSmartHome: { + appId: null, + }, + }, + ], + }, + }; + + expect(() => awsCompileAlexaSmartHomeEvents.compileAlexaSmartHomeEvents()).to.throw(Error); + }); + + it('should create corresponding resources when alexaSmartHome events are given', () => { + awsCompileAlexaSmartHomeEvents.serverless.service.functions = { + first: { + events: [ + { + alexaSmartHome: { + appId: 'amzn1.ask.skill.xx-xx-xx-xx', + enabled: false, + }, + }, + { + alexaSmartHome: { + appId: 'amzn1.ask.skill.yy-yy-yy-yy', + enabled: true, + }, + }, + { + alexaSmartHome: 'amzn1.ask.skill.zz-zz-zz-zz', + }, + ], + }, + }; + + awsCompileAlexaSmartHomeEvents.compileAlexaSmartHomeEvents(); + + expect(awsCompileAlexaSmartHomeEvents.serverless.service + .provider.compiledCloudFormationTemplate.Resources + .FirstLambdaPermissionAlexaSmartHome1.Type + ).to.equal('AWS::Lambda::Permission'); + expect(awsCompileAlexaSmartHomeEvents.serverless.service + .provider.compiledCloudFormationTemplate.Resources + .FirstLambdaPermissionAlexaSmartHome2.Type + ).to.equal('AWS::Lambda::Permission'); + expect(awsCompileAlexaSmartHomeEvents.serverless.service + .provider.compiledCloudFormationTemplate.Resources + .FirstLambdaPermissionAlexaSmartHome3.Type + ).to.equal('AWS::Lambda::Permission'); + expect(awsCompileAlexaSmartHomeEvents.serverless.service + .provider.compiledCloudFormationTemplate.Resources + .FirstLambdaPermissionAlexaSmartHome1.Properties.FunctionName + ).to.deep.equal({ 'Fn::GetAtt': ['FirstLambdaFunction', 'Arn'] }); + expect(awsCompileAlexaSmartHomeEvents.serverless.service + .provider.compiledCloudFormationTemplate.Resources + .FirstLambdaPermissionAlexaSmartHome2.Properties.FunctionName + ).to.deep.equal({ 'Fn::GetAtt': ['FirstLambdaFunction', 'Arn'] }); + expect(awsCompileAlexaSmartHomeEvents.serverless.service + .provider.compiledCloudFormationTemplate.Resources + .FirstLambdaPermissionAlexaSmartHome3.Properties.FunctionName + ).to.deep.equal({ 'Fn::GetAtt': ['FirstLambdaFunction', 'Arn'] }); + expect(awsCompileAlexaSmartHomeEvents.serverless.service + .provider.compiledCloudFormationTemplate.Resources + .FirstLambdaPermissionAlexaSmartHome1.Properties.Action + ).to.equal('lambda:DisableInvokeFunction'); + expect(awsCompileAlexaSmartHomeEvents.serverless.service + .provider.compiledCloudFormationTemplate.Resources + .FirstLambdaPermissionAlexaSmartHome2.Properties.Action + ).to.equal('lambda:InvokeFunction'); + expect(awsCompileAlexaSmartHomeEvents.serverless.service + .provider.compiledCloudFormationTemplate.Resources + .FirstLambdaPermissionAlexaSmartHome3.Properties.Action + ).to.equal('lambda:InvokeFunction'); + expect(awsCompileAlexaSmartHomeEvents.serverless.service + .provider.compiledCloudFormationTemplate.Resources + .FirstLambdaPermissionAlexaSmartHome1.Properties.Principal + ).to.equal('alexa-connectedhome.amazon.com'); + expect(awsCompileAlexaSmartHomeEvents.serverless.service + .provider.compiledCloudFormationTemplate.Resources + .FirstLambdaPermissionAlexaSmartHome2.Properties.Principal + ).to.equal('alexa-connectedhome.amazon.com'); + expect(awsCompileAlexaSmartHomeEvents.serverless.service + .provider.compiledCloudFormationTemplate.Resources + .FirstLambdaPermissionAlexaSmartHome3.Properties.Principal + ).to.equal('alexa-connectedhome.amazon.com'); + expect(awsCompileAlexaSmartHomeEvents.serverless.service + .provider.compiledCloudFormationTemplate.Resources + .FirstLambdaPermissionAlexaSmartHome1.Properties.EventSourceToken + ).to.equal('amzn1.ask.skill.xx-xx-xx-xx'); + expect(awsCompileAlexaSmartHomeEvents.serverless.service + .provider.compiledCloudFormationTemplate.Resources + .FirstLambdaPermissionAlexaSmartHome2.Properties.EventSourceToken + ).to.equal('amzn1.ask.skill.yy-yy-yy-yy'); + expect(awsCompileAlexaSmartHomeEvents.serverless.service + .provider.compiledCloudFormationTemplate.Resources + .FirstLambdaPermissionAlexaSmartHome3.Properties.EventSourceToken + ).to.equal('amzn1.ask.skill.zz-zz-zz-zz'); + }); + + it('should respect enabled variable, defaulting to true', () => { + awsCompileAlexaSmartHomeEvents.serverless.service.functions = { + first: { + events: [ + { + alexaSmartHome: { + appId: 'amzn1.ask.skill.xx-xx-xx-xx', + enabled: false, + }, + }, + { + alexaSmartHome: { + appId: 'amzn1.ask.skill.yy-yy-yy-yy', + enabled: true, + }, + }, + { + alexaSmartHome: { + appId: 'amzn1.ask.skill.jj-jj-jj-jj', + }, + }, + { + alexaSmartHome: 'amzn1.ask.skill.zz-zz-zz-zz', + }, + ], + }, + }; + + awsCompileAlexaSmartHomeEvents.compileAlexaSmartHomeEvents(); + + expect(awsCompileAlexaSmartHomeEvents.serverless.service + .provider.compiledCloudFormationTemplate.Resources + .FirstLambdaPermissionAlexaSmartHome1.Properties.Action + ).to.equal('lambda:DisableInvokeFunction'); + expect(awsCompileAlexaSmartHomeEvents.serverless.service + .provider.compiledCloudFormationTemplate.Resources + .FirstLambdaPermissionAlexaSmartHome2.Properties.Action + ).to.equal('lambda:InvokeFunction'); + expect(awsCompileAlexaSmartHomeEvents.serverless.service + .provider.compiledCloudFormationTemplate.Resources + .FirstLambdaPermissionAlexaSmartHome3.Properties.Action + ).to.equal('lambda:InvokeFunction'); + expect(awsCompileAlexaSmartHomeEvents.serverless.service + .provider.compiledCloudFormationTemplate.Resources + .FirstLambdaPermissionAlexaSmartHome4.Properties.Action + ).to.equal('lambda:InvokeFunction'); + }); + + it('should not create corresponding resources when alexaSmartHome events are not given', () => { + awsCompileAlexaSmartHomeEvents.serverless.service.functions = { + first: { + events: [ + 'alexaSkill', + ], + }, + }; + + awsCompileAlexaSmartHomeEvents.compileAlexaSmartHomeEvents(); + + expect( + awsCompileAlexaSmartHomeEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources + ).to.deep.equal({}); + }); + + it('should not create corresponding resources when events are not given', () => { + awsCompileAlexaSmartHomeEvents.serverless.service.functions = { + first: {}, + }; + + awsCompileAlexaSmartHomeEvents.compileAlexaSmartHomeEvents(); + + expect( + awsCompileAlexaSmartHomeEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources + ).to.deep.equal({}); + }); + }); +}); diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/authorizers.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/authorizers.js index 84b861684..e041f45fb 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/authorizers.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/authorizers.js @@ -37,7 +37,7 @@ module.exports = { '/invocations', ], ] }; - authorizerProperties.Type = 'TOKEN'; + authorizerProperties.Type = authorizer.type ? authorizer.type.toUpperCase() : 'TOKEN'; } _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, { diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/authorizers.test.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/authorizers.test.js index aa32f062c..ee84ad8a2 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/authorizers.test.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/authorizers.test.js @@ -96,6 +96,53 @@ describe('#compileAuthorizers()', () => { }); }); + it('should apply optional provided type value to Authorizer Type', () => { + awsCompileApigEvents.validated.events = [{ + http: { + path: 'users/create', + method: 'POST', + authorizer: { + name: 'authorizer', + arn: 'foo', + resultTtlInSeconds: 500, + identityValidationExpression: 'regex', + type: 'request', + }, + }, + }]; + + return awsCompileApigEvents.compileAuthorizers().then(() => { + const resource = awsCompileApigEvents.serverless.service.provider + .compiledCloudFormationTemplate.Resources.AuthorizerApiGatewayAuthorizer; + + expect(resource.Type).to.equal('AWS::ApiGateway::Authorizer'); + expect(resource.Properties.Type).to.equal('REQUEST'); + }); + }); + + it('should apply TOKEN as authorizer Type when not given a type value', () => { + awsCompileApigEvents.validated.events = [{ + http: { + path: 'users/create', + method: 'POST', + authorizer: { + name: 'authorizer', + arn: 'foo', + resultTtlInSeconds: 500, + identityValidationExpression: 'regex', + }, + }, + }]; + + return awsCompileApigEvents.compileAuthorizers().then(() => { + const resource = awsCompileApigEvents.serverless.service.provider + .compiledCloudFormationTemplate.Resources.AuthorizerApiGatewayAuthorizer; + + expect(resource.Type).to.equal('AWS::ApiGateway::Authorizer'); + expect(resource.Properties.Type).to.equal('TOKEN'); + }); + }); + it('should create a valid cognito user pool authorizer', () => { awsCompileApigEvents.validated.events = [{ http: { diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/method/index.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/method/index.js index e8e040b2b..ac0147d42 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/method/index.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/method/index.js @@ -26,6 +26,8 @@ module.exports = { if (event.http.private) { template.Properties.ApiKeyRequired = true; + } else { + template.Properties.ApiKeyRequired = false; } const methodLogicalId = this.provider.naming diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/method/index.test.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/method/index.test.js index c81ed7edb..5811f7312 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/method/index.test.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/method/index.test.js @@ -538,6 +538,24 @@ describe('#compileMethods()', () => { }); }); + it('should set api key as not required if private property is not specified', () => { + awsCompileApigEvents.validated.events = [ + { + functionName: 'First', + http: { + path: 'users/create', + method: 'post', + }, + }, + ]; + return awsCompileApigEvents.compileMethods().then(() => { + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.ApiGatewayMethodUsersCreatePost.Properties.ApiKeyRequired + ).to.equal(false); + }); + }); + it('should set the correct lambdaUri', () => { awsCompileApigEvents.validated.events = [ { 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 1d7e55b9d..247d07367 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/validate.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/validate.js @@ -251,6 +251,10 @@ module.exports = { throw new this.serverless.classes.Error('Please provide either an authorizer name or ARN'); } + if (!type) { + type = authorizer.type; + } + resultTtlInSeconds = Number.parseInt(authorizer.resultTtlInSeconds, 10); resultTtlInSeconds = Number.isNaN(resultTtlInSeconds) ? 300 : resultTtlInSeconds; claims = authorizer.claims || []; 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 6e9c6a12b..15fae5f22 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 @@ -406,6 +406,33 @@ describe('#validate()', () => { expect(authorizer.identityValidationExpression).to.equal('foo'); }); + it('should accept authorizer config with a type', () => { + awsCompileApigEvents.serverless.service.functions = { + foo: {}, + first: { + events: [ + { + http: { + method: 'GET', + path: 'foo/bar', + authorizer: { + name: 'foo', + type: 'request', + resultTtlInSeconds: 500, + identitySource: 'method.request.header.Custom', + identityValidationExpression: 'foo', + }, + }, + }, + ], + }, + }; + + const validated = awsCompileApigEvents.validate(); + const authorizer = validated.events[0].http.authorizer; + expect(authorizer.type).to.equal('request'); + }); + it('should accept authorizer config when resultTtlInSeconds is 0', () => { awsCompileApigEvents.serverless.service.functions = { foo: {}, diff --git a/lib/plugins/aws/provider/awsProvider.test.js b/lib/plugins/aws/provider/awsProvider.test.js index ca78c99b5..8e8389f5a 100644 --- a/lib/plugins/aws/provider/awsProvider.test.js +++ b/lib/plugins/aws/provider/awsProvider.test.js @@ -167,11 +167,11 @@ describe('AwsProvider', () => { return { send(cb) { if (first) { + first = false; cb(error); } else { cb(undefined, {}); } - first = false; }, }; } diff --git a/lib/plugins/aws/rollback/index.js b/lib/plugins/aws/rollback/index.js index 9df1c17d0..037fa0bd0 100644 --- a/lib/plugins/aws/rollback/index.js +++ b/lib/plugins/aws/rollback/index.js @@ -25,10 +25,21 @@ class AwsRollback { this.hooks = { 'before:rollback:initialize': () => BbPromise.bind(this) .then(this.validate), - 'rollback:rollback': () => BbPromise.bind(this) - .then(this.setBucketName) - .then(this.setStackToUpdate) - .then(this.updateStack), + 'rollback:rollback': () => { + if (!this.options.timestamp) { + const command = this.serverless.pluginManager.spawn('deploy:list'); + this.serverless.cli.log([ + 'Use a timestamp from the deploy list below to rollback to a specific version.', + 'Run `sls rollback -t YourTimeStampHere`', + ].join('\n')); + return command; + } + + return BbPromise.bind(this) + .then(this.setBucketName) + .then(this.setStackToUpdate) + .then(this.updateStack); + }, }; } diff --git a/lib/plugins/aws/rollback/index.test.js b/lib/plugins/aws/rollback/index.test.js index fcb25375c..e0e22e9dc 100644 --- a/lib/plugins/aws/rollback/index.test.js +++ b/lib/plugins/aws/rollback/index.test.js @@ -10,9 +10,11 @@ const sinon = require('sinon'); describe('AwsRollback', () => { let awsRollback; let s3Key; + let spawnStub; + let serverless; beforeEach(() => { - const serverless = new Serverless(); + serverless = new Serverless(); const options = { stage: 'dev', region: 'us-east-1', @@ -20,11 +22,16 @@ describe('AwsRollback', () => { }; serverless.setProvider('aws', new AwsProvider(serverless)); serverless.service.service = 'rollback'; + spawnStub = sinon.stub(serverless.pluginManager, 'spawn'); awsRollback = new AwsRollback(serverless, options); awsRollback.serverless.cli = new serverless.classes.CLI(); s3Key = `serverless/${serverless.service.service}/${options.stage}`; }); + afterEach(() => { + serverless.pluginManager.spawn.restore(); + }); + describe('#constructor()', () => { it('should have hooks', () => expect(awsRollback.hooks).to.be.not.empty); @@ -58,6 +65,15 @@ describe('AwsRollback', () => { .to.be.equal(true); }); }); + + it('should run "deploy:list" if timestamp is not specified', () => { + const spawnDeployListStub = spawnStub.withArgs('deploy:list').resolves(); + awsRollback.options.timestamp = undefined; + + return awsRollback.hooks['rollback:rollback']().then(() => { + expect(spawnDeployListStub.calledOnce).to.be.equal(true); + }); + }); }); describe('#setStackToUpdate()', () => { diff --git a/lib/plugins/create/create.js b/lib/plugins/create/create.js index cb2851a31..03880eddf 100644 --- a/lib/plugins/create/create.js +++ b/lib/plugins/create/create.js @@ -4,7 +4,9 @@ const BbPromise = require('bluebird'); const path = require('path'); const fse = require('fs-extra'); const _ = require('lodash'); + const userStats = require('../../utils/userStats'); +const download = require('../../utils/downloadTemplateFromRepo'); // class wide constants const validTemplates = [ @@ -31,6 +33,7 @@ const validTemplates = [ 'openwhisk-swift', 'spotinst-nodejs', 'spotinst-python', + 'spotinst-ruby', 'webtasks-nodejs', 'plugin', @@ -56,9 +59,12 @@ class Create { options: { template: { usage: `Template for the service. Available templates: ${humanReadableTemplateList}`, - required: true, shortcut: 't', }, + 'template-url': { + usage: 'Template URL for the service. Supports: GitHub, BitBucket', + shortcut: 'u', + }, path: { usage: 'The path where the service should be created (e.g. --path my-service)', shortcut: 'p', @@ -79,6 +85,42 @@ class Create { create() { this.serverless.cli.log('Generating boilerplate...'); + + if ('template' in this.options) { + this.createFromTemplate(); + } else if ('template-url' in this.options) { + return download.downloadTemplateFromRepo( + this.options['template-url'], + this.options.name, + this.options.path + ) + .then(dirName => { + const message = [ + `Successfully installed "${dirName}" `, + `${this.options.name && this.options.name !== dirName ? `as "${dirName}"` : ''}`, + ].join(''); + + this.serverless.cli.log(message); + + userStats.track('service_created', { + template: this.options.template, + serviceName: this.options.name, + }); + }) + .catch(err => { + throw new this.serverless.classes.Error(err); + }); + } else { + const errorMessage = [ + 'You must either pass a template name (--template) or a ', + 'a URL (--template-url).', + ].join(''); + throw new this.serverless.classes.Error(errorMessage); + } + return BbPromise.resolve(); + } + + createFromTemplate() { const notPlugin = this.options.template !== 'plugin'; if (validTemplates.indexOf(this.options.template) === -1) { @@ -180,8 +222,6 @@ class Create { this.serverless.cli .log('NOTE: Please update the "service" property in serverless.yml with your service name'); } - - return BbPromise.resolve(); } } diff --git a/lib/plugins/create/create.test.js b/lib/plugins/create/create.test.js index 3ec10d292..d60491cf6 100644 --- a/lib/plugins/create/create.test.js +++ b/lib/plugins/create/create.test.js @@ -9,6 +9,7 @@ const Serverless = require('../../Serverless'); const sinon = require('sinon'); const testUtils = require('../../../tests/utils'); const walkDirSync = require('../../utils/fs/walkDirSync'); +const download = require('./../../utils/downloadTemplateFromRepo'); describe('Create', () => { let create; @@ -462,6 +463,19 @@ describe('Create', () => { }); }); + it('should generate scaffolding for "spotinst-ruby" template', () => { + process.chdir(tmpDir); + create.options.template = 'spotinst-ruby'; + + return create.create().then(() => { + const dirContent = fs.readdirSync(tmpDir); + expect(dirContent).to.include('package.json'); + expect(dirContent).to.include('serverless.yml'); + expect(dirContent).to.include('handler.rb'); + expect(dirContent).to.include('.gitignore'); + }); + }); + it('should generate scaffolding for "webtasks-nodejs" template', () => { process.chdir(tmpDir); create.options.template = 'webtasks-nodejs'; @@ -606,5 +620,30 @@ describe('Create', () => { expect(() => create.create()).to.throw(Error); }); + + it('should reject if download fails', (done) => { + sinon.stub(download, 'downloadTemplateFromRepo'); + + create.options = {}; + create.options['template-url'] = 'https://github.com/serverless/serverless'; + + download.downloadTemplateFromRepo.rejects(new Error('Wrong')); + + create + .create() + .catch(() => download.downloadTemplateFromRepo.restore()) + .then(() => done()); + }); + + it('should resolve if download succeeds', () => { + sinon.stub(download, 'downloadTemplateFromRepo'); + + create.options = {}; + create.options['template-url'] = 'https://github.com/serverless/serverless'; + + download.downloadTemplateFromRepo.resolves(); + + return create.create().catch(() => download.downloadTemplateFromRepo.restore()); + }); }); }); diff --git a/lib/plugins/create/templates/aws-csharp/serverless.yml b/lib/plugins/create/templates/aws-csharp/serverless.yml index 1498ed2a5..fb55c7aac 100644 --- a/lib/plugins/create/templates/aws-csharp/serverless.yml +++ b/lib/plugins/create/templates/aws-csharp/serverless.yml @@ -68,6 +68,7 @@ functions: # - sns: greeter-topic # - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000 # - alexaSkill +# - alexaSmartHome: amzn1.ask.skill.xx-xx-xx-xx # - iot: # sql: "SELECT * FROM 'some_topic'" # - cloudwatchEvent: diff --git a/lib/plugins/create/templates/aws-fsharp/serverless.yml b/lib/plugins/create/templates/aws-fsharp/serverless.yml index 314962e28..21dd15dd3 100644 --- a/lib/plugins/create/templates/aws-fsharp/serverless.yml +++ b/lib/plugins/create/templates/aws-fsharp/serverless.yml @@ -68,6 +68,7 @@ functions: # - sns: greeter-topic # - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000 # - alexaSkill +# - alexaSmartHome: amzn1.ask.skill.xx-xx-xx-xx # - iot: # sql: "SELECT * FROM 'some_topic'" # - cloudwatchEvent: diff --git a/lib/plugins/create/templates/aws-groovy-gradle/serverless.yml b/lib/plugins/create/templates/aws-groovy-gradle/serverless.yml index 9ef210f6b..a1adaaf4d 100644 --- a/lib/plugins/create/templates/aws-groovy-gradle/serverless.yml +++ b/lib/plugins/create/templates/aws-groovy-gradle/serverless.yml @@ -65,6 +65,7 @@ functions: # - sns: greeter-topic # - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000 # - alexaSkill +# - alexaSmartHome: amzn1.ask.skill.xx-xx-xx-xx # - iot: # sql: "SELECT * FROM 'some_topic'" # - cloudwatchEvent: diff --git a/lib/plugins/create/templates/aws-java-gradle/serverless.yml b/lib/plugins/create/templates/aws-java-gradle/serverless.yml index d421ab1d5..e8811732e 100644 --- a/lib/plugins/create/templates/aws-java-gradle/serverless.yml +++ b/lib/plugins/create/templates/aws-java-gradle/serverless.yml @@ -65,6 +65,7 @@ functions: # - sns: greeter-topic # - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000 # - alexaSkill +# - alexaSmartHome: amzn1.ask.skill.xx-xx-xx-xx # - iot: # sql: "SELECT * FROM 'some_topic'" # - cloudwatchEvent: diff --git a/lib/plugins/create/templates/aws-java-maven/serverless.yml b/lib/plugins/create/templates/aws-java-maven/serverless.yml index d6fe45620..106bab8aa 100644 --- a/lib/plugins/create/templates/aws-java-maven/serverless.yml +++ b/lib/plugins/create/templates/aws-java-maven/serverless.yml @@ -65,6 +65,7 @@ functions: # - sns: greeter-topic # - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000 # - alexaSkill +# - alexaSmartHome: amzn1.ask.skill.xx-xx-xx-xx # - iot: # sql: "SELECT * FROM 'some_topic'" # - cloudwatchEvent: diff --git a/lib/plugins/create/templates/aws-kotlin-jvm-maven/serverless.yml b/lib/plugins/create/templates/aws-kotlin-jvm-maven/serverless.yml index 3659a390d..cb892140f 100644 --- a/lib/plugins/create/templates/aws-kotlin-jvm-maven/serverless.yml +++ b/lib/plugins/create/templates/aws-kotlin-jvm-maven/serverless.yml @@ -65,6 +65,7 @@ functions: # - sns: greeter-topic # - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000 # - alexaSkill +# - alexaSmartHome: amzn1.ask.skill.xx-xx-xx-xx # - iot: # sql: "SELECT * FROM 'some_topic'" # - cloudwatchEvent: diff --git a/lib/plugins/create/templates/aws-kotlin-nodejs-gradle/serverless.yml b/lib/plugins/create/templates/aws-kotlin-nodejs-gradle/serverless.yml index c590ec2e2..5fd69aab1 100644 --- a/lib/plugins/create/templates/aws-kotlin-nodejs-gradle/serverless.yml +++ b/lib/plugins/create/templates/aws-kotlin-nodejs-gradle/serverless.yml @@ -61,6 +61,7 @@ functions: # - sns: greeter-topic # - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000 # - alexaSkill +# - alexaSmartHome: amzn1.ask.skill.xx-xx-xx-xx # - iot: # sql: "SELECT * FROM 'some_topic'" # - cloudwatchEvent: @@ -91,4 +92,4 @@ functions: # Outputs: # NewOutput: # Description: "Description for the output" -# Value: "Some output value" \ No newline at end of file +# Value: "Some output value" diff --git a/lib/plugins/create/templates/aws-nodejs-ecma-script/package.json b/lib/plugins/create/templates/aws-nodejs-ecma-script/package.json index e6e8c12a2..bcfad7dd9 100644 --- a/lib/plugins/create/templates/aws-nodejs-ecma-script/package.json +++ b/lib/plugins/create/templates/aws-nodejs-ecma-script/package.json @@ -11,7 +11,7 @@ "babel-plugin-transform-runtime": "^6.23.0", "babel-polyfill": "^6.23.0", "babel-preset-env": "^1.6.0", - "serverless-webpack": "^2.2.0", + "serverless-webpack": "^3.1.1", "webpack": "^3.3.0" }, "author": "The serverless webpack authors (https://github.com/elastic-coders/serverless-webpack)", diff --git a/lib/plugins/create/templates/aws-nodejs-ecma-script/serverless.yml b/lib/plugins/create/templates/aws-nodejs-ecma-script/serverless.yml index 62e5090fb..1b53b5c30 100644 --- a/lib/plugins/create/templates/aws-nodejs-ecma-script/serverless.yml +++ b/lib/plugins/create/templates/aws-nodejs-ecma-script/serverless.yml @@ -10,14 +10,8 @@ provider: runtime: nodejs6.10 functions: - # Example without LAMBDA-PROXY integration - # Invoking locally: - # sls webpack invoke -f first first: handler: first.hello - # Example with LAMBDA-PROXY integration - # Invoking locally: - # sls webpack invoke -f second second: handler: second.hello events: diff --git a/lib/plugins/create/templates/aws-nodejs/serverless.yml b/lib/plugins/create/templates/aws-nodejs/serverless.yml index 059b386d4..1cce386a1 100644 --- a/lib/plugins/create/templates/aws-nodejs/serverless.yml +++ b/lib/plugins/create/templates/aws-nodejs/serverless.yml @@ -70,6 +70,7 @@ functions: # - sns: greeter-topic # - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000 # - alexaSkill +# - alexaSmartHome: amzn1.ask.skill.xx-xx-xx-xx # - iot: # sql: "SELECT * FROM 'some_topic'" # - cloudwatchEvent: diff --git a/lib/plugins/create/templates/aws-python/serverless.yml b/lib/plugins/create/templates/aws-python/serverless.yml index 110ae584f..f651608e0 100644 --- a/lib/plugins/create/templates/aws-python/serverless.yml +++ b/lib/plugins/create/templates/aws-python/serverless.yml @@ -70,6 +70,7 @@ functions: # - sns: greeter-topic # - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000 # - alexaSkill +# - alexaSmartHome: amzn1.ask.skill.xx-xx-xx-xx # - iot: # sql: "SELECT * FROM 'some_topic'" # - cloudwatchEvent: diff --git a/lib/plugins/create/templates/aws-python3/serverless.yml b/lib/plugins/create/templates/aws-python3/serverless.yml index 73cce39e6..1d69fcb9d 100644 --- a/lib/plugins/create/templates/aws-python3/serverless.yml +++ b/lib/plugins/create/templates/aws-python3/serverless.yml @@ -70,6 +70,7 @@ functions: # - sns: greeter-topic # - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000 # - alexaSkill +# - alexaSmartHome: amzn1.ask.skill.xx-xx-xx-xx # - iot: # sql: "SELECT * FROM 'some_topic'" # - cloudwatchEvent: diff --git a/lib/plugins/create/templates/aws-scala-sbt/serverless.yml b/lib/plugins/create/templates/aws-scala-sbt/serverless.yml index c7ce81f69..7eb89a39d 100644 --- a/lib/plugins/create/templates/aws-scala-sbt/serverless.yml +++ b/lib/plugins/create/templates/aws-scala-sbt/serverless.yml @@ -67,6 +67,7 @@ functions: # - sns: greeter-topic # - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000 # - alexaSkill +# - alexaSmartHome: amzn1.ask.skill.xx-xx-xx-xx # - iot: # sql: "SELECT * FROM 'some_topic'" # - cloudwatchEvent: diff --git a/lib/plugins/create/templates/spotinst-nodejs/package.json b/lib/plugins/create/templates/spotinst-nodejs/package.json index ccfcb802d..96824a70b 100644 --- a/lib/plugins/create/templates/spotinst-nodejs/package.json +++ b/lib/plugins/create/templates/spotinst-nodejs/package.json @@ -7,7 +7,7 @@ "serverless", "spotinst" ], - "dependencies": { + "devDependencies": { "serverless-spotinst-functions": "*" } } diff --git a/lib/plugins/create/templates/spotinst-nodejs/serverless.yml b/lib/plugins/create/templates/spotinst-nodejs/serverless.yml index 5b6231275..ac559c0d7 100644 --- a/lib/plugins/create/templates/spotinst-nodejs/serverless.yml +++ b/lib/plugins/create/templates/spotinst-nodejs/serverless.yml @@ -20,10 +20,14 @@ provider: functions: hello: - runtime: nodejs4.8 + runtime: nodejs8.3 handler: handler.main memory: 128 timeout: 30 + access: private +# cron: # Setup scheduled trigger with cron expression +# active: true +# value: '* * * * *' # extend the framework using plugins listed here: # https://github.com/serverless/plugins diff --git a/lib/plugins/create/templates/spotinst-python/package.json b/lib/plugins/create/templates/spotinst-python/package.json index a7aed6929..4a1f3a5f2 100644 --- a/lib/plugins/create/templates/spotinst-python/package.json +++ b/lib/plugins/create/templates/spotinst-python/package.json @@ -7,7 +7,7 @@ "serverless", "spotinst" ], - "dependencies": { + "devDependencies": { "serverless-spotinst-functions": "*" } } diff --git a/lib/plugins/create/templates/spotinst-python/serverless.yml b/lib/plugins/create/templates/spotinst-python/serverless.yml index 58cd69d69..93c56ea49 100644 --- a/lib/plugins/create/templates/spotinst-python/serverless.yml +++ b/lib/plugins/create/templates/spotinst-python/serverless.yml @@ -24,6 +24,10 @@ functions: handler: handler.main memory: 128 timeout: 30 + access: private +# cron: # Setup scheduled trigger with cron expression +# active: true +# value: '* * * * *' # extend the framework using plugins listed here: # https://github.com/serverless/plugins diff --git a/lib/plugins/create/templates/spotinst-ruby/gitignore b/lib/plugins/create/templates/spotinst-ruby/gitignore new file mode 100644 index 000000000..2b48c8bd5 --- /dev/null +++ b/lib/plugins/create/templates/spotinst-ruby/gitignore @@ -0,0 +1,6 @@ +# package directories +node_modules +jspm_packages + +# Serverless directories +.serverless \ No newline at end of file diff --git a/lib/plugins/create/templates/spotinst-ruby/handler.rb b/lib/plugins/create/templates/spotinst-ruby/handler.rb new file mode 100644 index 000000000..f1d2d9fd5 --- /dev/null +++ b/lib/plugins/create/templates/spotinst-ruby/handler.rb @@ -0,0 +1,13 @@ + +# Implement your function here. +# The function will get the request as parameter. +# The function should return an Hash + +def main(args) + queryparams = args["query"] + body = args["body"] + { + :statusCode => 200, + :body => '{"hello":"from Ruby2.4.1 function"}' + } +end \ No newline at end of file diff --git a/lib/plugins/create/templates/spotinst-ruby/package.json b/lib/plugins/create/templates/spotinst-ruby/package.json new file mode 100644 index 000000000..e1558bcd8 --- /dev/null +++ b/lib/plugins/create/templates/spotinst-ruby/package.json @@ -0,0 +1,13 @@ +{ + "name": "spotionst-ruby", + "version": "1.0.0", + "description": "Spotinst Functions Ruby sample for serverless framework service.", + "main": "handler.js", + "keywords": [ + "serverless", + "spotinst" + ], + "devDependencies": { + "serverless-spotinst-functions": "*" + } +} diff --git a/lib/plugins/create/templates/spotinst-ruby/serverless.yml b/lib/plugins/create/templates/spotinst-ruby/serverless.yml new file mode 100644 index 000000000..f46140a4f --- /dev/null +++ b/lib/plugins/create/templates/spotinst-ruby/serverless.yml @@ -0,0 +1,35 @@ +# 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: spotinst-ruby # NOTE: update this with your service name + +provider: + name: spotinst + spotinst: + #environment: # NOTE: Remember to add the environment ID + +functions: + hello: + runtime: ruby2.4.1 + handler: handler.main + memory: 128 + timeout: 30 + access: private +# cron: # Setup scheduled trigger with cron expression +# active: true +# value: '* * * * *' + +# extend the framework using plugins listed here: +# https://github.com/serverless/plugins +plugins: + - serverless-spotinst-functions diff --git a/lib/plugins/install/install.js b/lib/plugins/install/install.js index ff623c3fe..65641d7a8 100644 --- a/lib/plugins/install/install.js +++ b/lib/plugins/install/install.js @@ -1,12 +1,10 @@ 'use strict'; const BbPromise = require('bluebird'); -const path = require('path'); -const URL = require('url'); -const download = require('download'); -const fse = require('fs-extra'); -const os = require('os'); + const userStats = require('../../utils/userStats'); +const downloadTemplateFromRepo = require('../../utils/downloadTemplateFromRepo') + .downloadTemplateFromRepo; class Install { constructor(serverless, options) { @@ -37,130 +35,23 @@ class Install { 'install:install': () => BbPromise.bind(this) .then(this.install), }; - - this.renameService = (name, servicePath) => { - const serviceFile = path.join(servicePath, 'serverless.yml'); - const packageFile = path.join(servicePath, 'package.json'); - - if (!this.serverless.utils.fileExistsSync(serviceFile)) { - const errorMessage = [ - 'serverless.yml not found in', - ` ${servicePath}`, - ].join(''); - throw new this.serverless.classes.Error(errorMessage); - } - - const serverlessYml = - fse.readFileSync(serviceFile, 'utf-8') - .replace(/service\s*:.+/gi, (match) => { - const fractions = match.split('#'); - fractions[0] = `service: ${name}`; - return fractions.join(' #'); - }); - - fse.writeFileSync(serviceFile, serverlessYml); - - if (this.serverless.utils.fileExistsSync(packageFile)) { - const json = this.serverless.utils.readFileSync(packageFile); - this.serverless.utils.writeFile(packageFile, Object.assign(json, { name })); - } - }; } install() { - const url = URL.parse(this.options.url.replace(/\/$/, '')); + return downloadTemplateFromRepo(this.options.url, this.options.name) + .then(dirName => { + const message = [ + `Successfully installed "${dirName}" `, + `${this.options.name && this.options.name !== dirName ? `as "${dirName}"` : ''}`, + ].join(''); + userStats.track('service_installed', { + data: { // will be updated with core analtyics lib + url: this.options.url, + }, + }); - // check if url parameter is a valid url - if (!url.host) { - throw new this.serverless.classes.Error('The URL you passed is not a valid URL'); - } - - const parts = url.pathname.split('/'); - const parsedGitHubUrl = { - owner: parts[1], - repo: parts[2], - branch: parts[4] || 'master', - }; - - // validate if given url is a valid GitHub url - if (url.hostname !== 'github.com' || !parsedGitHubUrl.owner || !parsedGitHubUrl.repo) { - const errorMessage = [ - 'The URL must be a valid GitHub URL in the following format:', - ' https://github.com/serverless/serverless', - ].join(''); - throw new this.serverless.classes.Error(errorMessage); - } - - const downloadUrl = [ - 'https://github.com/', - parsedGitHubUrl.owner, - '/', - parsedGitHubUrl.repo, - '/archive/', - parsedGitHubUrl.branch, - '.zip', - ].join(''); - - const endIndex = parts.length - 1; - let dirName; - let serviceName; - let downloadServicePath; - - // check if it's a directory or the whole repository - if (parts.length > 4) { - serviceName = parts[endIndex]; - dirName = this.options.name || parts[endIndex]; - // download the repo into a temporary directory - downloadServicePath = path.join(os.tmpdir(), parsedGitHubUrl.repo); - } else { - serviceName = parsedGitHubUrl.repo; - dirName = this.options.name || parsedGitHubUrl.repo; - downloadServicePath = path.join(process.cwd(), dirName); - } - - const servicePath = path.join(process.cwd(), dirName); - const renamed = dirName !== (parts.length > 4 ? parts[endIndex] : parsedGitHubUrl.repo); - - if (this.serverless.utils.dirExistsSync(path.join(process.cwd(), dirName))) { - const errorMessage = `A folder named "${dirName}" already exists.`; - throw new this.serverless.classes.Error(errorMessage); - } - - this.serverless.cli.log(`Downloading and installing "${serviceName}"...`); - - const that = this; - - // download service - return download( - downloadUrl, - downloadServicePath, - { timeout: 30000, extract: true, strip: 1, mode: '755' } - ).then(() => { - // if it's a directory inside of git - if (parts.length > 4) { - let directory = downloadServicePath; - for (let i = 5; i <= endIndex; i++) { - directory = path.join(directory, parts[i]); - } - that.serverless.utils - .copyDirContentsSync(directory, servicePath); - fse.removeSync(downloadServicePath); - } - }).then(() => { - if (!renamed) return BbPromise.resolve(); - - 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, - }, + this.serverless.cli.log(message); }); - if (renamed) message = `${message} as "${dirName}"`; - - that.serverless.cli.log(message); - }); } } diff --git a/lib/plugins/install/install.test.js b/lib/plugins/install/install.test.js index 398dc3906..ed94b69a4 100644 --- a/lib/plugins/install/install.test.js +++ b/lib/plugins/install/install.test.js @@ -2,24 +2,18 @@ const expect = require('chai').expect; const Serverless = require('../../Serverless'); +const Install = require('./install.js'); const sinon = require('sinon'); -const BbPromise = require('bluebird'); const testUtils = require('../../../tests/utils'); const fse = require('fs-extra'); const path = require('path'); -const proxyquire = require('proxyquire'); - -const remove = BbPromise.promisify(fse.remove); describe('Install', () => { let install; let serverless; - let downloadStub; - let Install; let cwd; let servicePath; - let newServicePath; beforeEach(() => { const tmpDir = testUtils.getTmpDirPath(); @@ -29,13 +23,7 @@ describe('Install', () => { process.chdir(tmpDir); servicePath = tmpDir; - newServicePath = path.join(servicePath, 'new-service-name'); - downloadStub = sinon.stub().resolves(); - - Install = proxyquire('./install.js', { - download: downloadStub, - }); serverless = new Serverless(); install = new Install(serverless); serverless.init(); @@ -61,78 +49,6 @@ describe('Install', () => { install.install.restore(); }); }); - - it('should set new service in serverless.yml and name in package.json', () => { - const defaultServiceYml = - 'service: service-name\n\nprovider:\n name: aws\n'; - const newServiceYml = - 'service: new-service-name\n\nprovider:\n name: aws\n'; - - const defaultServiceName = 'service-name'; - const newServiceName = 'new-service-name'; - - const packageFile = path.join(servicePath, 'package.json'); - const serviceFile = path.join(servicePath, 'serverless.yml'); - - serverless.utils.writeFileSync(packageFile, { name: defaultServiceName }); - fse.writeFileSync(serviceFile, defaultServiceYml); - - install.renameService(newServiceName, servicePath); - const serviceYml = fse.readFileSync(serviceFile, 'utf-8'); - const packageJson = serverless.utils.readFileSync(packageFile); - expect(serviceYml).to.equal(newServiceYml); - expect(packageJson.name).to.equal(newServiceName); - }); - - it('should set new service in commented serverless.yml and name in package.json', () => { - const defaultServiceYml = - '# comment\nservice: service-name #comment\n\nprovider:\n name: aws\n# comment'; - const newServiceYml = - '# comment\nservice: new-service-name #comment\n\nprovider:\n name: aws\n# comment'; - - const defaultServiceName = 'service-name'; - const newServiceName = 'new-service-name'; - - const packageFile = path.join(servicePath, 'package.json'); - const serviceFile = path.join(servicePath, 'serverless.yml'); - - serverless.utils.writeFileSync(packageFile, { name: defaultServiceName }); - fse.writeFileSync(serviceFile, defaultServiceYml); - - install.renameService(newServiceName, servicePath); - const serviceYml = fse.readFileSync(serviceFile, 'utf-8'); - const packageJson = serverless.utils.readFileSync(packageFile); - expect(serviceYml).to.equal(newServiceYml); - expect(packageJson.name).to.equal(newServiceName); - }); - - it('should set new service in commented serverless.yml without existing package.json', () => { - const defaultServiceYml = - '# comment\nservice: service-name #comment\n\nprovider:\n name: aws\n# comment'; - const newServiceYml = - '# comment\nservice: new-service-name #comment\n\nprovider:\n name: aws\n# comment'; - - const serviceFile = path.join(servicePath, 'serverless.yml'); - - serverless.utils.writeFileDir(serviceFile); - fse.writeFileSync(serviceFile, defaultServiceYml); - - install.renameService('new-service-name', servicePath); - const serviceYml = fse.readFileSync(serviceFile, 'utf-8'); - expect(serviceYml).to.equal(newServiceYml); - }); - - it('should fail to set new service name in serverless.yml', () => { - const defaultServiceYml = - '# comment\nservice: service-name #comment\n\nprovider:\n name: aws\n# comment'; - - const serviceFile = path.join(servicePath, 'serverledss.yml'); - - serverless.utils.writeFileDir(serviceFile); - fse.writeFileSync(serviceFile, defaultServiceYml); - - expect(() => install.renameService('new-service-name', servicePath)).to.throw(Error); - }); }); describe('#install()', () => { @@ -156,89 +72,5 @@ describe('Install', () => { expect(() => install.install()).to.throw(Error); }); - - it('should download the service based on the GitHub URL', () => { - install.options = { url: 'https://github.com/johndoe/service-to-be-downloaded' }; - - return install.install().then(() => { - expect(downloadStub.calledOnce).to.equal(true); - expect(downloadStub.args[0][0]).to.equal(`${install.options.url}/archive/master.zip`); - }); - }); - - it('should download the service based on directories in the GitHub URL', () => { - install.options = { url: 'https://github.com/serverless/examples/tree/master/rest-api-with-dynamodb' }; - sinon.stub(serverless.utils, 'copyDirContentsSync').returns(true); - sinon.stub(fse, 'removeSync').returns(true); - - return install.install().then(() => { - expect(downloadStub.calledOnce).to.equal(true); - expect(downloadStub.args[0][0]).to.equal('https://github.com/serverless/examples/archive/master.zip'); - expect(serverless.utils.copyDirContentsSync.calledOnce).to.equal(true); - expect(fse.removeSync.calledOnce).to.equal(true); - - serverless.utils.copyDirContentsSync.restore(); - fse.removeSync.restore(); - }); - }); - - it('should throw an error if the same service name exists as directory in Github', () => { - install.options = { url: 'https://github.com/serverless/examples/tree/master/rest-api-with-dynamodb' }; - const serviceDirName = path.join(servicePath, 'rest-api-with-dynamodb'); - fse.mkdirsSync(serviceDirName); - - expect(() => install.install()).to.throw(Error); - }); - - it('should download and rename the service based on the GitHub URL', () => { - install.options = { - url: 'https://github.com/johndoe/service-to-be-downloaded', - name: 'new-service-name', - }; - - downloadStub.returns( - remove(newServicePath) - .then(() => { - const sp = downloadStub.args[0][1]; - const slsYml = path.join( - sp, - 'serverless.yml'); - serverless - .utils.writeFileSync(slsYml, 'service: service-name'); - })); - - return install.install().then(() => { - expect(downloadStub.calledOnce).to.equal(true); - expect(downloadStub.args[0][1]).to.contain(install.options.name); - expect(downloadStub.args[0][0]).to.equal(`${install.options.url}/archive/master.zip`); - const yml = serverless.utils.readFileSync(path.join(newServicePath, 'serverless.yml')); - expect(yml.service).to.equal(install.options.name); - }); - }); - - it('should download and rename the service based directories in the GitHub URL', () => { - install.options = { - url: 'https://github.com/serverless/examples/tree/master/rest-api-with-dynamodb', - name: 'new-service-name', - }; - - downloadStub.returns( - remove(newServicePath) - .then(() => { - const sp = downloadStub.args[0][1]; - const slsYml = path.join( - sp, - 'rest-api-with-dynamodb', - 'serverless.yml'); - serverless - .utils.writeFileSync(slsYml, 'service: service-name'); - })); - - return install.install().then(() => { - expect(downloadStub.calledOnce).to.equal(true); - const yml = serverless.utils.readFileSync(path.join(newServicePath, 'serverless.yml')); - expect(yml.service).to.equal(install.options.name); - }); - }); }); }); diff --git a/lib/plugins/package/lib/zipService.js b/lib/plugins/package/lib/zipService.js index fd0476fe0..1fe7ebe4f 100644 --- a/lib/plugins/package/lib/zipService.js +++ b/lib/plugins/package/lib/zipService.js @@ -80,26 +80,38 @@ module.exports = { output.on('open', () => { zip.pipe(output); - BbPromise.all(files.map((filePath) => { - const fullPath = path.resolve( - this.serverless.config.servicePath, - filePath - ); + BbPromise.all(files.map(this.getFileContentAndStat.bind(this))).then((contents) => { + _.forEach(_.sortBy(contents, ['filePath']), (file) => { + zip.append(file.data, { + name: file.filePath, + mode: file.stat.mode, + date: new Date(0), // necessary to get the same hash when zipping the same content + }); + }); - return fs.statAsync(fullPath).then(stats => - this.getFileContent(fullPath).then(fileContent => - zip.append(fileContent, { - name: filePath, - mode: stats.mode, - date: new Date(0), // necessary to get the same hash when zipping the same content - }) - ) - ); - })).then(() => zip.finalize()).catch(reject); + zip.finalize(); + }).catch(reject); }); }); }, + getFileContentAndStat(filePath) { + const fullPath = path.resolve( + this.serverless.config.servicePath, + filePath + ); + + return BbPromise.all([ // Get file contents and stat in parallel + this.getFileContent(fullPath), + fs.statAsync(fullPath), + ]).then((result) => ({ + data: result[0], + stat: result[1], + filePath, + })); + }, + + // Useful point of entry for e.g. transpilation plugins getFileContent(fullPath) { return fs.readFileAsync(fullPath); }, @@ -174,11 +186,38 @@ function excludeNodeDevDependencies(servicePath) { const nodeModulesRegex = new RegExp(`${path.join('node_modules', path.sep)}.*`, 'g'); if (!_.isEmpty(dependencies)) { - const globs = dependencies - .map((item) => item.replace(path.join(servicePath, path.sep), '')) + return BbPromise + .map(dependencies, (item) => item.replace(path.join(servicePath, path.sep), '')) .filter((item) => item.length > 0 && item.match(nodeModulesRegex)) - .map((item) => `${item}/**`); - exAndIn.exclude = globs; + .reduce((globs, item) => { + const packagePath = path.join(servicePath, item, 'package.json'); + return fs.readFileAsync(packagePath, 'utf-8').then((packageJsonFile) => { + const lastIndex = item.lastIndexOf(path.sep) + 1; + const moduleName = item.substr(lastIndex); + const modulePath = item.substr(0, lastIndex); + + const packageJson = JSON.parse(packageJsonFile); + const bin = packageJson.bin; + + const baseGlobs = [path.join(item, '**')]; + + // NOTE: pkg.bin can be object, string, or undefined + if (typeof bin === 'object') { + _.each(_.keys(bin), (executable) => { + baseGlobs.push(path.join(modulePath, '.bin', executable)); + }); + // only 1 executable with same name as lib + } else if (typeof bin === 'string') { + baseGlobs.push(path.join(modulePath, '.bin', moduleName)); + } + + return globs.concat(baseGlobs); + }); + }, []) + .then((globs) => { + exAndIn.exclude = exAndIn.exclude.concat(globs); + return exAndIn; + }); } return exAndIn; diff --git a/lib/plugins/package/lib/zipService.test.js b/lib/plugins/package/lib/zipService.test.js index 97c53c5ba..39cedf31f 100644 --- a/lib/plugins/package/lib/zipService.test.js +++ b/lib/plugins/package/lib/zipService.test.js @@ -68,6 +68,27 @@ describe('zipService', () => { }); }); + describe('#getFileContentAndStat()', () => { + let servicePath; + + beforeEach(() => { + servicePath = serverless.config.servicePath; + fs.mkdirSync(servicePath); + }); + + it('should keep the file content as is', () => { + const buf = new Buffer([10, 20, 30, 40, 50]); + const filePath = path.join(servicePath, 'bin-file'); + + fs.writeFileSync(filePath, buf); + + return expect(packagePlugin.getFileContentAndStat(filePath)).to.be.fulfilled + .then((result) => { + expect(result.data).to.deep.equal(buf); + }); + }); + }); + describe('#getFileContent()', () => { let servicePath; @@ -231,6 +252,7 @@ describe('zipService', () => { globbySyncStub.returns(filePaths); execAsyncStub.resolves(); + readFileAsyncStub.onCall(0).resolves(); readFileAsyncStub.onCall(1).rejects(); @@ -270,19 +292,45 @@ describe('zipService', () => { execAsyncStub.onCall(4).resolves(); execAsyncStub.onCall(5).resolves(); const depPaths = [ - path.join(`${servicePath}`, 'node_modules/module-1'), - path.join(`${servicePath}`, 'node_modules/module-2'), - path.join(`${servicePath}`, '1st/2nd/node_modules/module-1'), - path.join(`${servicePath}`, '1st/2nd/node_modules/module-2'), + path.join(servicePath, 'node_modules', 'module-1'), + path.join(servicePath, 'node_modules', 'module-2'), + path.join(servicePath, '1st', '2nd', 'node_modules', 'module-1'), + path.join(servicePath, '1st', '2nd', 'node_modules', 'module-2'), ].join('\n'); readFileAsyncStub.withArgs(sinon.match(/dev$/)).resolves(depPaths); readFileAsyncStub.withArgs(sinon.match(/prod$/)).resolves([]); + readFileAsyncStub.onCall(2).resolves('{}'); + readFileAsyncStub.onCall(3).resolves('{}'); + readFileAsyncStub.onCall(4).resolves('{}'); + readFileAsyncStub.onCall(5).resolves('{}'); return expect(packagePlugin.excludeDevDependencies(params)).to.be .fulfilled.then((updatedParams) => { expect(globbySyncStub).to.have.been.calledOnce; expect(execAsyncStub.callCount).to.equal(6); - expect(readFileAsyncStub).to.have.been.calledTwice; + expect(readFileAsyncStub).to.have.callCount(6); + expect(readFileAsyncStub).to.have.been + .calledWith(path.join(servicePath, 'node_modules', 'module-1', 'package.json')); + expect(readFileAsyncStub).to.have.been + .calledWith(path.join(servicePath, 'node_modules', 'module-2', 'package.json')); + expect(readFileAsyncStub).to.have.been + .calledWith(path.join( + servicePath, + '1st', + '2nd', + 'node_modules', + 'module-1', + 'package.json' + )); + expect(readFileAsyncStub).to.have.been + .calledWith(path.join( + servicePath, + '1st', + '2nd', + 'node_modules', + 'module-1', + 'package.json' + )); expect(globbySyncStub).to.have.been .calledWithExactly(['**/package.json'], { cwd: packagePlugin.serverless.config.servicePath, @@ -317,10 +365,10 @@ describe('zipService', () => { .match(/.+/); expect(updatedParams.exclude).to.deep.equal([ 'user-defined-exclude-me', - `${path.join('node_modules/module-1')}/**`, - `${path.join('node_modules/module-2')}/**`, - `${path.join('1st/2nd/node_modules/module-1')}/**`, - `${path.join('1st/2nd/node_modules/module-2')}/**`, + path.join('node_modules', 'module-1', '**'), + path.join('node_modules', 'module-2', '**'), + path.join('1st', '2nd', 'node_modules', 'module-1', '**'), + path.join('1st', '2nd', 'node_modules', 'module-2', '**'), ]); expect(updatedParams.include).to .deep.equal([ @@ -336,17 +384,23 @@ describe('zipService', () => { globbySyncStub.returns(filePaths); execAsyncStub.resolves(); const depPaths = [ - path.join(`${servicePath}`, 'node_modules/module-1'), - path.join(`${servicePath}`, 'node_modules/module-2'), + path.join(servicePath, 'node_modules', 'module-1'), + path.join(servicePath, 'node_modules', 'module-2'), ].join('\n'); readFileAsyncStub.withArgs(sinon.match(/dev$/)).resolves(depPaths); readFileAsyncStub.withArgs(sinon.match(/prod$/)).resolves([]); + readFileAsyncStub.onCall(2).resolves('{}'); + readFileAsyncStub.onCall(3).resolves('{}'); return expect(packagePlugin.excludeDevDependencies(params)).to.be .fulfilled.then((updatedParams) => { expect(globbySyncStub).to.have.been.calledOnce; expect(execAsyncStub).to.have.been.calledTwice; - expect(readFileAsyncStub).to.have.been.calledTwice; + expect(readFileAsyncStub).to.have.callCount(4); + expect(readFileAsyncStub).to.have.been + .calledWith(path.join(servicePath, 'node_modules', 'module-1', 'package.json')); + expect(readFileAsyncStub).to.have.been + .calledWith(path.join(servicePath, 'node_modules', 'module-2', 'package.json')); expect(globbySyncStub).to.have.been .calledWithExactly(['**/package.json'], { cwd: packagePlugin.serverless.config.servicePath, @@ -365,8 +419,8 @@ describe('zipService', () => { .match(/.+/); expect(updatedParams.exclude).to.deep.equal([ 'user-defined-exclude-me', - `${path.join('node_modules/module-1')}/**`, - `${path.join('node_modules/module-2')}/**`, + path.join('node_modules', 'module-1', '**'), + path.join('node_modules', 'module-2', '**'), ]); expect(updatedParams.include).to.deep.equal([ 'user-defined-include-me', @@ -390,22 +444,27 @@ describe('zipService', () => { globbySyncStub.returns(filePaths); execAsyncStub.resolves(); const depPaths = [ - path.join(`${servicePath}`, 'node_modules/module-1'), - path.join(`${servicePath}`, 'node_modules/module-2'), - path.join(`${servicePath}`, '1st/node_modules/module-1'), - path.join(`${servicePath}`, '1st/node_modules/module-2'), - path.join(`${servicePath}`, '1st/2nd/node_modules/module-1'), - path.join(`${servicePath}`, '1st/2nd/node_modules/module-2'), + path.join(servicePath, 'node_modules', 'module-1'), + path.join(servicePath, 'node_modules', 'module-2'), + path.join(servicePath, '1st', 'node_modules', 'module-1'), + path.join(servicePath, '1st', 'node_modules', 'module-2'), + path.join(servicePath, '1st', '2nd', 'node_modules', 'module-1'), + path.join(servicePath, '1st', '2nd', 'node_modules', 'module-2'), ].join('\n'); - readFileAsyncStub.resolves(depPaths); readFileAsyncStub.withArgs(sinon.match(/dev$/)).resolves(depPaths); readFileAsyncStub.withArgs(sinon.match(/prod$/)).resolves([]); + readFileAsyncStub.onCall(2).resolves('{}'); + readFileAsyncStub.onCall(3).resolves('{}'); + readFileAsyncStub.onCall(4).resolves('{}'); + readFileAsyncStub.onCall(5).resolves('{}'); + readFileAsyncStub.onCall(6).resolves('{}'); + readFileAsyncStub.onCall(7).resolves('{}'); return expect(packagePlugin.excludeDevDependencies(params)).to.be .fulfilled.then((updatedParams) => { expect(globbySyncStub).to.have.been.calledOnce; expect(execAsyncStub.callCount).to.equal(6); - expect(readFileAsyncStub).to.have.been.calledTwice; + expect(readFileAsyncStub).to.have.callCount(8); expect(globbySyncStub).to.have.been .calledWithExactly(['**/package.json'], { cwd: packagePlugin.serverless.config.servicePath, @@ -440,12 +499,12 @@ describe('zipService', () => { .match(/.+/); expect(updatedParams.exclude).to.deep.equal([ 'user-defined-exclude-me', - `${path.join('node_modules/module-1')}/**`, - `${path.join('node_modules/module-2')}/**`, - `${path.join('1st/node_modules/module-1')}/**`, - `${path.join('1st/node_modules/module-2')}/**`, - `${path.join('1st/2nd/node_modules/module-1')}/**`, - `${path.join('1st/2nd/node_modules/module-2')}/**`, + path.join('node_modules', 'module-1', '**'), + path.join('node_modules', 'module-2', '**'), + path.join('1st', 'node_modules', 'module-1', '**'), + path.join('1st', 'node_modules', 'module-2', '**'), + path.join('1st', '2nd', 'node_modules', 'module-1', '**'), + path.join('1st', '2nd', 'node_modules', 'module-2', '**'), ]); expect(updatedParams.include).to.deep.equal([ 'user-defined-include-me', @@ -461,21 +520,24 @@ describe('zipService', () => { execAsyncStub.resolves(); const devDepPaths = [ - path.join(`${servicePath}`, 'node_modules/module-1'), - path.join(`${servicePath}`, 'node_modules/module-2'), + path.join(servicePath, 'node_modules', 'module-1'), + path.join(servicePath, 'node_modules', 'module-2'), ].join('\n'); readFileAsyncStub.withArgs(sinon.match(/dev$/)).resolves(devDepPaths); const prodDepPaths = [ - path.join(`${servicePath}`, 'node_modules/module-2'), + path.join(servicePath, 'node_modules', 'module-2'), ]; readFileAsyncStub.withArgs(sinon.match(/prod$/)).resolves(prodDepPaths); + readFileAsyncStub.onCall(2).resolves('{}'); return expect(packagePlugin.excludeDevDependencies(params)).to.be .fulfilled.then((updatedParams) => { expect(globbySyncStub).to.have.been.calledOnce; expect(execAsyncStub).to.have.been.calledTwice; - expect(readFileAsyncStub).to.have.been.calledTwice; + expect(readFileAsyncStub).to.have.been.calledThrice; + expect(readFileAsyncStub).to.have.been + .calledWith(path.join(servicePath, 'node_modules', 'module-1', 'package.json')); expect(globbySyncStub).to.have.been .calledWithExactly(['**/package.json'], { cwd: packagePlugin.serverless.config.servicePath, @@ -494,7 +556,157 @@ describe('zipService', () => { .match(/.+/); expect(updatedParams.exclude).to.deep.equal([ 'user-defined-exclude-me', - `${path.join('node_modules/module-1')}/**`, + path.join('node_modules', 'module-1', '**'), + ]); + expect(updatedParams.include).to.deep.equal([ + 'user-defined-include-me', + ]); + expect(updatedParams.zipFileName).to.equal(params.zipFileName); + }); + }); + + it('should exclude dev dependency executables in node_modules/.bin', () => { + const devPaths = [ + 'node_modules/bro-module', + 'node_modules/node-dude', + 'node_modules/lumo-clj', + 'node_modules/meowmix', + ]; + + const prodPaths = [ + 'node_modules/node-dude', + ]; + + const filePaths = [ + 'node_modules/', + 'package.json', + ].concat(devPaths).concat(prodPaths); + + globbySyncStub.returns(filePaths); + execAsyncStub.resolves(); + + const mapper = (depPath) => path.join(`${servicePath}`, depPath); + + const devDepPaths = devPaths.map(mapper).join('\n'); + readFileAsyncStub.withArgs(sinon.match(/dev$/)).resolves(devDepPaths); + + const prodDepPaths = prodPaths.map(mapper).join('\n'); + readFileAsyncStub.withArgs(sinon.match(/prod$/)).resolves(prodDepPaths); + + readFileAsyncStub + .onCall(2) + .resolves('{"name": "bro-module", "bin": "main.js"}'); + readFileAsyncStub + .onCall(3) + .resolves('{"name": "lumo-clj", "bin": {"lumo": "./bin/lumo.js"}}'); + readFileAsyncStub + .onCall(4) + // need to handle possibility of multiple executables provided by the lib + .resolves('{"name": "meowmix", "bin": {"meow": "./bin/meow.js", "mix": "./bin/mix.js"}}'); + + return expect(packagePlugin.excludeDevDependencies(params)).to.be + .fulfilled.then((updatedParams) => { + expect(globbySyncStub).to.been.calledOnce; + expect(execAsyncStub).to.have.been.calledTwice; + + expect(readFileAsyncStub).to.have.callCount(5); + expect(readFileAsyncStub).to.have.been + .calledWith(path.join(servicePath, 'node_modules', 'bro-module', 'package.json')); + expect(readFileAsyncStub).to.have.been + .calledWith(path.join(servicePath, 'node_modules', 'lumo-clj', 'package.json')); + expect(readFileAsyncStub).to.have.been + .calledWith(path.join(servicePath, 'node_modules', 'meowmix', 'package.json')); + + expect(updatedParams.exclude).to.deep.equal([ + 'user-defined-exclude-me', + path.join('node_modules', 'bro-module', '**'), + path.join('node_modules', '.bin', 'bro-module'), + path.join('node_modules', 'lumo-clj', '**'), + path.join('node_modules', '.bin', 'lumo'), + path.join('node_modules', 'meowmix', '**'), + path.join('node_modules', '.bin', 'meow'), + path.join('node_modules', '.bin', 'mix'), + ]); + expect(updatedParams.include).to.deep.equal([ + 'user-defined-include-me', + ]); + expect(updatedParams.zipFileName).to.equal(params.zipFileName); + }); + }); + + it('should exclude .bin executables in deeply nested folders', () => { + const filePaths = [ + 'package.json', 'node_modules', + path.join('1st', 'package.json'), + path.join('1st', 'node_modules'), + path.join('1st', '2nd', 'package.json'), + path.join('1st', '2nd', 'node_modules'), + ]; + + globbySyncStub.returns(filePaths); + execAsyncStub.resolves(); + const deps = [ + 'node_modules/module-1', + 'node_modules/module-2', + '1st/node_modules/module-1', + '1st/node_modules/module-2', + '1st/2nd/node_modules/module-1', + '1st/2nd/node_modules/module-2', + ]; + const depPaths = deps.map((depPath) => path.join(`${servicePath}`, depPath)); + readFileAsyncStub.withArgs(sinon.match(/dev$/)).resolves(depPaths.join('\n')); + readFileAsyncStub.withArgs(sinon.match(/prod$/)).resolves([]); + + const module1PackageJson = JSON.stringify({ + name: 'module-1', + bin: { + 'cool-module': './index.js', + }, + }); + const module2PackageJson = JSON.stringify({ + name: 'module-2', + bin: './main.js', + }); + + readFileAsyncStub.onCall(2).resolves(module1PackageJson); + readFileAsyncStub.onCall(3).resolves(module2PackageJson); + readFileAsyncStub.onCall(4).resolves(module1PackageJson); + readFileAsyncStub.onCall(5).resolves(module2PackageJson); + readFileAsyncStub.onCall(6).resolves(module1PackageJson); + readFileAsyncStub.onCall(7).resolves(module2PackageJson); + + return expect(packagePlugin.excludeDevDependencies(params)).to.be + .fulfilled.then((updatedParams) => { + expect(globbySyncStub).to.have.been.calledOnce; + expect(execAsyncStub.callCount).to.equal(6); + expect(readFileAsyncStub).to.have.callCount(8); + for (const depPath of deps) { + expect(readFileAsyncStub).to.have.been + .calledWith(path.join(servicePath, depPath, 'package.json')); + } + expect(globbySyncStub).to.have.been + .calledWithExactly(['**/package.json'], { + cwd: packagePlugin.serverless.config.servicePath, + dot: true, + silent: true, + follow: true, + nosort: true, + }); + + expect(updatedParams.exclude).to.deep.equal([ + 'user-defined-exclude-me', + path.join('node_modules', 'module-1', '**'), + path.join('node_modules', '.bin', 'cool-module'), + path.join('node_modules', 'module-2', '**'), + path.join('node_modules', '.bin/module-2'), + path.join('1st', 'node_modules', 'module-1', '**'), + path.join('1st', 'node_modules', '.bin', 'cool-module'), + path.join('1st', 'node_modules', 'module-2', '**'), + path.join('1st', 'node_modules', '.bin', 'module-2'), + path.join('1st', '2nd', 'node_modules', 'module-1', '**'), + path.join('1st', '2nd', 'node_modules', '.bin', 'cool-module'), + path.join('1st', '2nd', 'node_modules', 'module-2', '**'), + path.join('1st', '2nd', 'node_modules', '.bin', 'module-2'), ]); expect(updatedParams.include).to.deep.equal([ 'user-defined-include-me', diff --git a/lib/plugins/print/print.js b/lib/plugins/print/print.js new file mode 100644 index 000000000..e4263aa8b --- /dev/null +++ b/lib/plugins/print/print.js @@ -0,0 +1,51 @@ +'use strict'; + +const BbPromise = require('bluebird'); +const getServerlessConfigFile = require('../../utils/getServerlessConfigFile'); +const YAML = require('js-yaml'); + +class Print { + constructor(serverless) { + this.serverless = serverless; + + this.commands = { + print: { + usage: 'Print your compiled and resolved config file', + lifecycleEvents: [ + 'print', + ], + }, + }; + this.hooks = { + 'print:print': () => BbPromise.bind(this) + .then(this.print), + }; + } + + print() { + let variableSyntax; + this.serverless.variables.options = this.serverless.processedInput.options; + this.serverless.variables.loadVariableSyntax(); + return getServerlessConfigFile(process.cwd()) + .then((data) => { + const conf = data; + // Need to delete variableSyntax to avoid potential matching errors + if (conf.provider.variableSyntax) { + variableSyntax = conf.provider.variableSyntax; + delete conf.provider.variableSyntax; + } + return conf; + }) + .then((data) => this.serverless.variables.populateObject(data)) + .then((data) => { + const conf = data; + if (variableSyntax !== undefined) { + conf.provider.variableSyntax = variableSyntax; + } + this.serverless.cli.consoleLog(YAML.dump(conf, { noRefs: true })); + }); + } + +} + +module.exports = Print; diff --git a/lib/plugins/print/print.test.js b/lib/plugins/print/print.test.js new file mode 100644 index 000000000..95fc05935 --- /dev/null +++ b/lib/plugins/print/print.test.js @@ -0,0 +1,160 @@ +'use strict'; + +const expect = require('chai').expect; +const sinon = require('sinon'); +const proxyquire = require('proxyquire'); +const Serverless = require('../../Serverless'); +const CLI = require('../../classes/CLI'); +const YAML = require('js-yaml'); + + +describe('Print', () => { + let print; + let serverless; + let getServerlessConfigFileStub; + + beforeEach(() => { + getServerlessConfigFileStub = sinon.stub(); + const PrintPlugin = proxyquire('./print.js', { + '../../utils/getServerlessConfigFile': getServerlessConfigFileStub, + }); + serverless = new Serverless(); + serverless.processedInput = { + commands: ['print'], + options: { stage: undefined, region: undefined }, + }; + serverless.cli = new CLI(serverless); + print = new PrintPlugin(serverless); + print.serverless.cli = { + consoleLog: sinon.spy(), + }; + }); + + afterEach(() => { + serverless.service.provider.variableSyntax = '\\${([ ~:a-zA-Z0-9._\'",\\-\\/\\(\\)]+?)}'; + }); + + it('should print standard config', () => { + const conf = { + service: 'my-service', + provider: { + name: 'aws', + }, + }; + getServerlessConfigFileStub.resolves(conf); + + return print.print().then(() => { + const message = print.serverless.cli.consoleLog.args.join(); + + expect(getServerlessConfigFileStub.calledOnce).to.equal(true); + expect(print.serverless.cli.consoleLog.called).to.be.equal(true); + expect(message).to.have.string(YAML.dump(conf)); + }); + }); + + it('should resolve command line variables', () => { + const conf = { + service: 'my-service', + provider: { + name: 'aws', + stage: '${opt:stage}', + }, + }; + getServerlessConfigFileStub.resolves(conf); + + serverless.processedInput = { + commands: ['print'], + options: { stage: 'dev', region: undefined }, + }; + + const expected = { + service: 'my-service', + provider: { + name: 'aws', + stage: 'dev', + }, + }; + + return print.print().then(() => { + const message = print.serverless.cli.consoleLog.args.join(); + + expect(getServerlessConfigFileStub.calledOnce).to.equal(true); + expect(print.serverless.cli.consoleLog.called).to.be.equal(true); + expect(message).to.equal(YAML.dump(expected)); + }); + }); + + it('should resolve using custom variable syntax', () => { + const conf = { + service: 'my-service', + provider: { + name: 'aws', + stage: '${{opt:stage}}', + variableSyntax: "\\${{([ ~:a-zA-Z0-9._\\'\",\\-\\/\\(\\)]+?)}}", + }, + }; + serverless.service.provider.variableSyntax = "\\${{([ ~:a-zA-Z0-9._\\'\",\\-\\/\\(\\)]+?)}}"; + getServerlessConfigFileStub.resolves(conf); + + serverless.processedInput = { + commands: ['print'], + options: { stage: 'dev', region: undefined }, + }; + + const expected = { + service: 'my-service', + provider: { + name: 'aws', + stage: 'dev', + variableSyntax: "\\${{([ ~:a-zA-Z0-9._\\'\",\\-\\/\\(\\)]+?)}}", + }, + }; + + return print.print().then(() => { + const message = print.serverless.cli.consoleLog.args.join(); + + expect(getServerlessConfigFileStub.calledOnce).to.equal(true); + expect(print.serverless.cli.consoleLog.called).to.be.equal(true); + expect(message).to.equal(YAML.dump(expected)); + }); + }); + + it('should resolve custom variables', () => { + const conf = { + service: 'my-service', + custom: { region: 'us-east-1' }, + provider: { + name: 'aws', + stage: '${opt:stage}', + region: '${self:custom.region}', + }, + }; + getServerlessConfigFileStub.resolves(conf); + + serverless.processedInput = { + commands: ['print'], + options: { stage: 'dev', region: undefined }, + }; + serverless.service.custom = { region: 'us-east-1' }; + + const expected = { + service: 'my-service', + custom: { + region: 'us-east-1', + }, + provider: { + name: 'aws', + stage: 'dev', + region: 'us-east-1', + }, + }; + + return print.print().then(() => { + const message = print.serverless.cli.consoleLog.args.join(); + + expect(getServerlessConfigFileStub.calledOnce).to.equal(true); + expect(print.serverless.cli.consoleLog.called).to.be.equal(true); + expect(message).to.equal(YAML.dump(expected)); + }); + }); +}); diff --git a/lib/plugins/rollback/index.js b/lib/plugins/rollback/index.js index d7e4a52ad..68713727a 100644 --- a/lib/plugins/rollback/index.js +++ b/lib/plugins/rollback/index.js @@ -18,7 +18,7 @@ class Rollback { timestamp: { usage: 'Timestamp of the deployment (list deployments with `serverless deploy list`)', shortcut: 't', - required: true, + required: false, }, verbose: { usage: 'Show all stack events during deployment', diff --git a/lib/plugins/rollback/index.test.js b/lib/plugins/rollback/index.test.js index 1ff84bb47..d5653a846 100644 --- a/lib/plugins/rollback/index.test.js +++ b/lib/plugins/rollback/index.test.js @@ -27,9 +27,9 @@ describe('Rollback', () => { ]); }); - it('should have a required option timestamp', () => { + it('should not have a required option timestamp', () => { // eslint-disable-next-line no-unused-expressions - expect(rollback.commands.rollback.options.timestamp.required).to.be.true; + expect(rollback.commands.rollback.options.timestamp.required).to.be.false; }); }); diff --git a/lib/utils/downloadTemplateFromRepo.js b/lib/utils/downloadTemplateFromRepo.js new file mode 100644 index 000000000..1ae503ec9 --- /dev/null +++ b/lib/utils/downloadTemplateFromRepo.js @@ -0,0 +1,195 @@ +'use strict'; + +const path = require('path'); +const os = require('os'); +const URL = require('url'); +const download = require('download'); +const BbPromise = require('bluebird'); +const fse = require('fs-extra'); +const qs = require('querystring'); +const renameService = require('./renameService').renameService; +const ServerlessError = require('../classes/Error').ServerlessError; +const copyDirContentsSync = require('./fs/copyDirContentsSync'); +const dirExistsSync = require('./fs/dirExistsSync'); +const log = require('./log/serverlessLog'); + +/** + * @param {Object} url + * @returns {Object} + */ +function parseGitHubURL(url) { + const parts = url.pathname.split('/'); + const isSubdirectory = parts.length > 4; + const owner = parts[1]; + const repo = parts[2]; + const branch = isSubdirectory ? parts[4] : 'master'; + + // validate if given url is a valid GitHub url + if (url.hostname !== 'github.com' || !owner || !repo) { + const errorMessage = [ + 'The URL must be a valid GitHub URL in the following format:', + ' https://github.com/serverless/serverless', + ].join(''); + throw new ServerlessError(errorMessage); + } + + let pathToDirectory = ''; + for (let i = 5; i <= (parts.length - 1); i++) { + pathToDirectory = path.join(pathToDirectory, parts[i]); + } + + const downloadUrl = [ + 'https://github.com/', + owner, + '/', + repo, + '/archive/', + branch, + '.zip', + ].join(''); + + return { + owner, + repo, + branch, + downloadUrl, + isSubdirectory, + pathToDirectory, + }; +} + +/** + * @param {Object} url + * @returns {Object} + */ +function parseBitBucketURL(url) { + const parts = url.pathname.split('/'); + const isSubdirectory = parts.length > 4; + const owner = parts[1]; + const repo = parts[2]; + + const query = qs.parse(url.query); + const branch = 'at' in query ? query.at : 'master'; + + // validate if given url is a valid GitHub url + if (url.hostname !== 'bitbucket.org' || !owner || !repo) { + const errorMessage = [ + 'The URL must be a valid GitHub URL in the following format:', + ' https://github.com/serverless/serverless', + ].join(''); + throw new ServerlessError(errorMessage); + } + + let pathToDirectory = ''; + for (let i = 5; i <= (parts.length - 1); i++) { + pathToDirectory = path.join(pathToDirectory, parts[i]); + } + + const downloadUrl = [ + 'https://bitbucket.org/', + owner, + '/', + repo, + '/get/', + branch, + '.zip', + ].join(''); + + return { + owner, + repo, + branch, + downloadUrl, + isSubdirectory, + pathToDirectory, + }; +} + +/** + * Parse URL and call the appropriate adaptor + * + * @param {string} inputUrl + * @throws {ServerlessError} + * @returns {Object} + */ +function parseRepoURL(inputUrl) { + if (!inputUrl) { + throw new ServerlessError('URL is required'); + } + + const url = URL.parse(inputUrl.replace(/\/$/, '')); + + // check if url parameter is a valid url + if (!url.host) { + throw new ServerlessError('The URL you passed is not a valid URL'); + } + + switch (url.hostname) { + case 'github.com': { + return parseGitHubURL(url); + } + case 'bitbucket.org': { + return parseBitBucketURL(url); + } + default: { + const msg = 'The URL you passed is not one of the valid providers: "GitHub", "BitBucket".'; + throw new ServerlessError(msg); + } + } +} + +/** + * @param {string} inputUrl + * @param {string} [templateName] + * @param {string} [path] + * @returns {Promise} + */ +function downloadTemplateFromRepo(inputUrl, templateName, downloadPath) { + const repoInformation = parseRepoURL(inputUrl); + + let serviceName; + let dirName; + let downloadServicePath; + + if (repoInformation.isSubdirectory) { + const folderName = repoInformation.pathToDirectory.split('/').splice(-1)[0]; + serviceName = folderName; + dirName = downloadPath || templateName || folderName; + downloadServicePath = path.join(os.tmpdir(), repoInformation.repo); + } else { + serviceName = repoInformation.repo; + dirName = downloadPath || templateName || repoInformation.repo; + downloadServicePath = path.join(process.cwd(), dirName); + } + + const servicePath = path.join(process.cwd(), dirName); + const renamed = dirName !== repoInformation.repo; + + if (dirExistsSync(path.join(process.cwd(), dirName))) { + const errorMessage = `A folder named "${dirName}" already exists.`; + throw new ServerlessError(errorMessage); + } + + log(`Downloading and installing "${serviceName}"...`); + + // download service + return download( + repoInformation.downloadUrl, + downloadServicePath, + { timeout: 30000, extract: true, strip: 1, mode: '755' } + ).then(() => { + // if it's a directory inside of git + if (repoInformation.isSubdirectory) { + const directory = path.join(downloadServicePath, repoInformation.pathToDirectory); + copyDirContentsSync(directory, servicePath); + fse.removeSync(downloadServicePath); + } + }).then(() => { + if (!renamed) return BbPromise.resolve(); + + return renameService(dirName, servicePath); + }); +} + +module.exports.downloadTemplateFromRepo = downloadTemplateFromRepo; +module.exports.parseRepoURL = parseRepoURL; diff --git a/lib/utils/downloadTemplateFromRepo.test.js b/lib/utils/downloadTemplateFromRepo.test.js new file mode 100644 index 000000000..d1cdf290f --- /dev/null +++ b/lib/utils/downloadTemplateFromRepo.test.js @@ -0,0 +1,200 @@ +'use strict'; + +const expect = require('chai').expect; +const sinon = require('sinon'); +const BbPromise = require('bluebird'); +const testUtils = require('../../tests/utils'); +const fse = require('fs-extra'); +const path = require('path'); +const proxyquire = require('proxyquire'); + +const writeFileSync = require('./fs/writeFileSync'); +const readFileSync = require('./fs/readFileSync'); + +const remove = BbPromise.promisify(fse.remove); + +const parseRepoURL = require('./downloadTemplateFromRepo').parseRepoURL; + +describe('downloadTemplateFromRepo', () => { + let downloadTemplateFromRepo; + let downloadStub; + let cwd; + + let servicePath; + let newServicePath; + + beforeEach(() => { + const tmpDir = testUtils.getTmpDirPath(); + cwd = process.cwd(); + + fse.mkdirsSync(tmpDir); + process.chdir(tmpDir); + + servicePath = tmpDir; + newServicePath = path.join(servicePath, 'new-service-name'); + + downloadStub = sinon.stub().resolves(); + + downloadTemplateFromRepo = proxyquire('./downloadTemplateFromRepo', { + download: downloadStub, + }).downloadTemplateFromRepo; + }); + + afterEach(() => { + // change back to the old cwd + process.chdir(cwd); + }); + + describe('downloadTemplateFromRepo', () => { + it('should throw an error if the passed URL option is not a valid URL', () => { + expect(() => downloadTemplateFromRepo('invalidUrl')).to.throw(Error); + }); + + it('should throw an error if the passed URL is not a valid GitHub URL', () => { + expect(() => downloadTemplateFromRepo('http://no-github-url.com/foo/bar')).to.throw(Error); + }); + + it('should throw an error if a directory with the same service name is already present', () => { + const serviceDirName = path.join(servicePath, 'existing-service'); + fse.mkdirsSync(serviceDirName); + + expect(() => downloadTemplateFromRepo('https://github.com/johndoe/existing-service')).to.throw(Error); + }); + + it('should download the service based on the GitHub URL', () => { + const url = 'https://github.com/johndoe/service-to-be-downloaded'; + + return downloadTemplateFromRepo(url).then(() => { + expect(downloadStub.calledOnce).to.equal(true); + expect(downloadStub.args[0][0]).to.equal(`${url}/archive/master.zip`); + }); + }); + + it('should throw an error if the same service name exists as directory in Github', () => { + const url = 'https://github.com/serverless/examples/tree/master/rest-api-with-dynamodb'; + const serviceDirName = path.join(servicePath, 'rest-api-with-dynamodb'); + fse.mkdirsSync(serviceDirName); + + expect(() => downloadTemplateFromRepo(null, url)).to.throw(Error); + }); + + it('should download and rename the service based on the GitHub URL', () => { + const url = 'https://github.com/johndoe/service-to-be-downloaded'; + const name = 'new-service-name'; + + downloadStub.returns( + remove(newServicePath) + .then(() => { + const sp = downloadStub.args[0][1]; + const slsYml = path.join( + sp, + 'serverless.yml'); + writeFileSync(slsYml, 'service: service-name'); + })); + + return downloadTemplateFromRepo(url, name).then(() => { + expect(downloadStub.calledOnce).to.equal(true); + expect(downloadStub.args[0][1]).to.contain(name); + expect(downloadStub.args[0][0]).to.equal(`${url}/archive/master.zip`); + const yml = readFileSync(path.join(newServicePath, 'serverless.yml')); + expect(yml.service).to.equal(name); + }); + }); + + it('should download and rename the service based directories in the GitHub URL', () => { + const url = 'https://github.com/serverless/examples/tree/master/rest-api-with-dynamodb'; + const name = 'new-service-name'; + + downloadStub.returns( + remove(newServicePath) + .then(() => { + const sp = downloadStub.args[0][1]; + const slsYml = path.join( + sp, + 'rest-api-with-dynamodb', + 'serverless.yml'); + writeFileSync(slsYml, 'service: service-name'); + })); + + return downloadTemplateFromRepo(url, name).then(() => { + expect(downloadStub.calledOnce).to.equal(true); + const yml = readFileSync(path.join(newServicePath, 'serverless.yml')); + expect(yml.service).to.equal(name); + }); + }); + }); + + describe('parseRepoURL', () => { + it('should throw an error if no URL is provided', () => { + expect(parseRepoURL).to.throw(Error); + }); + + it('should throw an error if URL is not valid', () => { + try { + parseRepoURL('non_valid_url'); + } catch (e) { + expect(e).to.be.an.instanceOf(Error); + } + }); + + it('should throw an error if URL is not of valid provider', () => { + try { + parseRepoURL('https://kostasbariotis.com/repo/owner'); + } catch (e) { + expect(e).to.be.an.instanceOf(Error); + } + }); + + it('should parse a valid GitHub URL', () => { + const output = parseRepoURL('https://github.com/serverless/serverless'); + + expect(output).to.deep.eq({ + owner: 'serverless', + repo: 'serverless', + branch: 'master', + downloadUrl: 'https://github.com/serverless/serverless/archive/master.zip', + isSubdirectory: false, + pathToDirectory: '', + }); + }); + + it('should parse a valid GitHub URL with subdirectory', () => { + const output = parseRepoURL('https://github.com/serverless/serverless/tree/master/assets'); + + expect(output).to.deep.eq({ + owner: 'serverless', + repo: 'serverless', + branch: 'master', + downloadUrl: 'https://github.com/serverless/serverless/archive/master.zip', + isSubdirectory: true, + pathToDirectory: 'assets', + }); + }); + + it('should parse a valid BitBucket URL ', () => { + const output = parseRepoURL('https://bitbucket.org/atlassian/localstack'); + + expect(output).to.deep.eq({ + owner: 'atlassian', + repo: 'localstack', + branch: 'master', + downloadUrl: 'https://bitbucket.org/atlassian/localstack/get/master.zip', + isSubdirectory: false, + pathToDirectory: '', + }); + }); + + it('should parse a valid BitBucket URL with subdirectory', () => { + const output = parseRepoURL('https://bitbucket.org/atlassian/localstack/src/85870856fd6941ae75c0fa946a51cf756ff2f53a/localstack/dashboard/?at=mvn'); + + expect(output).to.deep.eq({ + owner: 'atlassian', + repo: 'localstack', + branch: 'mvn', + downloadUrl: 'https://bitbucket.org/atlassian/localstack/get/mvn.zip', + isSubdirectory: true, + pathToDirectory: `localstack${path.sep}dashboard`, + }); + }); + }); +}); diff --git a/lib/utils/fs/copyDirContentsSync.js b/lib/utils/fs/copyDirContentsSync.js new file mode 100644 index 000000000..5c38378b2 --- /dev/null +++ b/lib/utils/fs/copyDirContentsSync.js @@ -0,0 +1,16 @@ +'use strict'; + +const path = require('path'); +const fse = require('./fse'); +const walkDirSync = require('./walkDirSync'); + +function fileExists(srcDir, destDir) { + const fullFilesPaths = walkDirSync(srcDir); + + fullFilesPaths.forEach(fullFilePath => { + const relativeFilePath = fullFilePath.replace(srcDir, ''); + fse.copySync(fullFilePath, path.join(destDir, relativeFilePath)); + }); +} + +module.exports = fileExists; diff --git a/lib/utils/fs/dirExistsSync.js b/lib/utils/fs/dirExistsSync.js new file mode 100644 index 000000000..d04d53de5 --- /dev/null +++ b/lib/utils/fs/dirExistsSync.js @@ -0,0 +1,14 @@ +'use strict'; + +const fse = require('./fse'); + +function dirExistsSync(dirPath) { + try { + const stats = fse.statSync(dirPath); + return stats.isDirectory(); + } catch (e) { + return false; + } +} + +module.exports = dirExistsSync; diff --git a/lib/utils/renameService.js b/lib/utils/renameService.js new file mode 100644 index 000000000..c0da76f91 --- /dev/null +++ b/lib/utils/renameService.js @@ -0,0 +1,39 @@ +const path = require('path'); +const fse = require('fs-extra'); + +const fileExistsSync = require('./fs/fileExistsSync'); +const readFileSync = require('./fs/readFileSync'); +const writeFileSync = require('./fs/writeFileSync'); +const ServerlessError = require('../classes/Error').ServerlessError; + +function renameService(name, servicePath) { + const serviceFile = path.join(servicePath, 'serverless.yml'); + const packageFile = path.join(servicePath, 'package.json'); + + if (!fileExistsSync(serviceFile)) { + const errorMessage = [ + 'serverless.yml not found in', + ` ${servicePath}`, + ].join(''); + throw new ServerlessError(errorMessage); + } + + const serverlessYml = + fse.readFileSync(serviceFile, 'utf-8') + .replace(/service\s*:.+/gi, (match) => { + const fractions = match.split('#'); + fractions[0] = `service: ${name}`; + return fractions.join(' #'); + }); + + fse.writeFileSync(serviceFile, serverlessYml); + + if (fileExistsSync(packageFile)) { + const json = readFileSync(packageFile); + writeFileSync(packageFile, Object.assign(json, { name })); + } + + return name; +} + +module.exports.renameService = renameService; diff --git a/lib/utils/renameService.test.js b/lib/utils/renameService.test.js new file mode 100644 index 000000000..0eecc66cd --- /dev/null +++ b/lib/utils/renameService.test.js @@ -0,0 +1,106 @@ +'use strict'; + +const expect = require('chai').expect; +const Serverless = require('../Serverless'); +const testUtils = require('../../tests/utils'); +const fse = require('fs-extra'); +const path = require('path'); + +const renameService = require('./renameService').renameService; + +describe('renameService', () => { + let serverless; + let cwd; + + let servicePath; + + beforeEach(() => { + const tmpDir = testUtils.getTmpDirPath(); + cwd = process.cwd(); + + fse.mkdirsSync(tmpDir); + process.chdir(tmpDir); + + servicePath = tmpDir; + + serverless = new Serverless(); + serverless.init(); + }); + + afterEach(() => { + // change back to the old cwd + process.chdir(cwd); + }); + + it('should set new service in serverless.yml and name in package.json', () => { + const defaultServiceYml = + 'service: service-name\n\nprovider:\n name: aws\n'; + const newServiceYml = + 'service: new-service-name\n\nprovider:\n name: aws\n'; + + const defaultServiceName = 'service-name'; + const newServiceName = 'new-service-name'; + + const packageFile = path.join(servicePath, 'package.json'); + const serviceFile = path.join(servicePath, 'serverless.yml'); + + serverless.utils.writeFileSync(packageFile, { name: defaultServiceName }); + fse.writeFileSync(serviceFile, defaultServiceYml); + + renameService(newServiceName, servicePath); + const serviceYml = fse.readFileSync(serviceFile, 'utf-8'); + const packageJson = serverless.utils.readFileSync(packageFile); + expect(serviceYml).to.equal(newServiceYml); + expect(packageJson.name).to.equal(newServiceName); + }); + + it('should set new service in commented serverless.yml and name in package.json', () => { + const defaultServiceYml = + '# comment\nservice: service-name #comment\n\nprovider:\n name: aws\n# comment'; + const newServiceYml = + '# comment\nservice: new-service-name #comment\n\nprovider:\n name: aws\n# comment'; + + const defaultServiceName = 'service-name'; + const newServiceName = 'new-service-name'; + + const packageFile = path.join(servicePath, 'package.json'); + const serviceFile = path.join(servicePath, 'serverless.yml'); + + serverless.utils.writeFileSync(packageFile, { name: defaultServiceName }); + fse.writeFileSync(serviceFile, defaultServiceYml); + + renameService(newServiceName, servicePath); + const serviceYml = fse.readFileSync(serviceFile, 'utf-8'); + const packageJson = serverless.utils.readFileSync(packageFile); + expect(serviceYml).to.equal(newServiceYml); + expect(packageJson.name).to.equal(newServiceName); + }); + + it('should set new service in commented serverless.yml without existing package.json', () => { + const defaultServiceYml = + '# comment\nservice: service-name #comment\n\nprovider:\n name: aws\n# comment'; + const newServiceYml = + '# comment\nservice: new-service-name #comment\n\nprovider:\n name: aws\n# comment'; + + const serviceFile = path.join(servicePath, 'serverless.yml'); + + serverless.utils.writeFileDir(serviceFile); + fse.writeFileSync(serviceFile, defaultServiceYml); + + renameService('new-service-name', servicePath); + const serviceYml = fse.readFileSync(serviceFile, 'utf-8'); + expect(serviceYml).to.equal(newServiceYml); + }); + + it('should fail to set new service name in serverless.yml', () => { + const defaultServiceYml = + '# comment\nservice: service-name #comment\n\nprovider:\n name: aws\n# comment'; + + const serviceFile = path.join(servicePath, 'serverledss.yml'); + + serverless.utils.writeFileDir(serviceFile); + fse.writeFileSync(serviceFile, defaultServiceYml); + + expect(() => renameService('new-service-name', servicePath)).to.throw(Error); + }); +}); diff --git a/package.json b/package.json index 41f3fe3c0..42e13af39 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "archiver": "^1.1.0", "async": "^1.5.2", "aws-sdk": "^2.75.0", - "bluebird": "^3.4.0", + "bluebird": "^3.5.0", "chalk": "^2.0.0", "ci-info": "^1.1.1", "download": "^5.0.2", diff --git a/scripts/postinstall.js b/scripts/postinstall.js index a32f64078..bad733a49 100644 --- a/scripts/postinstall.js +++ b/scripts/postinstall.js @@ -27,12 +27,12 @@ function setupAutocomplete() { const tabtabCliPath = path.join(tabtabPath, 'src', 'cli.js'); try { - execSync(`node ${tabtabCliPath} install --name serverless --auto`); - execSync(`node ${tabtabCliPath} install --name sls --auto`); + execSync(`node "${tabtabCliPath}" install --name serverless --auto`); + execSync(`node "${tabtabCliPath}" install --name sls --auto`); return resolve(); } catch (error) { - execSync(`node ${tabtabCliPath} install --name serverless --stdout`); - execSync(`node ${tabtabCliPath} 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 badbe786a..4ad8b7041 100755 --- a/tests/templates/test_all_templates +++ b/tests/templates/test_all_templates @@ -24,4 +24,5 @@ integration-test aws-nodejs-ecma-script integration-test google-nodejs integration-test spotinst-nodejs integration-test spotinst-python -integration-test webtasks-nodejs +integration-test spotinst-ruby +integration-test webtasks-nodejs \ No newline at end of file