diff --git a/.appveyor.yml b/.appveyor.yml deleted file mode 100644 index f6818ae30..000000000 --- a/.appveyor.yml +++ /dev/null @@ -1,24 +0,0 @@ -environment: - global: - SLS_IGNORE_WARNING: "*" - matrix: - - NODEJS_VERSION: "4" - - NODEJS_VERSION: "6" - - NODEJS_VERSION: "8" - -install: - # For Ruby invoke local testing - - set PATH=C:\Ruby25\bin;%PATH% - # Get the version of Node.js - - ps: Install-Product node $Env:NODEJS_VERSION - # install modules - - rm -rf node_modules - - npm install - - node --version - - npm --version - -test_script: - - npm test - -# Don't actually build. -build: off diff --git a/.editorconfig b/.editorconfig index 49dafa5e2..28009b667 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,3 +8,6 @@ indent_size = 2 indent_style = space insert_final_newline = true trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index c29438b87..000000000 --- a/.eslintignore +++ /dev/null @@ -1,5 +0,0 @@ -coverage -node_modules -tmp -tmpdirs-serverless -lib/plugins/create/templates/** diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 778635bfb..000000000 --- a/.eslintrc.js +++ /dev/null @@ -1,18 +0,0 @@ -module.exports = { - "root": true, - "extends": "airbnb", - "plugins": [], - "rules": { - "func-names": "off", - - // doesn't work in node v4 :( - "strict": "off", - "prefer-rest-params": "off", - "react/require-extension": "off", - "import/no-extraneous-dependencies": "off" - }, - "env": { - "mocha": true, - "jest": true - } -}; diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index ded04c7a1..7dc59f594 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -16,17 +16,18 @@ about: Create a report to help us improve ## Description -* What went wrong? -* What did you expect should have happened? -* What was the config you used? -* What stacktrace or error message from your provider did you see? +- What went wrong? +- What did you expect should have happened? +- What was the config you used? +- What stacktrace or error message from your provider did you see? Similar or dependent issues: -* #12345 + +- #12345 ## Additional Data -* ***Serverless Framework Version you're using***: -* ***Operating System***: -* ***Stack Trace***: -* ***Provider Error messages***: +- **_Serverless Framework Version you're using_**: +- **_Operating System_**: +- **_Stack Trace_**: +- **_Provider Error messages_**: diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 1374800f2..5c520999f 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -16,8 +16,9 @@ about: Suggest an idea for serverless framework ## Description -* What is the use case that should be solved. The more detail you describe this in the easier it is to understand for us. -* If there is additional config how would it look +- What is the use case that should be solved. The more detail you describe this in the easier it is to understand for us. +- If there is additional config how would it look Similar or dependent issues: -* #12345 + +- #12345 diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 35838a3e7..4d9bf3c49 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -34,13 +34,21 @@ Examples: ## Todos: -- [ ] Write tests +_**Note: Run `npm run test-ci` to run all validation checks on proposed changes**_ + +- [ ] Write tests and confirm existing functionality is not broken. + **Validate via `npm test`** - [ ] Write documentation -- [ ] Fix linting errors +- [ ] Ensure there are no lint errors. + **Validate via `npm run lint-updated`** + _Note: Some reported issues can be automatically fixed by running `npm run lint:fix`_ +- [ ] Ensure introduced changes match Prettier formatting. + **Validate via `npm run prettier-check-updated`** + _Note: All reported issues can be automatically fixed by running `npm run prettify-updated`_ - [ ] Make sure code coverage hasn't dropped - [ ] Provide verification config / commands / resources - [ ] Enable "Allow edits from maintainers" for this PR - [ ] Update the messages below -***Is this ready for review?:*** NO -***Is it a breaking change?:*** NO +**_Is this ready for review?:_** NO +**_Is it a breaking change?:_** NO diff --git a/.gitignore b/.gitignore index 7ecc11ddb..71b581d34 100755 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ *.log npm-debug.log npm-shrinkwrap.json +/package-lock.json # Runtime data pids @@ -12,8 +13,9 @@ dist # Directory for instrumented libs generated by jscoverage/JSCover lib-cov -# Coverage directory used by tools like istanbul +# Coverage directory used by tools like nyc coverage +/.nyc_output # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt @@ -57,5 +59,7 @@ jest # DotNet obj/ -[Bb]in/ [Oo]bj/ + +# Tests +!tests/**/*.zip diff --git a/.jsbeautifyrc b/.jsbeautifyrc deleted file mode 100644 index 059689735..000000000 --- a/.jsbeautifyrc +++ /dev/null @@ -1,48 +0,0 @@ -{ - "html": { - "allowed_file_extensions": ["htm", "html", "xhtml", "shtml", "xml", "svg"], - "brace_style": "collapse", - "end_with_newline": false, - "indent_char": " ", - "indent_handlebars": false, - "indent_inner_html": false, - "indent_scripts": "keep", - "indent_size": 2, - "max_preserve_newlines": 4, - "preserve_newlines": true, - "unformatted": ["a", "span", "img", "code", "pre", "sub", "sup", "em", "strong", "b", "i", "u", "strike", "big", "small", "pre", "h1", "h2", "h3", "h4", "h5", "h6"], - "wrap_line_length": 0 - }, - "css": { - "allowed_file_extensions": ["css", "scss", "sass", "less"], - "end_with_newline": true, - "indent_char": " ", - "indent_size": 2, - "max_preserve_newlines": 4, - "newline_between_rules": true, - "selector_separator": " ", - "selector_separator_newline": true - }, - "js": { - "allowed_file_extensions": ["jshintrc", "jsbeautifyrc"], - "brace_style": "collapse", - "break_chained_methods": false, - "e4x": false, - "end_with_newline": false, - "indent_char": " ", - "indent_level": 0, - "indent_size": 2, - "indent_with_tabs": false, - "jslint_happy": false, - "keep_array_indentation": false, - "keep_function_indentation": false, - "max_preserve_newlines": 0, - "preserve_newlines": true, - "space_after_anon_function": false, - "space_before_conditional": true, - "space_in_empty_paren": false, - "space_in_paren": false, - "unescape_strings": false, - "wrap_line_length": 0 - } -} diff --git a/.travis.yml b/.travis.yml index 3bf161084..e05e5c044 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,62 +1,128 @@ language: node_js -matrix: - exclude: - - node_js: '6.2' - env: - - secure: ruPmxtjLY9jX/TQQe1d8vxwL0KsjsTzJK/EL6rZM954JJc+3c5gshxB1Ca9+WRwIGdMjCZgKo4dQcn2DrAap7KoDz+GRCi40kc5AOr2ZLmjxJeCJls+sQ713QJ01GgIA1X2zmz/bDSw5lan8ODR3eYI0fIn37ZV4yxNJ5PObBLCMQW6nnOdMXxcLLi2iJRqu3k0gb85wsUNOB0AF2cBrbxoYmGsrGjGuPjIXPeEdnVJ0jzBY+jDrBraIHEBN9tecIyG77ZgEhlPopqTrj39dEPMwAow+derEVWugPRF3Wps6z+Yx0qtxETptWuOgn0V9GpfZF9vJlHifQUuuJEGtttVCYjZCUOIoIRwWnX+/SygjlgLJ9PpVa6XyZfnqygJXnv7wouGGkiyZEDbayu65qP3Ls/Dj+N2nEtamOPf3dGBeaX8KKF61h80bH7o9JL/t6yxyvm+yDRTkYd9SNk3U7dFXoBcCcjMNNUkKBKtY9G3EZlE0ZV0D7jD33Na4yX8mKBHkYLKXCAn+rh6DIwLNkV8IOtAYD77qan6v4qH9fpPMX4UkLu9SRM1r5RfsZU0gX7sCP9OKFKkAx5BWUqjg+D6Xa0EACtO8bPDHA548hWU2vYx7ghuVJfVNOUlAl97OXclx2SZwXJ46rHaHgrH2I3gi3qyHMib3iUV7SLdRQnY= - - secure: p9ka7mRzo/ecjnDh/dz19g0iVfQdvsGRAtg/4ONeiq75I2+oqHzu+VxUBA1Z2IQbpCEAMo21CarR3fg2I6MFUeazL0nEpqr1PoOAI8nPFeQlg/h+jLXsrPAkDcu2/b8ij7J5MXeLdZXUVqiPcGkr68x/tCMk/rwxftljQhvXPQfc7Lxm/m61ELnC7rLJulhxWZLNIq1hwQ9nh0GMKb4hm0KmPn8ksccVL+wyDikkgXCuvIujhTBjhNivAe4mG8mqnNsW1Ugh++SUe1ld27TtbH7wQj02SSG4Bxfwc3Gz0GFdAL1GyOkWI2WvrqP4a0KYTRUo+pUr9E+HZ1SNlxU5t6QWtmDiy5MKkxzgeTXmkKiJ98vMlF0ja5bpp46NjYarzDafqE8FozHzLtr+uAtqr6gRAgU1rWaG9BE3gKeW/f4B/2MfPI26b7SxuU1MwGVy0I76hb0Ujbgb3X8G4TYTGb6Nhoewc+RZExPwVhfrN8cJjo45masndv5tQAZMSRX/JUFjs4h/QMXNsn0A53GXgf6eIzUu15m+W8TJYFiKQeq9nMejzEE4sWMO3BFnkxueBGVCEurOc1GgdEnKxeqlp+psxHcJRlNCxC1HkUVOzfpkCr/Jy42vM8jQomAMv41Z9zWjOagVphWT25xNeSILfRt4yPku5wfW4CAxp+fl4KQ= - include: - - node_js: '4.4' - env: SLS_IGNORE_WARNING=* - - node_js: '5.11' - env: SLS_IGNORE_WARNING=* - - node_js: '6.2' - env: SLS_IGNORE_WARNING=* - - node_js: '6.2' - env: - - INTEGRATION_TEST=true - - INTEGRATION_TEST_SUITE=simple - - 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: '8.9' - env: SLS_IGNORE_WARNING=* - - node_js: '8.9' - env: - - INTEGRATION_TEST=true - - INTEGRATION_TEST_SUITE=simple - - 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: '8.9' - env: - - DISABLE_TESTS=true - - LINTING=true - - node_js: '10.6' - env: - - DISABLE_TESTS=true - - LINTING=true - - node_js: '10.7' - env: - - DISABLE_TESTS=true - - LINTING=true -sudo: false + +git: + # Minimize git history, but ensure to not break things: + # - Merging multiple PR's around same time may introduce a case where it's not + # the last merge commit that is to be tested + depth: 10 + +cache: + # Not relying on 'npm' shortcut, as per Travis docs it's the only 'node_modules' that it'll cache + directories: + - $HOME/.npm + - node_modules + +# Ensure to install dependencies at their latest versions install: -- travis_retry npm install -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 -after_success: -- cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage -deploy: - provider: npm - email: services@serverless.com - skip_cleanup: true - on: - tags: true - node: 10.7 - api_key: - secure: EgoetjrRlGfvGnmVp8A0btr1CzB0hl7owVIpbfk4zXJzhGEbHoVu0CG0IdmyLN+JlaZa7EDJTjkDCd6g3fVAh9TT7ZCeaq8YwbZDrql7mAJj7xYQAyM4eSkc95BRzcFJBx7Mxr6H90IDLxKr6ZtB+HEdiHN+59XbepKYYJeb1jHfnKn5xzOqk4BdnZo6pKfudfeO+7/BwJJ0FwlFA40bY2HS/Lp+NG/2IedNR7k3m/5W83/XH5qlWP8jhBKlxrAzks27aNo+42xHkRCVyPViJKq0mfz1hl2bfswChWHgaCuajp+0amNL39pgIX9eXxFc3bNX9Iftox5t31elEhsw06vvuAaVkKEd+VEMaDySbQ9M+irKZeREg+NFYZLnc2WiEE3Sexo6hm9eM2q2KEZ7bleN9B0CQAut1XXLRQEts80rzss4Z2Q7AZb9cOYBQlj9Wf1X0Y74UqvnDn83a4Y38a+lhx7J2q691ZeM1UFSCdO0QfeJRkB55bSyHqUqrLAqUN7eNsKGdBH0kvYIGFREgGgReEpBRAuNqWuJ/5qexp63Kbf+09raG5IvfxSIM5fJ5KE5VxSduBdRnSH0GNKfjuq296/Rg4fmm/bygZ3Yk5L6Wd41SUU8uHzlZFBwtcvxAKDTQe6s+5JU24ilqxOx6J4Ut34X3dIbLLAmoB1ogdM= + # Note: with `npm update` there seems no way to update all project dependency groups in one run + - npm update --no-save # Updates just dependencies + # Note: npm documents --dev option for dev dependencies update, but it's only --save-dev that works + - npm update --save-dev --no-save # Updates just devDependencies + +branches: + only: + - master # Do not build PR branches + - /^v\d+\.\d+\.\d+$/ # Ensure to build release tags + +env: + global: + - SLS_IGNORE_WARNING=* + - FORCE_COLOR=1 # Ensure colored output (color support is not detected in some cases) + +stages: + - name: Test + - name: Integration Test + if: branch = master AND type = push + - name: Deploy + if: tag =~ ^v\d+\.\d+\.\d+$ + +before_script: + # Fail build right after first script fails. Travis doesn't ensure that: https://github.com/travis-ci/travis-ci/issues/1066 + # More info on below line: https://www.davidpashley.com/articles/writing-robust-shell-scripts/#idm5413512 + - set -e + +# Ensure to fail build if deploy fails, Travis doesn't ensure that: https://github.com/travis-ci/travis-ci/issues/921 +before_deploy: + # Remove eventual old npm logs + - rm -rf ~/.npm/_logs +after_deploy: + - | + # npm creates log only on failure + if [ -d ~/.npm/_logs ]; then + # Undocumented way to force Travis build to fail + travis_terminate 1 + fi + +jobs: + include: + # In most cases it's best to configure one job per Platform & Node.js version combination + # (job boot & setup takes ca 1 minute, one task run usually lasts seconds) + + # PR's + - name: 'Prettier check updated, Lint updated, Unit Tests - Linux - Node.js v12' + if: type = pull_request + node_js: 12 + script: + - npm run prettier-check-updated + - npm run lint-updated + - npm test + + # master branch and version tags + - name: 'Lint, Unit Tests - Linux - Node.js v12' + if: type != pull_request + node_js: 12 + script: + - npm run lint + - npm test + + - name: 'Unit Tests - Windows - Node.js v12' + os: windows + node_js: 12 + before_install: + # Ensure Python 2 in Windows enviroment (Ruby is already preinstalled) + - | + if [ $TRAVIS_OS_NAME = windows ] + then + choco install python2 && + export PATH="/c/Python27:/c/Python27/Scripts:$PATH" + fi + + - name: 'Isolated Unit Tests, Package Integration Tests - Linux - Node.js v10' + node_js: 10 + script: + - npm run test-isolated + - npm run integration-test-run-package + + - name: 'Unit Tests, Coverage - Linux - Node.js v8' + node_js: 8 + script: npm run coverage + after_success: + - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js + - rm -rf ./coverage + + - name: 'Unit Tests - Linux - Node.js v6' + node_js: 6 + + - stage: Integration Test + name: 'Integration Tests - Linux - Node.js v12' + node_js: 12 + env: + # AWS_ACCESS_KEY_ID + - secure: Ia2nYzOeYvTE6qOP7DBKX3BO7s/U7TXdsvB2nlc3kOPFi//IbTVD0/cLKCAE5XqTzrrliHINSVsFcJNSfjCwmDSRmgoIGrHj5CJkWpkI6FEPageo3mdqFQYEc8CZeAjsPBNaHe6Ewzg0Ev/sjTByLSJYVqokzDCF1QostSxx1Ss6SGt1zjxeP/Hp4yOJn52VAm9IHAKYn7Y62nMAFTaaTPUQHvW0mJj6m2Z8TWyPU+2Bx6mliO65gTPFGs+PdHGwHtmSF/4IcUO504x+HjDuwzW2itomLXZmIOFfGDcFYadKWzVMAfJzoRWOcVKF4jXdMoSCOviWpHGtK35E7K956MTXkroVoWCS7V0knQDovbRZj8c8td8mS4tdprUA+TzgZoHet2atWNtMuTh79rdmwoAO+IAWJegYj62Tdfy3ycESzY+KxSaV8kysG9sR3PRFoWjZerA7MhLZEzQMORXDGjJlgwLaZfYVqjlsGe5p5etFBUTd0WbFgSwOKLoA2U/fm7WzqItkjs3UWaHuvFVvwYixGxjEVmVczS6wa2cdGpHtVD9H7km4fPEzljHqQ26v0P5e8eylgqLF2IB6mL7UqGFrAtrMvAgN/M3gnq4dTs/wq1AJIOxEP7YW7kc0NAldk8vUz6t5GzCPNcuukxAku91Awnh0twxgUywatgJLZPY= + # AWS_SECRET_ACCESS_KEY + - secure: Dgaa5XIsA5Vbw/CYQLUAuVVsDX26C8+f1XYGwsbNmFQKbKvM8iy9lGrHlfrT3jftJkJH6re8tP1RjyZjjzLe25KPk4Tps7grNteCyiIIEDsC2aHhiXHD6zNHsItpxYusaFfyQinFWnK4CAYKWb9ZNIwHIDUIB4vq807QGAhYsnoj1Lg/ajWvtEKBwYjEzDz9OjB91lw7lpCnHtmKKw5A+TNIVGpDDZ/jRBqETsPaePtiXC9UTHZQyM3gFoeVXiJw9KSU/gjIx9REihCaWWPbnuQSeIONGGlVWY9V4DTZIsJr9/uwDcbioeXDD3G1ezGtNPPRSNTtq08QlUtE4mEtKea/+ObpllKZCeZGn6AJhMn+uqMIP95FFlqBB55YzRcLZY+Igi/qm/9LJ9RinAhxRVXiwzeQ+BdVA6jshAAzr+7wklux6lZAa0xGw9pgTv7MI4RP2LJ/LMP1ppFsnv9n/qt93Ax1VEwEu3xHZe3VTYL9tbXOPTZutf6fKjUrW7wSSuy637queESjYnnPKSb1vZcPxjSFlyh+GJvxu/3PurF9aqfiBdiorIBre+pQS4lakLtoft5nsbA+4iYUwrXR58qUPVUqQ7a0A0hedOWlp6g9ixLa6nugUP5aobJzR71T8l/IjqpnY2EEd/iINEb0XfUiZtB5zHaqFWejBtmWwCI= + script: + - npm run integration-test-run-basic || { npm run integration-test-cleanup; exit 1; } + - npm run integration-test-run-all || { npm run integration-test-cleanup; exit 1; } + - npm run integration-test-cleanup + + - stage: Deploy + node_js: 12 + script: skip + deploy: + provider: npm + email: services@serverless.com + on: + tags: true + api_key: + secure: EgoetjrRlGfvGnmVp8A0btr1CzB0hl7owVIpbfk4zXJzhGEbHoVu0CG0IdmyLN+JlaZa7EDJTjkDCd6g3fVAh9TT7ZCeaq8YwbZDrql7mAJj7xYQAyM4eSkc95BRzcFJBx7Mxr6H90IDLxKr6ZtB+HEdiHN+59XbepKYYJeb1jHfnKn5xzOqk4BdnZo6pKfudfeO+7/BwJJ0FwlFA40bY2HS/Lp+NG/2IedNR7k3m/5W83/XH5qlWP8jhBKlxrAzks27aNo+42xHkRCVyPViJKq0mfz1hl2bfswChWHgaCuajp+0amNL39pgIX9eXxFc3bNX9Iftox5t31elEhsw06vvuAaVkKEd+VEMaDySbQ9M+irKZeREg+NFYZLnc2WiEE3Sexo6hm9eM2q2KEZ7bleN9B0CQAut1XXLRQEts80rzss4Z2Q7AZb9cOYBQlj9Wf1X0Y74UqvnDn83a4Y38a+lhx7J2q691ZeM1UFSCdO0QfeJRkB55bSyHqUqrLAqUN7eNsKGdBH0kvYIGFREgGgReEpBRAuNqWuJ/5qexp63Kbf+09raG5IvfxSIM5fJ5KE5VxSduBdRnSH0GNKfjuq296/Rg4fmm/bygZ3Yk5L6Wd41SUU8uHzlZFBwtcvxAKDTQe6s+5JU24ilqxOx6J4Ut34X3dIbLLAmoB1ogdM= diff --git a/0.5.x-RESOURCES.md b/0.5.x-RESOURCES.md index bde126bf2..a3aa0d860 100644 --- a/0.5.x-RESOURCES.md +++ b/0.5.x-RESOURCES.md @@ -5,37 +5,41 @@ Below are projects and plugins relating to version 0.5 and below. Note that thes You can read the v0.5.x documentation at [readme.io](https://serverless.readme.io/v0.5.0/docs). ## Projects (v0.5.x) -Serverless Projects are shareable and installable. You can publish them to npm and install them via the Serverless Framework CLI by using `$ serverless project install ` -* [serverless-graphql](https://github.com/serverless/serverless-graphql) - Official Serverless boilerplate to kick start your project -* [serverless-starter](https://github.com/serverless/serverless-starter) - A simple boilerplate for new projects (JavaScript) with a few architectural options -* [serverless-starter-python](https://github.com/alexcasalboni/serverless-starter-python) - A simple boilerplate for new projects (Python) with a few architectural options -* [serverless-graphql-blog](https://github.com/serverless/serverless-graphql-blog) - A blog boilerplate that leverages GraphQL in front of DynamoDB to offer a minimal REST API featuring only 1 endpoint -* [serverless-authentication-boilerplate](https://github.com/laardee/serverless-authentication-boilerplate) - A generic authentication boilerplate for Serverless framework -* [sc5-serverless-boilerplate](https://github.com/SC5/sc5-serverless-boilerplate) - A boilerplate for test driven development of REST endpoints -* [MoonMail] (https://github.com/microapps/MoonMail) - Build your own email marketing infrastructure using Lambda + SES + +Serverless Projects are shareable and installable. You can publish them to npm and install them via the Serverless Framework CLI by using `$ serverless project install ` + +- [serverless-graphql](https://github.com/serverless/serverless-graphql) - Official Serverless boilerplate to kick start your project +- [serverless-starter](https://github.com/serverless/serverless-starter) - A simple boilerplate for new projects (JavaScript) with a few architectural options +- [serverless-starter-python](https://github.com/alexcasalboni/serverless-starter-python) - A simple boilerplate for new projects (Python) with a few architectural options +- [serverless-graphql-blog](https://github.com/serverless/serverless-graphql-blog) - A blog boilerplate that leverages GraphQL in front of DynamoDB to offer a minimal REST API featuring only 1 endpoint +- [serverless-authentication-boilerplate](https://github.com/laardee/serverless-authentication-boilerplate) - A generic authentication boilerplate for Serverless framework +- [sc5-serverless-boilerplate](https://github.com/SC5/sc5-serverless-boilerplate) - A boilerplate for test driven development of REST endpoints +- [MoonMail](https://github.com/microapps/MoonMail) - Build your own email marketing infrastructure using Lambda + SES ## Plugins (v0.5.x) -Serverless is composed of Plugins. A group of default Plugins ship with the Framework, and here are some others you can add to improve/help your workflow: -* [Meta Sync](https://github.com/serverless/serverless-meta-sync) - Securely sync your the variables in your project's `_meta/variables` across your team. -* [Hook Scripts](https://github.com/kennu/serverless-plugin-hookscripts) - Easily create shell script hooks that are run whenever Serverless actions are executed. -* [CORS](https://github.com/joostfarla/serverless-cors-plugin) - Adds support for CORS (Cross-origin resource sharing). -* [Serve](https://github.com/Nopik/serverless-serve) - Simulate API Gateway locally, so all function calls can be run via localhost. -* [Webpack](https://github.com/asprouse/serverless-webpack-plugin) - Use Webpack to optimize your Serverless Node.js Functions. -* [Serverless Client](https://github.com/serverless/serverless-client-s3) - Deploy and config a web client for your Serverless project to S3. -* [Alerting](https://github.com/martinlindenberg/serverless-plugin-alerting) - This Plugin adds Cloudwatch Alarms with SNS notifications for your Lambda functions. -* [Optimizer](https://github.com/serverless/serverless-optimizer-plugin) - Optimizes your code for performance in Lambda. Supports coffeeify, babelify and other transforms -* [CloudFormation Validator](https://github.com/tmilewski/serverless-resources-validation-plugin) - Adds support for validating your CloudFormation template. -* [Prune](https://github.com/Nopik/serverless-lambda-prune-plugin) - Delete old versions of AWS lambdas from your account so that you don't exceed the code storage limit. -* [Base-Path](https://github.com/daffinity/serverless-base-path-plugin) - Sets a base path for all API Gateway endpoints in a Component. -* [Test](https://github.com/arabold/serverless-test-plugin) - A Simple Integration Test Framework for Serverless. -* [SNS Subscribe](https://github.com/martinlindenberg/serverless-plugin-sns) - This plugin easily subscribes your lambda functions to SNS notifications. -* [JSHint](https://github.com/joostfarla/serverless-jshint-plugin) - Detect errors and potential problems in your Lambda functions. -* [ESLint](https://github.com/nishantjain91/serverless-eslint-plugin) - Detect errors and potential problems in your Lambda functions using eslint. -* [Mocha](https://github.com/SC5/serverless-mocha-plugin) - Enable test driven development by creating test cases when creating new functions -* [Function-Package](https://github.com/HyperBrain/serverless-package-plugin) - Package your lambdas without deploying to AWS. -* [Sentry](https://github.com/arabold/serverless-sentry-plugin) - Automatically send errors and exceptions to [Sentry](https://getsentry.com). -* [Auto-Prune](https://github.com/arabold/serverless-autoprune-plugin) - Delete old AWS Lambda versions. -* [Serverless Secrets](https://github.com/trek10inc/serverless-secrets) - Easily encrypt and decrypt secrets in your Serverless projects -* [Serverless DynamoDB Local](https://github.com/99xt/serverless-dynamodb-local) - Simulate DynamoDB instance locally. -* [Serverless Dependency Install](https://github.com/99xt/serverless-dependency-install) - Manage node, serverless dependencies easily within the project. -* [Serverless Header Function](https://github.com/blackevil245/serverless-header-function) - Automatically run a javascript script on every Serverless action hooks. + +Serverless is composed of Plugins. A group of default Plugins ship with the Framework, and here are some others you can add to improve/help your workflow: + +- [Meta Sync](https://github.com/serverless/serverless-meta-sync) - Securely sync your the variables in your project's `_meta/variables` across your team. +- [Hook Scripts](https://github.com/kennu/serverless-plugin-hookscripts) - Easily create shell script hooks that are run whenever Serverless actions are executed. +- [CORS](https://github.com/joostfarla/serverless-cors-plugin) - Adds support for CORS (Cross-origin resource sharing). +- [Serve](https://github.com/Nopik/serverless-serve) - Simulate API Gateway locally, so all function calls can be run via localhost. +- [Webpack](https://github.com/asprouse/serverless-webpack-plugin) - Use Webpack to optimize your Serverless Node.js Functions. +- [Serverless Client](https://github.com/serverless/serverless-client-s3) - Deploy and config a web client for your Serverless project to S3. +- [Alerting](https://github.com/martinlindenberg/serverless-plugin-alerting) - This Plugin adds Cloudwatch Alarms with SNS notifications for your Lambda functions. +- [Optimizer](https://github.com/serverless/serverless-optimizer-plugin) - Optimizes your code for performance in Lambda. Supports coffeeify, babelify and other transforms +- [CloudFormation Validator](https://github.com/tmilewski/serverless-resources-validation-plugin) - Adds support for validating your CloudFormation template. +- [Prune](https://github.com/Nopik/serverless-lambda-prune-plugin) - Delete old versions of AWS lambdas from your account so that you don't exceed the code storage limit. +- [Base-Path](https://github.com/daffinity/serverless-base-path-plugin) - Sets a base path for all API Gateway endpoints in a Component. +- [Test](https://github.com/arabold/serverless-test-plugin) - A Simple Integration Test Framework for Serverless. +- [SNS Subscribe](https://github.com/martinlindenberg/serverless-plugin-sns) - This plugin easily subscribes your lambda functions to SNS notifications. +- [JSHint](https://github.com/joostfarla/serverless-jshint-plugin) - Detect errors and potential problems in your Lambda functions. +- [ESLint](https://github.com/nishantjain91/serverless-eslint-plugin) - Detect errors and potential problems in your Lambda functions using eslint. +- [Mocha](https://github.com/SC5/serverless-mocha-plugin) - Enable test driven development by creating test cases when creating new functions +- [Function-Package](https://github.com/HyperBrain/serverless-package-plugin) - Package your lambdas without deploying to AWS. +- [Sentry](https://github.com/arabold/serverless-sentry-plugin) - Automatically send errors and exceptions to [Sentry](https://getsentry.com). +- [Auto-Prune](https://github.com/arabold/serverless-autoprune-plugin) - Delete old AWS Lambda versions. +- [Serverless Secrets](https://github.com/trek10inc/serverless-secrets) - Easily encrypt and decrypt secrets in your Serverless projects +- [Serverless DynamoDB Local](https://github.com/99xt/serverless-dynamodb-local) - Simulate DynamoDB instance locally. +- [Serverless Dependency Install](https://github.com/99xt/serverless-dependency-install) - Manage node, serverless dependencies easily within the project. +- [Serverless Header Function](https://github.com/blackevil245/serverless-header-function) - Automatically run a javascript script on every Serverless action hooks. diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f0c3cbf6..4d221d9e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,140 @@ +# 1.47.0 (2019-07-10) + +- [Add Onica as a Consultant](https://github.com/serverless/serverless/pull/6300) +- [Correct typo](https://github.com/serverless/serverless/pull/6301) +- [Adapt new ESLint and Prettier configuration](https://github.com/serverless/serverless/pull/6284) +- [Ensure deploy is triggered in CI](https://github.com/serverless/serverless/pull/6306) +- [Remove jsbeautify configuration](https://github.com/serverless/serverless/pull/6309) +- [Improve PR template](https://github.com/serverless/serverless/pull/6308) +- [Allow users to specify API Gateway Access Log format](https://github.com/serverless/serverless/pull/6299) +- [Fix service.provider.region resolution](https://github.com/serverless/serverless/pull/6317) +- [Add null as a consultant](https://github.com/serverless/serverless/pull/6323) +- [Update very minor typo in credentials.md](https://github.com/serverless/serverless/pull/6321) +- [Expose non-errors in informative way](https://github.com/serverless/serverless/pull/6318) +- [Fix async leaks detection conditional](https://github.com/serverless/serverless/pull/6319) +- [Typo fix in AWS ALB event documentation](https://github.com/serverless/serverless/pull/6325) +- [Websockets: fix passing log group ARN](https://github.com/serverless/serverless/pull/6310) +- [Specify invoke local option in the guide](https://github.com/serverless/serverless/pull/6327) +- [Update Webpack version and usage of aws-nodejs-ecma-script template](https://github.com/serverless/serverless/pull/6324) +- [Make ALB event target group names unique](https://github.com/serverless/serverless/pull/6322) +- [Improve Travis CI conf](https://github.com/serverless/serverless/pull/6330) +- [Support for Github Entreprise in sls create](https://github.com/serverless/serverless/pull/6332) +- [Merge patch 1.46.1 release artifacts back into master](https://github.com/serverless/serverless/pull/6343) +- [Add support for existing S3 buckets](https://github.com/serverless/serverless/pull/6290) +- [PLAT-1202 - Interactive `serverless` create](https://github.com/serverless/serverless/pull/6294) +- [PLAT-1091 - message in `npm i` output about the `serverless` quickstart command](https://github.com/serverless/serverless/pull/6238) + +## Meta + +- [Comparison since last release](https://github.com/serverless/serverless/compare/v1.46.1...v1.47.0) + +# 1.46.1 (2019-06-28) + +- [Fix service.provider.region resolution](https://github.com/serverless/serverless/pull/6317) +- [Ensure deploy is triggered in CI](https://github.com/serverless/serverless/pull/6306) + +## Meta + +- [Comparison since last release](https://github.com/serverless/serverless/compare/v1.46.0...v1.46.1) + +# 1.46.0 (2019-06-26) + +- [Fix formatting issue with Markdown link](https://github.com/serverless/serverless/pull/6228) +- [Update docs | dont use provider.tags with shared API Gateway](https://github.com/serverless/serverless/pull/6225) +- [Fix: Update azure template](https://github.com/serverless/serverless/pull/6258) +- [Improve user message](https://github.com/serverless/serverless/pull/6254) +- [Reference custom ApiGateway for models and request validators if conf…](https://github.com/serverless/serverless/pull/6231) +- [Ensure integration tests do not fail when run concurrently](https://github.com/serverless/serverless/pull/6256) +- [Improve integration test experience](https://github.com/serverless/serverless/pull/6253) +- [Fix lambda integration timeout response template](https://github.com/serverless/serverless/pull/6255) +- [Fix duplicate packaging issue](https://github.com/serverless/serverless/pull/6244) +- [Fix Travis configuration for branch/tag runs](https://github.com/serverless/serverless/pull/6265) +- [fixed a typo 🖊](https://github.com/serverless/serverless/pull/6275) +- [Fix #6267](https://github.com/serverless/serverless/pull/6268) +- [#6017 Allow to load plugin from path](https://github.com/serverless/serverless/pull/6261) +- [Added correction based on community feedback](https://github.com/serverless/serverless/pull/6286) +- [Remove package-lock.json and shrinkwrap scripts](https://github.com/serverless/serverless/pull/6280) +- [Remove README redundant link](https://github.com/serverless/serverless/pull/6288) +- [Remove default stage value in provider object](https://github.com/serverless/serverless/pull/6200) +- [Use naming to get stackName](https://github.com/serverless/serverless/pull/6285) +- [Fix typo in link to ALB docs](https://github.com/serverless/serverless/pull/6292) +- [Add ip, method, header and query conditions to ALB events](https://github.com/serverless/serverless/pull/6293) +- [Feature/support external websocket api](https://github.com/serverless/serverless/pull/6272) + +## Meta + +- [Comparison since last release](https://github.com/serverless/serverless/compare/v1.45.1...v1.46.0) + +# 1.45.1 (2019-06-12) + +- [Fix IAM policies setup for functions with custom name](https://github.com/serverless/serverless/pull/6240) +- [Fix Travis CI deploy config](https://github.com/serverless/serverless/pull/6234) + +## Meta + +- [Comparison since last release](https://github.com/serverless/serverless/compare/v1.45.0...v1.45.1) + +# 1.45.0 (2019-06-12) + +- [Add `--config` option](https://github.com/serverless/serverless/pull/6216) +- [Fix and improve ESlint config](https://github.com/serverless/serverless/pull/6188) +- [Tests: Fix mocha config](https://github.com/serverless/serverless/pull/6187) +- [Thorough integration testing](https://github.com/serverless/serverless/pull/6148) +- [Tests: Isolation improvements](https://github.com/serverless/serverless/pull/6186) +- [Add support for Websocket Logs](https://github.com/serverless/serverless/pull/6088) +- [Cleanup and improve Travis CI configuration](https://github.com/serverless/serverless/pull/6178) +- [Tests: Fix stub configuration](https://github.com/serverless/serverless/pull/6205) +- [Tests: Upgrade Sinon](https://github.com/serverless/serverless/pull/6206) +- [Add Application Load Balancer event source](https://github.com/serverless/serverless/pull/6073) +- [Do not run integration tests for PR's](https://github.com/serverless/serverless/pull/6207) +- [Adding a validation to validation.js script](https://github.com/serverless/serverless/pull/6192) +- [Tests: Upgrade dependencies, improve isolation and experience on Windows](https://github.com/serverless/serverless/pull/6208) +- [Add support for S3 hosted package artifacts](https://github.com/serverless/serverless/pull/6196) +- [Remove root README generator](https://github.com/serverless/serverless/pull/6215) +- [Myho/npm lint fix](https://github.com/serverless/serverless/pull/6217) +- [Use common prefix for log groups permissions at Lambdas' execution roles](https://github.com/serverless/serverless/pull/6212) +- [Update Scala version to 2.13.0 for aws-scala-sbt template](https://github.com/serverless/serverless/pull/6222) + +## Meta + +- [Comparison since last release](https://github.com/serverless/serverless/compare/v1.44.1...v1.45.0) + +# 1.44.1 (2019-05-28) + +- [Fix enterprise plugin lookup in global yarn installs](https://github.com/serverless/serverless/pull/6183) + +## Meta + +- [Comparison since last release](https://github.com/serverless/serverless/compare/v1.44.0...v1.44.1) + +# 1.44.0 (2019-05-28) + +- [Built in integration of Serverless Enterprise](https://github.com/serverless/serverless/pull/6074) +- [Setup Travis Windows support / Remove AppVeyor](https://github.com/serverless/serverless/pull/6132) +- [Update required Node.js version / Add version check](https://github.com/serverless/serverless/pull/6077) +- [Add scopes for cognito type APIGW referenced authorizer ](https://github.com/serverless/serverless/pull/6150) +- [Do not throw error if authorizer has empty claims](https://github.com/serverless/serverless/pull/6121) +- [Tests: Patch mocha bugs and fix broken async flow cases](https://github.com/serverless/serverless/pull/6157) +- [Fix tagging API Gateway stage fails if tag contains special characters like space](https://github.com/serverless/serverless/pull/6139) +- [Solve the problem of principal format in China region](https://github.com/serverless/serverless/pull/6127) +- [Upgrade mocha, switch from istanbul to nyc, improve tests configuration](https://github.com/serverless/serverless/pull/6169) + +## Meta + +- [Comparison since last release](https://github.com/serverless/serverless/compare/v1.43.0...v1.44.0) + +# 1.43.0 (2019-05-20) + +- [Update services.md](https://github.com/serverless/serverless/pull/6138) +- [Azure: exclude development dependency files when packaging functions](https://github.com/serverless/serverless/pull/6137) +- [Update release process docs and toolings](https://github.com/serverless/serverless/pull/6113) +- [Update AWS Node.js runtime to version 10](https://github.com/serverless/serverless/pull/6142) +- [Fix tests setup issues](https://github.com/serverless/serverless/pull/6147) + +## Meta + +- [Comparison since last release](https://github.com/serverless/serverless/compare/v1.42.3...v1.43.0) + # 1.42.3 (2019-05-14) - [Update deploy.md](https://github.com/serverless/serverless/pull/6110) @@ -11,7 +148,8 @@ - [Improve handling of custom API Gateway options](https://github.com/serverless/serverless/pull/6129) ## Meta - - [Comparison since last release](https://github.com/serverless/serverless/compare/v1.42.2...v1.42.3) + +- [Comparison since last release](https://github.com/serverless/serverless/compare/v1.42.2...v1.42.3) # 1.42.2 (2019-05-10) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 77ae89c1e..9d7afa9c9 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -14,21 +14,21 @@ orientation. Examples of behavior that contributes to creating a positive environment include: -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members Examples of unacceptable behavior by participants include: -* The use of sexualized language or imagery and unwelcome sexual attention or -advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic +- The use of sexualized language or imagery and unwelcome sexual attention or + advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a +- Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dacac1823..90d0863ae 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,7 +38,19 @@ You can do that by replying to [issues on Github](https://github.com/serverless/ # Code Style -We aim for clean, consistent code style. We're using ESlint to check for codestyle issues using the Airbnb preset (you can run `npm run lint` to lint your code). +We aim for clean, consistent code style. We're using ESlint to check for codestyle issues using the Airbnb preset. + +## Verifying linting style + +``` +npm run lint +``` + +## Fixing lint issues + +``` +npm run lint:fix +``` To help reduce the effort of creating contributions with this style, an [.editorconfig file](http://editorconfig.org/) is provided that your editor may use to override any conflicting global defaults and automate a subset of the style settings. @@ -52,11 +64,11 @@ During development, you can easily check coverage by running `npm test`, then op Please follow these Testing guidelines when writing your unit tests: -- Include a top-level `describe('ClassName')` block, with the name of the class you are testing -- Inside that top-level `describe()` block, create another `describe('#methodOne()')` block for each class method you might create or modify -- For each method, include an `it('should do something')` test case for each logical edge case in your changes -- As you write tests, check the code coverage and make sure all lines of code are covered. If not, just add more test cases until everything is covered -- For reference and inspiration, please check our `tests` directory +- Include a top-level `describe('ClassName')` block, with the name of the class you are testing +- Inside that top-level `describe()` block, create another `describe('#methodOne()')` block for each class method you might create or modify +- For each method, include an `it('should do something')` test case for each logical edge case in your changes +- As you write tests, check the code coverage and make sure all lines of code are covered. If not, just add more test cases until everything is covered +- For reference and inspiration, please check our `tests` directory ## Testing templates @@ -65,7 +77,7 @@ If you add a new template or want to test a template after changing it you can r To run all integration tests run: ``` -./tests/templates/test_all_templates +./tests/templates/test-all-templates ``` To run only a specific integration test run: @@ -80,7 +92,7 @@ so for example: tests/templates/integration-test-template aws-java-maven mvn package ``` -If you add a new template make sure to add it to the `test_all_templates` file and configure the `docker-compose.yml` file for your template. +If you add a new template make sure to add it to the `test-all-templates` file and configure the `docker-compose.yml` file for your template. # Our Code of Conduct diff --git a/README.md b/README.md index 3880d8e5d..962f8694a 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,9 @@ [Website](http://www.serverless.com) • [Docs](https://serverless.com/framework/docs/) • [Newsletter](https://serverless.com/subscribe/) • [Gitter](https://gitter.im/serverless/serverless) • [Forum](http://forum.serverless.com) • [Meetups](https://github.com/serverless/meetups) • [Twitter](https://twitter.com/goserverless) • [We're Hiring](https://serverless.com/company/jobs/) • [Enterprise](https://serverless.com/enterprise/) -**The Serverless Framework** – Build applications comprised of microservices that run in response to events, auto-scale for you, and only charge you when they run. This lowers the total cost of maintaining your apps, enabling you to build more logic, faster. +**The Serverless Framework** – Build applications comprised of microservices that run in response to events, auto-scale for you, and only charge you when they run. This lowers the total cost of maintaining your apps, enabling you to build more logic, faster. -The Framework uses new event-driven compute services, like AWS Lambda, Google Cloud Functions, and more. It's a command-line tool, providing scaffolding, workflow automation and best practices for developing and deploying your serverless architecture. It's also completely extensible via plugins. +The Framework uses new event-driven compute services, like AWS Lambda, Google Cloud Functions, and more. It's a command-line tool, providing scaffolding, workflow automation and best practices for developing and deploying your serverless architecture. It's also completely extensible via plugins. Serverless is an MIT open-source project, actively maintained by a full-time, venture-backed team. @@ -24,77 +24,91 @@ Serverless is an MIT open-source project, actively maintained by a full-time, ve -* [Quick Start](#quick-start) -* [Examples](https://github.com/serverless/examples) -* [Services](#services) -* [Features](#features) -* [Plugins](#v1-plugins) -* [Example Projects](#v1-projects) -* [Contributing](#contributing) -* [Community](#community) -* [Consultants](#consultants) -* [Licensing](#licensing) -* [Previous Version 0.5.x](#v.5) +- [Quick Start](#quick-start) +- [Examples](https://github.com/serverless/examples) +- [Services](#services) +- [Features](#features) +- [Plugins](https://github.com/serverless/plugins) +- [Contributing](#contributing) +- [Community](#community) +- [Consultants](#consultants) +- [Licensing](#licensing) +- [Previous Version 0.5.x](#v.5) ## Quick Start [Watch the video guide here](https://serverless.com/framework/) or follow the steps below to create and deploy your first serverless microservice in minutes. 1. **Install via npm:** - ```bash - npm install -g serverless - ``` + +```bash +npm install -g serverless +``` 2. **Set-up your [Provider Credentials](./docs/providers/aws/guide/credentials.md)**. [Watch the video on setting up credentials](https://www.youtube.com/watch?v=HSd9uYj2LJA) 3. **Create a Service:** - You can create a new service or [install existing services](#how-to-install-a-service). - ```bash - # Create a new Serverless Service/Project - serverless create --template aws-nodejs --path my-service - # Change into the newly created directory - cd my-service - ``` +You can create a new service or [install existing services](#how-to-install-a-service). + +```bash +# Create a new Serverless Service/Project +serverless create --template aws-nodejs --path my-service +# Change into the newly created directory +cd my-service +``` 4. **Deploy a Service:** - Use this when you have made changes to your Functions, Events or Resources in `serverless.yml` or you simply want to deploy all changes within your Service at the same time. - ```bash - serverless deploy -v - ``` +Use this when you have made changes to your Functions, Events or Resources in `serverless.yml` or you simply want to deploy all changes within your Service at the same time. + +```bash +serverless deploy -v +``` 5. **Deploy the Function:** - Use this to quickly upload and overwrite your AWS Lambda code on AWS, allowing you to develop faster. - ```bash - serverless deploy function -f hello - ``` +Use this to quickly upload and overwrite your AWS Lambda code on AWS, allowing you to develop faster. -6. **Invoke the Function:** +```bash +serverless deploy function -f hello +``` - Invokes an AWS Lambda Function on AWS and returns logs. - ```bash - serverless invoke -f hello -l - ``` +6. **Invoke the Function on AWS:** -7. **Fetch the Function Logs:** +Invokes an AWS Lambda Function on AWS and returns logs. - Open up a separate tab in your console and stream all logs for a specific Function using this command. - ```bash - serverless logs -f hello -t - ``` +```bash +serverless invoke -f hello -l +``` -8. **Remove the Service:** +7. **Invoke the Function on your machine:** - Removes all Functions, Events and Resources from your AWS account. - ```bash - serverless remove - ``` +Invokes an AWS Lambda Function on your local machine and returns logs. + +```bash +serverless invoke local -f hello -l +``` + +8. **Fetch the Function Logs:** + +Open up a separate tab in your console and stream all logs for a specific Function using this command. + +```bash +serverless logs -f hello -t +``` + +9. **Remove the Service:** + +Removes all Functions, Events and Resources from your AWS account. + +```bash +serverless remove +``` ### How to Install a Service: -This is a convenience method to install a pre-made Serverless Service locally by downloading the Github repo and unzipping it. Services are listed below. +This is a convenience method to install a pre-made Serverless Service locally by downloading the Github repo and unzipping it. Services are listed below. ```bash serverless install -u https://github.com/your-url-to-the-serverless-service @@ -106,365 +120,94 @@ Check out the [Serverless Framework Guide](./docs/providers/aws/guide/README.md) The following are services you can instantly install and use by running `serverless install --url ` -* [serverless-examples](https://github.com/serverless/examples) -* [CRUD](https://github.com/pmuens/serverless-crud) - CRUD service, [Scala Port](https://github.com/jahangirmohammed/serverless-crud-scala) -* [CRUD with FaunaDB](https://github.com/faunadb/serverless-crud) - CRUD service using FaunaDB -* [CRUD with S3](https://github.com/tscanlin/serverless-s3-crud) - CRUD service using S3 -* [GraphQL Boilerplate](https://github.com/serverless/serverless-graphql) - GraphQL application Boilerplate service -* [Authentication](https://github.com/laardee/serverless-authentication-boilerplate) - Authentication boilerplate service -* [Mailer](https://github.com/eahefnawy/serverless-mailer) - Service for sending emails -* [Kinesis streams](https://github.com/pmuens/serverless-kinesis-streams) - Service to showcase Kinesis stream support -* [DynamoDB streams](https://github.com/pmuens/serverless-dynamodb-streams) - Service to showcase DynamoDB stream support -* [Landingpage backend](https://github.com/pmuens/serverless-landingpage-backend) - Landingpage backend service to store E-Mail addresses -* [Facebook Messenger Chatbot](https://github.com/pmuens/serverless-facebook-messenger-bot) - Chatbot for the Facebook Messenger platform -* [Lambda chaining](https://github.com/pmuens/serverless-lambda-chaining) - Service which chains Lambdas through SNS -* [Secured API](https://github.com/pmuens/serverless-secured-api) - Service which exposes an API key accessible API -* [Authorizer](https://github.com/eahefnawy/serverless-authorizer) - Service that uses API Gateway custom authorizers -* [Thumbnails](https://github.com/eahefnawy/serverless-thumbnails) - Service that takes an image url and returns a 100x100 thumbnail -* [Boilerplate](https://github.com/eahefnawy/serverless-boilerplate) - Opinionated boilerplate -* [ES6 + Jest](https://github.com/americansystems/serverless-es6-jest) - ES6 + Jest Boilerplate -* [PHP](https://github.com/ZeroSharp/serverless-php) - Call a PHP function from your lambda -* [Ruby](https://github.com/stewartlord/serverless-ruby) - Call a Ruby function from your lambda -* [Slack App](https://github.com/johnagan/serverless-slack-app) - Slack App Boilerplate with OAuth and Bot actions -* [Swift](https://github.com/choefele/swift-lambda-app) - Full-featured project template to develop Lambda functions in Swift -* [Cloudwatch Alerts on Slack](https://github.com/dav009/serverless-aws-alarms-notifier) - Get AWS Cloudwatch alerts notifications on Slack +- [serverless-examples](https://github.com/serverless/examples) +- [CRUD](https://github.com/pmuens/serverless-crud) - CRUD service, [Scala Port](https://github.com/jahangirmohammed/serverless-crud-scala) +- [CRUD with FaunaDB](https://github.com/faunadb/serverless-crud) - CRUD service using FaunaDB +- [CRUD with S3](https://github.com/tscanlin/serverless-s3-crud) - CRUD service using S3 +- [GraphQL Boilerplate](https://github.com/serverless/serverless-graphql) - GraphQL application Boilerplate service +- [Authentication](https://github.com/laardee/serverless-authentication-boilerplate) - Authentication boilerplate service +- [Mailer](https://github.com/eahefnawy/serverless-mailer) - Service for sending emails +- [Kinesis streams](https://github.com/pmuens/serverless-kinesis-streams) - Service to showcase Kinesis stream support +- [DynamoDB streams](https://github.com/pmuens/serverless-dynamodb-streams) - Service to showcase DynamoDB stream support +- [Landingpage backend](https://github.com/pmuens/serverless-landingpage-backend) - Landingpage backend service to store E-Mail addresses +- [Facebook Messenger Chatbot](https://github.com/pmuens/serverless-facebook-messenger-bot) - Chatbot for the Facebook Messenger platform +- [Lambda chaining](https://github.com/pmuens/serverless-lambda-chaining) - Service which chains Lambdas through SNS +- [Secured API](https://github.com/pmuens/serverless-secured-api) - Service which exposes an API key accessible API +- [Authorizer](https://github.com/eahefnawy/serverless-authorizer) - Service that uses API Gateway custom authorizers +- [Thumbnails](https://github.com/eahefnawy/serverless-thumbnails) - Service that takes an image url and returns a 100x100 thumbnail +- [Boilerplate](https://github.com/eahefnawy/serverless-boilerplate) - Opinionated boilerplate +- [ES6 + Jest](https://github.com/americansystems/serverless-es6-jest) - ES6 + Jest Boilerplate +- [PHP](https://github.com/ZeroSharp/serverless-php) - Call a PHP function from your lambda +- [Ruby](https://github.com/stewartlord/serverless-ruby) - Call a Ruby function from your lambda +- [Slack App](https://github.com/johnagan/serverless-slack-app) - Slack App Boilerplate with OAuth and Bot actions +- [Swift](https://github.com/choefele/swift-lambda-app) - Full-featured project template to develop Lambda functions in Swift +- [Cloudwatch Alerts on Slack](https://github.com/dav009/serverless-aws-alarms-notifier) - Get AWS Cloudwatch alerts notifications on Slack **Note**: the `serverless install` command will only work on V1.0 or later. ## Features -* Supports Node.js, Python, Java, Go, C#, Ruby, Swift, Kotlin, PHP, Scala, & F# -* Manages the lifecycle of your serverless architecture (build, deploy, update, delete). -* Safely deploy functions, events and their required resources together via provider resource managers (e.g., AWS CloudFormation). -* Functions can be grouped ("serverless services") for easy management of code, resources & processes, across large projects & teams. -* Minimal configuration and scaffolding. -* Built-in support for multiple stages. -* Optimized for CI/CD workflows. -* Loaded with automation, optimization and best practices. -* 100% Extensible: Extend or modify the Framework and its operations via Plugins. -* An ecosystem of serverless services and plugins. -* A passionate and welcoming community! - -## Plugins (V1.0) - -Use these plugins to extend or overwrite the Framework's functionality. - -[Add a plugin to this list](https://github.com/serverless/community-plugins/blob/master/plugins.json) - - -| Plugin | Author | -| :----- | :----: | -| **[Fullstack Serverless](https://github.com/MadSkills-io/fullstack-serverless)**
A Serverless plugin to create an AWS CloudFront distribution that serves static web content from S3 and routes API traffic to API Gateway. | [MadSkills-io](http://github.com/MadSkills-io) | -| **[Go Serverless](https://github.com/thepauleh/goserverless)**
GoFormation for Serverless. Create serverless configs with Go Structs. | [thepauleh](http://github.com/thepauleh) | -| **[Raml Serverless](https://github.com/andrewcurioso/raml-serverless)**
Serverless plugin to work with RAML API spec documents | [andrewcurioso](http://github.com/andrewcurioso) | -| **[Serverless Alexa Plugin](https://github.com/rajington/serverless-alexa-plugin)**
Serverless plugin to support Alexa Lambda events | [rajington](http://github.com/rajington) | -| **[Serverless Alexa Skills](https://github.com/marcy-terui/serverless-alexa-skills)**
Manage your Alexa Skills with Serverless Framework. | [marcy-terui](http://github.com/marcy-terui) | -| **[Serverless Aliyun Function Compute](https://github.com/aliyun/serverless-aliyun-function-compute)**
Serverless Alibaba Cloud Function Compute Plugin | [aliyun](http://github.com/aliyun) | -| **[Serverless Api Cloudfront](https://github.com/Droplr/serverless-api-cloudfront)**
Plugin that adds CloudFront distribution in front of your API Gateway for custom domain, CDN caching and access log. | [Droplr](http://github.com/Droplr) | -| **[Serverless Api Stage](https://github.com/leftclickben/serverless-api-stage)**
Serverless API Stage plugin, enables stage variables and logging for AWS API Gateway. | [leftclickben](http://github.com/leftclickben) | -| **[Serverless Apib Validator](https://github.com/onlicar/serverless-apib-validator)**
Validate that an API Blueprint has full coverage over a Serverless config | [onlicar](http://github.com/onlicar) | -| **[Serverless Apig S 3](https://github.com/sdd/serverless-apig-s3)**
Serve static front-end content from S3 via the API Gateway and deploy client bundle to S3. | [sdd](http://github.com/sdd) | -| **[Serverless Apigateway Plugin](https://github.com/GFG/serverless-apigateway-plugin)**
Configure the AWS api gateway: Binary support, Headers and Body template mappings | [GFG](http://github.com/GFG) | -| **[Serverless Apigw Binary](https://github.com/maciejtreder/serverless-apigw-binary)**
Plugin to enable binary support in AWS API Gateway. | [maciejtreder](http://github.com/maciejtreder) | -| **[Serverless Apigwy Binary](https://github.com/ryanmurakami/serverless-apigwy-binary)**
Serverless plugin for configuring API Gateway to return binary responses | [ryanmurakami](http://github.com/ryanmurakami) | -| **[Serverless Appsync Plugin](https://github.com/sid88in/serverless-appsync-plugin)**
Serverless Plugin to deploy AppSync GraphQL API | [sid88in](http://github.com/sid88in) | -| **[Serverless Attach Managed Policy](https://github.com/Nordstrom/serverless-attach-managed-policy)**
A Serverless plugin to automatically attach an AWS Managed IAM Policy (or Policies) to all IAM Roles created by the Service. | [Nordstrom](http://github.com/Nordstrom) | -| **[Serverless Aws Alias](https://github.com/HyperBrain/serverless-aws-alias)**
This plugin enables use of AWS aliases on Lambda functions. | [HyperBrain](http://github.com/HyperBrain) | -| **[Serverless Aws Documentation](https://github.com/9cookies/serverless-aws-documentation)**
Serverless plugin to add documentation and models to the serverless generated API Gateway | [9cookies](http://github.com/9cookies) | -| **[Serverless Aws Nested Stacks](https://github.com/concon121/serverless-plugin-nested-stacks)**
Yet another AWS nested stack plugin! | [concon121](http://github.com/concon121) | -| **[Serverless Aws Resource Names](https://github.com/concon121/serverless-plugin-aws-resource-names)**
Serverless plugin to alter the default naming conventions for sls resources via a simple mapping file. | [concon121](http://github.com/concon121) | -| **[Serverless Basic Authentication](https://github.com/svdgraaf/serverless-basic-authentication)**
Serverless Plugin for adding Basic Authentication to your api | [svdgraaf](http://github.com/svdgraaf) | -| **[Serverless Build Client](https://github.com/tgfischer/serverless-build-client)**
Build your static website with environment variables defined in serverless.yml | [tgfischer](http://github.com/tgfischer) | -| **[Serverless Build Plugin](https://github.com/nfour/serverless-build-plugin)**
A Node.js focused build plugin for serverless. | [nfour](http://github.com/nfour) | -| **[Serverless Cf Vars](https://gitlab.com/kabo/serverless-cf-vars)**
Enables use of AWS pseudo functions and Fn::Sub string substitution | [kabo](http://github.com/kabo) | -| **[Serverless Cljs Plugin](https://github.com/nervous-systems/serverless-cljs-plugin)**
Enables Clojurescript as an implementation language for Lambda handlers | [nervous-systems](http://github.com/nervous-systems) | -| **[Serverless Cloudformation Changesets](https://github.com/trek10inc/serverless-cloudformation-changesets)**
Natively deploy to CloudFormation via Change sets, instead of directly. Allowing you to queue changes, and safely require escalated roles for final deployment. | [trek10inc](http://github.com/trek10inc) | -| **[Serverless Cloudformation Parameter Setter](https://github.com/trek10inc/serverless-cloudformation-parameter-setter)**
Set CloudFormation parameters when deploying. | [trek10inc](http://github.com/trek10inc) | -| **[Serverless Cloudformation Resource Counter](https://github.com/drexler/serverless-cloudformation-resource-counter)**
A plugin to count the resources generated in the AWS CloudFormation stack after deployment. | [drexler](http://github.com/drexler) | -| **[Serverless Cloudformation Sub Variables](https://github.com/santiagocardenas/serverless-cloudformation-sub-variables)**
Serverless framework plugin for easily supporting AWS CloudFormation Sub function variables | [santiagocardenas](http://github.com/santiagocardenas) | -| **[Serverless Coffeescript](https://github.com/duanefields/serverless-coffeescript)**
A Serverless plugin to compile your CoffeeScript into JavaScript at deployment | [duanefields](http://github.com/duanefields) | -| **[Serverless Cognito Add Custom Attributes](https://github.com/GetWala/serverless-cognito-add-custom-attributes)**
Serverless Plugin for adding custom attributes to an existing CloudFormation-managed CognitoUserPool and CognitoUserPoolClient without losing all your users | [GetWala](http://github.com/GetWala) | -| **[Serverless Command Line Event Args](https://github.com/horike37/serverless-command-line-event-args)**
This module is Serverless Framework plugin. Event JSON passes to your Lambda function in commandline. | [horike37](http://github.com/horike37) | -| **[Serverless Content Encoding](https://github.com/dong-dohai/serverless-content-encoding)**
Enable Content Encoding in AWS API Gateway during deployment | [dong-dohai](http://github.com/dong-dohai) | -| **[Serverless Create Global Dynamodb Table](https://github.com/rrahul963/serverless-create-global-dynamodb-table)**
Serverless plugin to create dynamodb global tables | [rrahul963](http://github.com/rrahul963) | -| **[Serverless Crypt](https://github.com/marcy-terui/serverless-crypt)**
Securing the secrets on Serverless Framework by AWS KMS encryption. | [marcy-terui](http://github.com/marcy-terui) | -| **[Serverless Custom Packaging Plugin](https://github.com/hypoport/serverless-custom-packaging-plugin)**
Plugin to package your sourcecode using a custom target path inside the zip. | [hypoport](http://github.com/hypoport) | -| **[Serverless Dependson Plugin](https://github.com/bwinant/serverless-dependson-plugin)**
Serverless plugin that automatically generates DependsOn references for AWS Lambdas to prevent AWS RequestLimitExceeded errors. | [bwinant](http://github.com/bwinant) | -| **[Serverless Ding](https://github.com/sidgonuts/serverless-ding)**
Serverless plugin to audibly alert user after deployment | [sidgonuts](http://github.com/sidgonuts) | -| **[Serverless Dir Config Plugin](https://github.com/economysizegeek/serverless-dir-config-plugin)**
EXPERIMENTAL - Serverless plugin to load function and resource definitions from a directory. | [economysizegeek](http://github.com/economysizegeek) | -| **[Serverless Domain Manager](https://github.com/amplify-education/serverless-domain-manager)**
Serverless plugin for managing custom domains with API Gateways. | [amplify-education](http://github.com/amplify-education) | -| **[Serverless Dotenv](https://github.com/Jimdo/serverless-dotenv)**
Fetch environment variables and write it to a .env file | [Jimdo](http://github.com/Jimdo) | -| **[Serverless Dotnet](https://github.com/fruffin/serverless-dotnet)**
A serverless plugin to run 'dotnet' commands as part of the deploy process | [fruffin](http://github.com/fruffin) | -| **[Serverless Dynalite](https://github.com/sdd/serverless-dynalite)**
Run dynalite locally (no JVM, all JS) to simulate DynamoDB. Watch serverless.yml for table config updates. | [sdd](http://github.com/sdd) | -| **[Serverless Dynamodb Autoscaling](https://github.com/sbstjn/serverless-dynamodb-autoscaling)**
Configure Amazon DynamoDB's native Auto Scaling for your table capacities. | [sbstjn](http://github.com/sbstjn) | -| **[Serverless Dynamodb Fixtures](https://github.com/chechu/serverless-dynamodb-fixtures)**
Serverless Dynamodb Fixtures - Allows to load data on DynamoDB tables | [chechu](http://github.com/chechu) | -| **[Serverless Dynamodb Local](https://github.com/99xt/serverless-dynamodb-local)**
Serverless Dynamodb Local Plugin - Allows to run dynamodb locally for serverless | [99xt](http://github.com/99xt) | -| **[Serverless Dynamodb Ttl](https://github.com/Jimdo/serverless-dynamodb-ttl)**
Configure DynamoDB TTL in serverless.yml (until CloudFormation supports this). | [Jimdo](http://github.com/Jimdo) | -| **[Serverless Enable Api Logs](https://github.com/paulSambolin/serverless-enable-api-logs)**
Enables Coudwatch logging for API Gateway events | [paulSambolin](http://github.com/paulSambolin) | -| **[Serverless Env Generator](https://github.com/DieProduktMacher/serverless-env-generator)**
Manage environment variables with YAML and load them with dotenv. Supports variable encryption with KMS, multiple stages and custom profiles. | [DieProduktMacher](http://github.com/DieProduktMacher) | -| **[Serverless Ephemeral](https://github.com/Accenture/serverless-ephemeral)**
Build and include custom stateless libraries for any language | [Accenture](http://github.com/Accenture) | -| **[Serverless Event Constant Inputs](https://github.com/dittto/serverless-event-constant-inputs)**
Allows you to add constant inputs to events in Serverless 1.0. For more info see [constant values in Cloudwatch](https://aws.amazon.com/blogs/compute/simply-serverless-use-constant-values-in-cloudwatch-event-triggered-lambda-functions/) | [dittto](http://github.com/dittto) | -| **[Serverless Export Env](https://github.com/arabold/serverless-export-env)**
Export environment variables into a .env file with automatic AWS CloudFormation reference resolution. | [arabold](http://github.com/arabold) | -| **[Serverless Express](https://github.com/mikestaub/serverless-express)**
Making express app development compatible with serverless framework. | [mikestaub](http://github.com/mikestaub) | -| **[Serverless Finch](https://github.com/fernando-mc/serverless-finch)**
A Serverless plugin to deploy static website assets to AWS S3. | [fernando-mc](http://github.com/fernando-mc) | -| **[Serverless Functions Base Path](https://github.com/kevinrambaud/serverless-functions-base-path)**
Easily define a base path where your serverless functions are located. | [kevinrambaud](http://github.com/kevinrambaud) | -| **[Serverless Go Build](https://github.com/sean9keenan/serverless-go-build)**
Build go source files (or public functions) using yml definition file | [sean9keenan](http://github.com/sean9keenan) | -| **[Serverless Gulp](https://github.com/rhythminme/serverless-gulp)**
A thin task wrapper around @goserverless that allows you to automate build, test and deploy tasks using gulp | [rhythminme](http://github.com/rhythminme) | -| **[Serverless Haskell](https://github.com/seek-oss/serverless-haskell)**
Deploying Haskell applications to AWS Lambda with Serverless | [seek-oss](http://github.com/seek-oss) | -| **[Serverless Hooks Plugin](https://github.com/uswitch/serverless-hooks-plugin)**
Run arbitrary commands on any lifecycle event in serverless | [uswitch](http://github.com/uswitch) | -| **[Serverless Iam Roles Per Function](https://github.com/functionalone/serverless-iam-roles-per-function)**
Serverless Plugin for easily defining IAM roles per function via the use of iamRoleStatements at the function level. | [functionalone](http://github.com/functionalone) | -| **[Serverless Ignore](https://github.com/nya1/serverless-ignore)**
Serverless plugin to ignore files (.slsignore) | [nya1](http://github.com/nya1) | -| **[Serverless Iot Local](https://github.com/tradle/serverless-iot-local)**
AWS Iot events with serverless-offline | [tradle](http://github.com/tradle) | -| **[Serverless Jest Plugin](https://github.com/SC5/serverless-jest-plugin)**
A Serverless Plugin for the Serverless Framework which adds support for test-driven development using Jest | [SC5](http://github.com/SC5) | -| **[Serverless Kms Secrets](https://github.com/SC5/serverless-kms-secrets)**
Allows to easily encrypt and decrypt secrets using KMS from the serverless CLI | [SC5](http://github.com/SC5) | -| **[Serverless Kubeless](https://github.com/serverless/serverless-kubeless)**
Serverless plugin for deploying functions to Kubeless. | [serverless](http://github.com/serverless) | -| **[Serverless Local Dev Server](https://github.com/DieProduktMacher/serverless-local-dev-server)**
Speeds up development of Alexa Skills, Chatbots and APIs by exposing your functions as local HTTP endpoints and mapping received events. | [DieProduktMacher](http://github.com/DieProduktMacher) | -| **[Serverless Local Environment](https://github.com/piercus/serverless-local-environment)**
Serverless plugin to set local environment variables and remote environment variable to different values | [piercus](http://github.com/piercus) | -| **[Serverless Local Schedule](https://github.com/UnitedIncome/serverless-local-schedule)**
Schedule AWS CloudWatch Event based invocations in local time(with DST support!) | [UnitedIncome](http://github.com/UnitedIncome) | -| **[Serverless Log Forwarding](https://github.com/amplify-education/serverless-log-forwarding)**
Serverless plugin for forwarding CloudWatch logs to another Lambda function. | [amplify-education](http://github.com/amplify-education) | -| **[Serverless Micro](https://github.com/barstoolsports/serverless-micro)**
Plugin to help manage multiple micro services under one main service. | [barstoolsports](http://github.com/barstoolsports) | -| **[Serverless Mocha Plugin](https://github.com/SC5/serverless-mocha-plugin)**
A Serverless Plugin for the Serverless Framework which adds support for test-driven development using Mocha | [SC5](http://github.com/SC5) | -| **[Serverless Multi Dotnet](https://github.com/tsibelman/serverless-multi-dotnet)**
A serverless plugin to pack C# lambdas functions that are spread to multiple CS projects. | [tsibelman](http://github.com/tsibelman) | -| **[Serverless Nested Stack](https://github.com/jagdish-176/serverless-nested-stack)**
A plugin to Workaround for Cloudformation 200 resource limit | [jagdish-176](http://github.com/jagdish-176) | -| **[Serverless Offline](https://github.com/dherault/serverless-offline)**
Emulate AWS λ and API Gateway locally when developing your Serverless project | [dherault](http://github.com/dherault) | -| **[Serverless Offline Direct Lambda](https://github.com/civicteam/serverless-offline-direct-lambda)**
Allow offline direct lambda-to-lambda interactions by exposing lambdas with no API Gateway event via HTTP. | [civicteam](http://github.com/civicteam) | -| **[Serverless Offline Scheduler](https://github.com/ajmath/serverless-offline-scheduler)**
Runs scheduled functions offline while integrating with serverless-offline | [ajmath](http://github.com/ajmath) | -| **[Serverless Offline Sns](https://github.com/mj1618/serverless-offline-sns)**
Serverless plugin to run a local SNS server and call serverless SNS handlers with events notifications. | [mj1618](http://github.com/mj1618) | -| **[Serverless Offline Ssm](https://github.com/janders223/serverless-offline-ssm)**
Read SSM parameters from a .env file instead of AWS | [janders223](http://github.com/janders223) | -| **[Serverless Package Common](https://github.com/onlicar/serverless-package-common)**
Deploy microservice Python Serverless services with common code | [onlicar](http://github.com/onlicar) | -| **[Serverless Package Python Functions](https://github.com/ubaniabalogun/serverless-package-python-functions)**
Packaging Python Lambda functions with only the dependencies/requirements they need. | [ubaniabalogun](http://github.com/ubaniabalogun) | -| **[Serverless Parameters](https://github.com/svdgraaf/serverless-parameters)**
Add parameters to the generated cloudformation templates | [svdgraaf](http://github.com/svdgraaf) | -| **[Serverless Plugin Aws Alerts](https://github.com/ACloudGuru/serverless-plugin-aws-alerts)**
A Serverless plugin to easily add CloudWatch alarms to functions | [ACloudGuru](http://github.com/ACloudGuru) | -| **[Serverless Plugin Aws Resolvers](https://github.com/DopplerLabs/serverless-plugin-aws-resolvers)**
Resolves variables from ESS, RDS, or Kinesis for serverless services | [DopplerLabs](http://github.com/DopplerLabs) | -| **[Serverless Plugin Bespoken](https://github.com/bespoken/serverless-plugin-bespoken)**
Creates a local server and a proxy so you don't have to deploy anytime you want to test your code | [bespoken](http://github.com/bespoken) | -| **[Serverless Plugin Bind Deployment Id](https://github.com/jacob-meacham/serverless-plugin-bind-deployment-id)**
A Serverless plugin to bind the randomly generated deployment resource to your custom resources | [jacob-meacham](http://github.com/jacob-meacham) | -| **[Serverless Plugin Browserifier](https://github.com/digitalmaas/serverless-plugin-browserifier)**
Reduce the size and speed up your Node.js based lambda's using browserify. | [digitalmaas](http://github.com/digitalmaas) | -| **[Serverless Plugin Browserify](https://github.com/doapp-ryanp/serverless-plugin-browserify)**
Speed up your node based lambda's | [doapp-ryanp](http://github.com/doapp-ryanp) | -| **[Serverless Plugin Canary Deployments](https://github.com/davidgf/serverless-plugin-canary-deployments)**
A Serverless plugin to implement canary deployments of Lambda functions | [davidgf](http://github.com/davidgf) | -| **[Serverless Plugin Cfauthorizer](https://github.com/SC5/serverless-plugin-cfauthorizer)**
This plugin allows you to define your own API Gateway Authorizers as the Serverless CloudFormation resources and apply them to HTTP endpoints. | [SC5](http://github.com/SC5) | -| **[Serverless Plugin Chrome](https://github.com/adieuadieu/serverless-chrome/tree/master/packages/serverless-plugin)**
Plugin which bundles and ensures that Headless Chrome/Chromium is running when your AWS Lambda function handler is invoked. | [adieuadieu](http://github.com/adieuadieu) | -| **[Serverless Plugin Cloudwatch Sumologic](https://github.com/ACloudGuru/serverless-plugin-cloudwatch-sumologic)**
Plugin which auto-subscribes a log delivery lambda function to lambda log groups created by serverless | [ACloudGuru](http://github.com/ACloudGuru) | -| **[Serverless Plugin Colocate](https://github.com/aronim/serverless-plugin-colocate)**
Serverless Plugin to keep your configuration next to your code. | [aronim](http://github.com/aronim) | -| **[Serverless Plugin Common Excludes](https://github.com/dougmoscrop/serverless-plugin-common-excludes)**
Adds commonly excluded files to package.excludes | [dougmoscrop](http://github.com/dougmoscrop) | -| **[Serverless Plugin Custom Domain](https://github.com/dougmoscrop/serverless-plugin-custom-domain)**
Reliably sets a BasePathMapping to an API Gateway Custom Domain | [dougmoscrop](http://github.com/dougmoscrop) | -| **[Serverless Plugin Deploy Environment](https://github.com/DopplerLabs/serverless-plugin-deploy-environment)**
Plugin to manage deployment environment across stages | [DopplerLabs](http://github.com/DopplerLabs) | -| **[Serverless Plugin Diff](https://github.com/nicka/serverless-plugin-diff)**
Compares your local AWS CloudFormation templates against deployed ones. | [nicka](http://github.com/nicka) | -| **[Serverless Plugin Dynamodb Autoscaling](https://github.com/medikoo/serverless-plugin-dynamodb-autoscaling)**
Auto generate auto scaling configuration for configured DynamoDB tables | [medikoo](http://github.com/medikoo) | -| **[Serverless Plugin Elastic Beanstalk](https://github.com/rawphp/serverless-plugin-elastic-beanstalk)**
A serverless plugin to deploy applications to AWS ElasticBeanstalk. | [rawphp](http://github.com/rawphp) | -| **[Serverless Plugin Embedded Env In Code](https://github.com/zaru/serverless-plugin-embedded-env-in-code)**
Replace environment variables with static strings before deployment. It’s for Lambda@Edge. | [zaru](http://github.com/zaru) | -| **[Serverless Plugin Encode Env Var Objects](https://github.com/yonomi/serverless-plugin-encode-env-var-objects)**
Serverless plugin to encode any environment variable objects. | [yonomi](http://github.com/yonomi) | -| **[Serverless Plugin External Sns Events](https://github.com/silvermine/serverless-plugin-external-sns-events)**
Add ability for functions to use existing or external SNS topics as an event source | [silvermine](http://github.com/silvermine) | -| **[Serverless Plugin Fastdeploy](https://github.com/aronim/serverless-plugin-fastdeploy)**
Serverless Plugin to perform fast deployments for large service packages. | [aronim](http://github.com/aronim) | -| **[Serverless Plugin Git Variables](https://github.com/jacob-meacham/serverless-plugin-git-variables)**
A Serverless plugin to expose git variables (branch name, HEAD description, full commit hash) to your serverless services | [jacob-meacham](http://github.com/jacob-meacham) | -| **[Serverless Plugin Graphiql](https://github.com/bencooling/serverless-plugin-graphiql)**
A Serverless plugin to run a local http server for graphiql and your graphql handler | [bencooling](http://github.com/bencooling) | -| **[Serverless Plugin Ifelse](https://github.com/anantab/serverless-plugin-ifelse)**
A Serverless Plugin to write If Else conditions in serverless YAML file | [anantab](http://github.com/anantab) | -| **[Serverless Plugin Include Dependencies](https://github.com/dougmoscrop/serverless-plugin-include-dependencies)**
This is a Serverless plugin that should make your deployed functions smaller. | [dougmoscrop](http://github.com/dougmoscrop) | -| **[Serverless Plugin Inject Dependencies](https://github.com/loanmarket/serverless-plugin-inject-dependencies)**
Painlessly deploy serverless projects with only the required dependencies. | [loanmarket](http://github.com/loanmarket) | -| **[Serverless Plugin Iopipe](https://github.com/iopipe/serverless-plugin-iopipe)**
See inside your Lambda functions with high fidelity metrics and monitoring. | [iopipe](http://github.com/iopipe) | -| **[Serverless Plugin Lambda Dead Letter](https://github.com/gmetzker/serverless-plugin-lambda-dead-letter)**
A Serverless plugin that can configure a lambda with a dead letter queue or topic | [gmetzker](http://github.com/gmetzker) | -| **[Serverless Plugin Log Subscription](https://github.com/dougmoscrop/serverless-plugin-log-subscription)**
Adds a CloudWatch LogSubscription for functions | [dougmoscrop](http://github.com/dougmoscrop) | -| **[Serverless Plugin Metric](https://github.com/alex20465/serverless-plugin-metric)**
Creates dynamically AWS metric-filter resources with custom patterns | [alex20465](http://github.com/alex20465) | -| **[Serverless Plugin Multiple Responses](https://github.com/silvermine/serverless-plugin-multiple-responses)**
Enable multiple content-types for Response template | [silvermine](http://github.com/silvermine) | -| **[Serverless Plugin Node Shim](https://github.com/jzimmek/serverless-plugin-node-shim)**
Serverless plugin to run your functions in nodejs version (8 LTS, 9+) on aws lambda | [jzimmek](http://github.com/jzimmek) | -| **[Serverless Plugin Offline Dynamodb Stream](https://github.com/orchestrated-io/serverless-plugin-offline-dynamodb-stream)**
Serverless Plugin for emulating dynamodb stream triggering lambda functions offline | [orchestrated-io](http://github.com/orchestrated-io) | -| **[Serverless Plugin Offline Kinesis Events](https://github.com/DopplerLabs/serverless-plugin-offline-kinesis-events)**
Plugin that works with serverless-offline to allow offline testing of serverless functions that are triggered by Kinesis events. | [DopplerLabs](http://github.com/DopplerLabs) | -| **[Serverless Plugin Offline Kinesis](https://github.com/godu/serverless/tree/master/packages/serverless-offline-kinesis)**
ServerlessOffline's plugin which listens Kinesis stream and invokes locally your handlers | [godu](http://github.com/godu) | -| **[Serverless Plugin Offline DynamoDBStreams](https://github.com/godu/serverless/tree/master/packages/serverless-offline-dynamodb-streams)**
ServerlessOffline's plugin which listens DynamoDBStreams stream and invokes locally your handlers | [godu](http://github.com/godu) | -| **[Serverless Plugin Offline SQS](https://github.com/godu/serverless/tree/master/packages/serverless-offline-sqs)**
ServerlessOffline's plugin which listens SQS queue and invokes locally your handlers | [godu](http://github.com/godu) | -| **[Serverless Plugin Optimize](https://github.com/FidelLimited/serverless-plugin-optimize)**
Bundle with Browserify, transpile with Babel to ES5 and minify with Uglify your Serverless functions. | [FidelLimited](http://github.com/FidelLimited) | -| **[Serverless Plugin Package Dotenv File](https://github.com/ACloudGuru/serverless-plugin-package-dotenv-file)**
A Serverless plugin to copy a .env file into the serverless package | [ACloudGuru](http://github.com/ACloudGuru) | -| **[Serverless Plugin Parent](https://github.com/aronim/serverless-plugin-parent)**
Serverless Plugin that allows you to keep common configuration in a parent serverless.yml | [aronim](http://github.com/aronim) | -| **[Serverless Plugin Provider Groups](https://github.com/loanmarket/serverless-plugin-provider-groups)**
A plugin to allow management of grouped settings within large serverless projects. | [loanmarket](http://github.com/loanmarket) | -| **[Serverless Plugin Reducer](https://github.com/medikoo/serverless-plugin-reducer)**
Reduce Node.js lambda package so it contains only lambda dependencies | [medikoo](http://github.com/medikoo) | -| **[Serverless Plugin Registry](https://github.com/aronim/serverless-plugin-registry)**
Serverless plugin to register function names with AWS SSM Parameter Store. | [aronim](http://github.com/aronim) | -| **[Serverless Plugin Scripts](https://github.com/mvila/serverless-plugin-scripts)**
Add scripting capabilities to the Serverless Framework | [mvila](http://github.com/mvila) | -| **[Serverless Plugin Select](https://github.com/FidelLimited/serverless-plugin-select)**
Select which functions are to be deployed based on region and stage. | [FidelLimited](http://github.com/FidelLimited) | -| **[Serverless Plugin Simulate](https://github.com/gertjvr/serverless-plugin-simulate)**
Simulate AWS Lambda and API Gateway locally using Docker | [gertjvr](http://github.com/gertjvr) | -| **[Serverless Plugin Split Stacks](https://github.com/dougmoscrop/serverless-plugin-split-stacks)**
Migrate certain resources to nested stacks | [dougmoscrop](http://github.com/dougmoscrop) | -| **[Serverless Plugin Stack Config](https://github.com/rawphp/serverless-plugin-stack-config)**
A serverless plugin to manage configurations for a stack across micro-services. | [rawphp](http://github.com/rawphp) | -| **[Serverless Plugin Stack Outputs](https://github.com/svdgraaf/serverless-plugin-stack-outputs)**
Displays stack outputs for your serverless stacks when `sls info` is ran | [svdgraaf](http://github.com/svdgraaf) | -| **[Serverless Plugin Stackstorm](https://github.com/StackStorm/serverless-plugin-stackstorm)**
Reusable Functions from StackStorm Exchange | [StackStorm](http://github.com/StackStorm) | -| **[Serverless Plugin Stage Variables](https://github.com/svdgraaf/serverless-plugin-stage-variables)**
Add stage variables for Serverless 1.x to ApiGateway, so you can use variables in your Lambda's | [svdgraaf](http://github.com/svdgraaf) | -| **[Serverless Plugin Subscription Filter](https://github.com/tsub/serverless-plugin-subscription-filter)**
A serverless plugin to register AWS CloudWatchLogs subscription filter | [tsub](http://github.com/tsub) | -| **[Serverless Plugin Tracer](https://github.com/enykeev/serverless-plugin-tracer/)**
Trace serverless hooks as they execute | [enykeev](http://github.com/enykeev) | -| **[Serverless Plugin Transpiler](https://github.com/medikoo/serverless-plugin-transpiler)**
Transpile lambda files during packaging step | [medikoo](http://github.com/medikoo) | -| **[Serverless Plugin Typescript](https://github.com/graphcool/serverless-plugin-typescript)**
Serverless plugin for zero-config Typescript support. | [graphcool](http://github.com/graphcool) | -| **[Serverless Plugin Vpc Eni Cleanup](https://github.com/medikoo/serverless-plugin-vpc-eni-cleanup)**
Automatic cleanup of VPC network interfaces on stage removal | [medikoo](http://github.com/medikoo) | -| **[Serverless Plugin Warmup](https://github.com/FidelLimited/serverless-plugin-warmup)**
Keep your lambdas warm during Winter. | [FidelLimited](http://github.com/FidelLimited) | -| **[Serverless Plugin Webpack](https://github.com/goldwasserexchange/serverless-plugin-webpack)**
A serverless plugin to automatically bundle your functions individually with webpack | [goldwasserexchange](http://github.com/goldwasserexchange) | -| **[Serverless Plugin Write Env Vars](https://github.com/silvermine/serverless-plugin-write-env-vars)**
Write environment variables out to a file that is compatible with dotenv | [silvermine](http://github.com/silvermine) | -| **[Serverless Prune Plugin](https://github.com/claygregory/serverless-prune-plugin)**
Deletes old versions of functions from AWS, preserving recent and aliased versions | [claygregory](http://github.com/claygregory) | -| **[Serverless Pseudo Parameters](https://github.com/svdgraaf/serverless-pseudo-parameters)**
Use ${AWS::AccountId} and other cloudformation pseudo parameters in your serverless.yml values | [svdgraaf](http://github.com/svdgraaf) | -| **[Serverless Puresec Cli](https://github.com/puresec/serverless-puresec-cli)**
Serverless Plugin for magically creating IAM roles that are least privileged per function. | [puresec](http://github.com/puresec) | -| **[Serverless Python Individually](https://github.com/cfchou/serverless-python-individually)**
A serverless framework plugin to install multiple lambda functions written in python | [cfchou](http://github.com/cfchou) | -| **[Serverless Python Requirements](https://github.com/UnitedIncome/serverless-python-requirements)**
Serverless plugin to bundle Python packages | [UnitedIncome](http://github.com/UnitedIncome) | -| **[Serverless Reqvalidator Plugin](https://github.com/RafPe/serverless-reqvalidator-plugin)**
Serverless plugin to add request validator to API Gateway methods | [RafPe](http://github.com/RafPe) | -| **[Serverless Resources Env](https://github.com/rurri/serverless-resources-env)**
After Deploy, this plugin fetches cloudformation resource identifiers and sets them on AWS lambdas, and creates local .-env file | [rurri](http://github.com/rurri) | -| **[Serverless Run Function Plugin](https://github.com/lithin/serverless-run-function-plugin)**
Run serverless function locally | [lithin](http://github.com/lithin) | -| **[Serverless Rust](https://github.com/softprops/serverless-rust)**
Deploy Rustlang applications to AWS Lambda | [softprops](http://github.com/softprops) | -| **[Serverless S 3 Encryption](https://github.com/tradle/serverless-s3-encryption)**
Set or remove the encryption settings on your s3 buckets | [tradle](http://github.com/tradle) | -| **[Serverless S 3 Remover](https://github.com/sinofseven/serverless-s3-remover)**
A serverless plugin to make s3 buckets empty before deleting cloudformation stack when ```sls remove``` | [sinofseven](http://github.com/sinofseven) | -| **[Serverless S 3 Sync](https://github.com/k1LoW/serverless-s3-sync)**
A plugin to sync local directories and S3 prefixes for Serverless Framework, | [k1LoW](http://github.com/k1LoW) | -| **[Serverless S 3 Bucket Sync](https://github.com/sbstjn/serverless-s3bucket-sync)**
Sync a local folder with a S3 bucket after sls deploy | [sbstjn](http://github.com/sbstjn) | -| **[Serverless Sam](https://github.com/SAPessi/serverless-sam)**
Exports an AWS SAM template for a service created with the Serverless Framework. | [SAPessi](http://github.com/SAPessi) | -| **[Serverless Scriptable Plugin](https://github.com/weixu365/serverless-scriptable-plugin)**
Customize Serverless behavior without writing a plugin. | [weixu365](http://github.com/weixu365) | -| **[Serverless Sentry](https://github.com/arabold/serverless-sentry-plugin)**
Automatic monitoring of memory usage, execution timeouts and forwarding of Lambda errors to Sentry (https://sentry.io). | [arabold](http://github.com/arabold) | -| **[Serverless Shell](https://github.com/UnitedIncome/serverless-shell)**
Drop to a runtime shell with all the environment variables set that you'd have in lambda. | [UnitedIncome](http://github.com/UnitedIncome) | -| **[Serverless Sns Filter](https://github.com/MechanicalRock/serverless-sns-filter)**
Serverless plugin to add SNS Subscription Filters to events | [MechanicalRock](http://github.com/MechanicalRock) | -| **[Serverless Spa](https://github.com/gilmarsquinelato/serverless-spa)**
Serverless plugin to deploy your website to AWS S3 using Webpack to bundle it. | [gilmarsquinelato](http://github.com/gilmarsquinelato) | -| **[Serverless Sqs Alarms Plugin](https://github.com/sbstjn/serverless-sqs-alarms-plugin)**
Wrapper to setup CloudWatch Alarms on SQS queue length | [sbstjn](http://github.com/sbstjn) | -| **[Serverless Sqs Fifo](https://github.com/vortarian/serverless-sqs-fifo)**
A serverless plugin to handle creation of sqs fifo queue's in aws (stop-gap) | [vortarian](http://github.com/vortarian) | -| **[Serverless Ssm Fetch](https://github.com/gozup/serverless-ssm-fetch)**
Sets "AWS Systems Manager Parameter Store (SSM)" parameters into functions' environment variables. | [gozup](http://github.com/gozup) | -| **[Serverless Stack Output](https://github.com/sbstjn/serverless-stack-output)**
Store output from your AWS CloudFormation Stack in JSON/YAML/TOML files, or to pass it to a JavaScript function for further processing. | [sbstjn](http://github.com/sbstjn) | -| **[Serverless Stage Manager](https://github.com/jeremydaly/serverless-stage-manager)**
Super simple Serverless plugin for validating stage names before deployment | [jeremydaly](http://github.com/jeremydaly) | -| **[Serverless Static](https://github.com/iliasbhal/serverless-static)**
Easily serve files from a folder while developing on localhost with the serverless-offline plugin | [iliasbhal](http://github.com/iliasbhal) | -| **[Serverless Step Functions](https://github.com/horike37/serverless-step-functions)**
AWS Step Functions with Serverless Framework. | [horike37](http://github.com/horike37) | -| **[Serverless Sthree Env](https://github.com/StyleTributeIT/serverless-sthree-env)**
Serverless plugin to get config from a json formatted file in S3 and copy them to environment variable | [StyleTributeIT](http://github.com/StyleTributeIT) | -| **[Serverless Subscription Filter](https://github.com/blackevil245/serverless-subscription-filter)**
Serverless plugin to register subscription filter for Lambda logs. Register and pipe the logs of one lambda to another to process. | [blackevil245](http://github.com/blackevil245) | -| **[Serverless Tag Api Gateway](https://github.com/gfragoso/serverless-tag-api-gateway)**
Serverless plugin to tag API Gateway | [gfragoso](http://github.com/gfragoso) | -| **[Serverless Tag Cloud Watch Logs](https://github.com/gfragoso/serverless-tag-cloud-watch-logs)**
Serverless plugin to tag CloudWatchLogs | [gfragoso](http://github.com/gfragoso) | -| **[Serverless Tag Sqs](https://github.com/gfragoso/serverless-tag-sqs)**
Serverless plugin to tag SQS - Simple Queue Service | [gfragoso](http://github.com/gfragoso) | -| **[Serverless Vpc Discovery](https://github.com/amplify-education/serverless-vpc-discovery)**
Serverless plugin for discovering VPC / Subnet / Security Group configuration by name. | [amplify-education](http://github.com/amplify-education) | -| **[Serverless Webpack](https://github.com/serverless-heaven/serverless-webpack)**
Serverless plugin to bundle your lambdas with Webpack | [serverless-heaven](http://github.com/serverless-heaven) | -| **[Serverless Wsgi](https://github.com/logandk/serverless-wsgi)**
Serverless plugin to deploy WSGI applications (Flask/Django/Pyramid etc.) and bundle Python packages | [logandk](http://github.com/logandk) | - - -## Example Projects (V1.0) - - -| Project Name | Author | -|:-------------|:------:| -| **[Serverless Pipeline](https://github.com/msfidelis/serverless-pipeline)**
Simple CI/CD Pipeline to Serverless Projects on AWS using Codepipeline, Codebuild and Terraform | [msfidelis](http://github.com/msfidelis) | -| **[Jwtauthorizr](https://github.com/serverlessbuch/jwtAuthorizr)**
Custom JWT Authorizer Lambda function for Amazon API Gateway with Bearer JWT | [serverlessbuch](http://github.com/serverlessbuch) | -| **[AWS Demo Java Spring Cloud Function Serverless](https://github.com/mbsambangi/aws-java-spring-cloud-function-demo)**
If Java is your choice of programming language-Spring Cloud Function,Serverless Framework makes a great technology stack. It boosts developer productivity by decoupling from Vendor specific FaaS API, and deployment activities. | [mbsambangi](http://github.com/mbsambangi) | -| **[Serverless Architecture Boilerplate](https://github.com/msfidelis/serverless-architecture-boilerplate)**
Boilerplate to organize and deploy big projects using Serverless and CloudFormation on AWS | [msfidelis](http://github.com/msfidelis) | -| **[Bablebot](https://github.com/abiglobalhealth/babelbot)**
Lambda + API Gateway: Zero-to-chatbot in <10 lines of JS. Built-in integrations for Messenger, Telegram, Kik, Line, Twilio, Skype, and Wechat. Or roll your own! | [abiglobalhealth](http://github.com/abiglobalhealth) | -| **[Jwt Authorizr](https://github.com/serverlessbuch/jwtAuthorizr)**
Custom JWT Authorizer Lambda function for Amazon API Gateway with Bearer JWT | [serverlessbuch](http://github.com/serverlessbuch) | -| **[Slack Signup Serverless](https://github.com/dzimine/slack-signup-serverless)**
Serverless signup to Slack and more. Lambda with Python, StepFunctions, and Web front end. Python boilerplate included. | [dzimine](http://github.com/dzimine) | -| **[Serverless Graphql Api](https://github.com/boazdejong/serverless-graphql-api)**
Serverless GraphQL API using Lambda and DynamoDB | [boazdejong](http://github.com/boazdejong) | -| **[Serverless Screenshot](https://github.com/svdgraaf/serverless-screenshot)**
Serverless Screenshot Service using PhantomJS | [svdgraaf](http://github.com/svdgraaf) | -| **[Serverless Postgraphql](https://github.com/rentrop/serverless-postgraphql)**
GraphQL endpoint for PostgreSQL using postgraphql | [rentrop](http://github.com/rentrop) | -| **[Serverless Messenger Boilerplate](https://github.com/SC5/serverless-messenger-boilerplate)**
Serverless messenger bot boilerplate | [SC5](http://github.com/SC5) | -| **[Serverless Npm Registry](https://github.com/craftship/yith)**
Serverless private npm registry, proxy and cache. | [craftship](http://github.com/craftship) | -| **[Serverless Pokego](https://github.com/jch254/pokego-serverless)**
Serverless-powered API to fetch nearby Pokemon Go data | [jch254](http://github.com/jch254) | -| **[Serverless Weekly 2 Pocket App](https://github.com/s0enke/weekly2pocket)**
Serverless-powered API for sending posts to pocket app | [s0enke](http://github.com/s0enke) | -| **[Serverless Facebook Quotebot](https://github.com/pmuens/quotebot)**
100% Serverless Facebook messenger chatbot which will respond with inspiring quotes | [pmuens](http://github.com/pmuens) | -| **[Serverless Slack Trevorbot](https://github.com/conveyal/trevorbot)**
Slack bot for info on where in the world is Trevor Gerhardt? | [conveyal](http://github.com/conveyal) | -| **[Serverless Garden Aid](https://github.com/garden-aid/web-bff)**
IoT Garden Aid Backend | [garden-aid](http://github.com/garden-aid) | -| **[Serverless React Boilerplate](https://github.com/99xt/serverless-react-boilerplate)**
A serverless react boilerplate for offline development | [99xt](http://github.com/99xt) | -| **[Serverless Delivery Framework](https://github.com/99xt/serverless-delivery-framework)**
This is a boilerplate for version release pipeline with serverless framework | [99xt](http://github.com/99xt) | -| **[Serverless Mailgun Slack](https://github.com/Marcus-L/serverless-mailgun-slack)**
A Serverless function for posting to a Slack Webhook in response to a Mailgun route | [Marcus-L](http://github.com/Marcus-L) | -| **[Pfs Email Serverless](https://github.com/SCPR/pfs-email-serverless)**
This is a lambda function created by the serverless framework. It searches through members in our mongodb who have not been sent emails and sends them an email with their custom token to unlock the pledge free stream. It then marks those members off as already receiving the email. | [SCPR](http://github.com/SCPR) | -| **[Plaid Cashburndown Service](https://github.com/cplee/cashburndown-service)**
Service for calculating cash burndown with plaid. Frontend code can be found here: https://github.com/cplee/cashburndown-site | [cplee](http://github.com/cplee) | -| **[Cordis Serverless](https://github.com/marzeelabs/cordis-serverless)**
A serverless API for EU Cordis data | [marzeelabs](http://github.com/marzeelabs) | -| **[Serverless Newsletter Signup](https://github.com/ivanderbu2/serverless-newsletter-signup)**
Saves user details into DynamoDB table. Required values are email, first_name and last_name. | [ivanderbu2](http://github.com/ivanderbu2) | -| **[Serverless Slack Cron](https://github.com/ivanderbu2/serverless-slack-cron)**
Lambda function which sends messages to Slack channel in regular intervals via cron trigger. | [ivanderbu2](http://github.com/ivanderbu2) | -| **[Giphy Bot](https://github.com/tywong/lambda-workshop-2016/tree/master/giphy-bot)**
giphy-bot for Facebook chat | [tywong](http://github.com/tywong) | -| **[Jwt Lambda Python](https://github.com/mikaelmork/jwt-auth.serverless)**
Minimal proof-of-concept implementation of JWT with Serverless / AWS Lambda | [mikaelmork](http://github.com/mikaelmork) | -| **[Sls Access Counter](https://github.com/takahashim/sls-access-counter)**
Site visitor counter | [takahashim](http://github.com/takahashim) | -| **[Sls Form Mail](https://github.com/takahashim/sls-form-mail)**
Send SNS email from form data | [takahashim](http://github.com/takahashim) | -| **[Serverless Python Sample](https://github.com/bennybauer/serverless-python-sample)**
A simple serverless python sample with REST API endpoints and dependencies | [bennybauer](http://github.com/bennybauer) | -| **[Serverless Msg Gateway](https://github.com/yonahforst/msg-gateway)**
A messaging aggregator for kik, skype, twilio, telegram, & messenger. Send and receive messages in a standard format. | [yonahforst](http://github.com/yonahforst) | -| **[Serverless AWS Rekognition Finpics](https://github.com/rgfindl/finpics)**
Use AWS Rekognition to provide a faces search of finpics.com | [rgfindl](http://github.com/rgfindl) | -| **[Serverless Slack Emojibot](https://github.com/markhobson/emojibot)**
Serverless slack bot for emoji | [markhobson](http://github.com/markhobson) | -| **[Keboola Developer Portal](https://github.com/keboola/developer-portal)**
Keboola developer portal built with Serverless | [keboola](http://github.com/keboola) | -| **[Serverless Cloudwatch Rds Custom Metrics](https://github.com/AndrewFarley/serverless-cloudwatch-rds-custom-metrics)**
A NodeJS-based MySQL RDS Data Collection script to push Custom Metrics to Cloudwatch with Serverless | [AndrewFarley](http://github.com/AndrewFarley) | -| **[Jrestless Examples](https://github.com/bbilger/jrestless-examples)**
[JRestless](https://github.com/bbilger/jrestless) (Java / JAX-RS) examples for [API Gateway Functions](https://github.com/bbilger/jrestless-examples/tree/master/aws/gateway) ([plain JAX-RS](https://github.com/bbilger/jrestless-examples/blob/master/aws/gateway/aws-gateway-showcase), [Spring](https://github.com/bbilger/jrestless-examples/blob/master/aws/gateway/aws-gateway-spring), [binary data requests/responses](https://github.com/bbilger/jrestless-examples/blob/master/aws/gateway/aws-gateway-binary), [custom authorizers](https://github.com/bbilger/jrestless-examples/blob/master/aws/gateway/aws-gateway-security-custom-authorizer) and [Cognito User Pool authorizers](https://github.com/bbilger/jrestless-examples/blob/master/aws/gateway/aws-gateway-security-cognito-authorizer)), [SNS Functions](https://github.com/bbilger/jrestless-examples/blob/master/aws/sns/aws-sns-usage-example) (asynchronous communication between functions) and [Service Functions](https://github.com/bbilger/jrestless-examples/blob/master/aws/service/aws-service-usage-example) (synchronous HTTP-like communication between functions - transparent through Feign) | [bbilger](http://github.com/bbilger) | -| **[Sc 5 Serverless Boilerplate](https://github.com/SC5/sc5-serverless-boilerplate)**
A boilerplate that contains setup for test-driven development | [SC5](http://github.com/SC5) | -| **[Serverless Blog To Podcast](https://github.com/SC5/serverless-blog-to-podcast)**
Service that reads RSS feed and converts the entries to a podcast feed and audio files using Amazon Polly | [SC5](http://github.com/SC5) | -| **[Offset Trump](https://github.com/FLGMwt/offset-trump)**
Single page app using Serverless (C# runtime) and S3 site hosting. Pledge to do a good thing for the next four years to offset the potential negative effects of the US Presidency | [FLGMwt](http://github.com/FLGMwt) | -| **[Serverless Url Shortener](https://github.com/aletheia/serverless-url-shortener)**
A simple url-shortener, using Serverless framework | [aletheia](http://github.com/aletheia) | -| **[Serverless Html Pdf](https://github.com/calvintychan/serverless-html-pdf)**
Service that convert HTML to PDF using PhantomJS's rasterize example. | [calvintychan](http://github.com/calvintychan) | -| **[Serverless Examples Cached Rds Ws](https://github.com/mugglmenzel/serverless-examples-cached-rds-ws)**
A serverless framework example project that uses API Gateway, ElastiCache, and RDS PostgreSQL. | [mugglmenzel](http://github.com/mugglmenzel) | -| **[Bittman](https://github.com/rhlsthrm/bittman)**
A serverless project that follows a stock trading algorithm and uses scheduled functions to save data to DynamoDB and send emails through Mailgun. | [rhlsthrm](http://github.com/rhlsthrm) | -| **[Adoptable Pet Bot](https://github.com/lynnaloo/adoptable-pet-bot)**
Tweets adoptable pets using Serverless (Node.js) and AWS Lambda | [lynnaloo](http://github.com/lynnaloo) | -| **[Owntracks Serverless](https://github.com/dschep/owntracks-serverless)**
A serverless implementation of the OwnTracks HTTP backend | [dschep](http://github.com/dschep) | -| **[Serverless Modern Koa](https://github.com/barczaG/serverless-modern-koa)**
Serverless modern koa starter kit | [barczaG](http://github.com/barczaG) | -| **[Serverless React JS Universal Rendering Boilerplate](https://github.com/TylorShin/react-universal-in-serverless)**
ReactJS web app Starter kit does universal (isomorphic) rendering with Serverless | [TylorShin](http://github.com/TylorShin) | -| **[Open Bot](https://github.com/open-bot/open-bot)**
An unoptionated Github bot driven by a configuration file in the repository | [open-bot](http://github.com/open-bot) | -| **[Aws Ses Serverless Example](https://github.com/lakshmantgld/aws-ses-serverless-example)**
AWS SES example in NodeJS using lambda | [lakshmantgld](http://github.com/lakshmantgld) | -| **[AWS API Gateway Serverless Project Written In Go](https://github.com/yunspace/serverless-golang)**
A serverless project that contains an API Gateway endpoint powered by a Lambda function written in golang and built using [eawsy/aws-lambda-go-shim](https://github.com/eawsy/aws-lambda-go-shim). | [yunspace](http://github.com/yunspace) | -| **[Video Preview And Analysis Service](https://github.com/laardee/video-preview-and-analysis-service)**
An event-driven service that generates labels using Amazon Rekognition and creates preview GIF animation from a video file. | [laardee](http://github.com/laardee) | -| **[Serverless ES 6 7 CRUD API](https://github.com/AnomalyInnovations/serverless-stack-demo-api)**
[Serverless Stack](http://serverless-stack.com) examples of backend CRUD APIs (DynamoDB + Lambda + API Gateway + Cognito User Pool authorizer) for [React.js single-page app](http://demo.serverless-stack.com) | [AnomalyInnovations](http://github.com/AnomalyInnovations) | -| **[SQS Worker With AWS Lambda And Cloud Watch Alarms](https://github.com/sbstjn/sqs-worker-serverless)**
Process messages stored in SQS with an [auto-scaled AWS Lambda worker](https://sbstjn.com/serverless-sqs-worker-with-aws-lambda.html) function. | [sbstjn](http://github.com/sbstjn) | -| **[Serverless Image Manager](https://github.com/TylorShin/lambda-image-manager)**
image upload / download with resizing. Used API gateway's binary support & serverless | [TylorShin](http://github.com/TylorShin) | -| **[AWS Lambda Power Tuning Powered By Step Functions](https://github.com/alexcasalboni/aws-lambda-power-tuning)**
Build a [Step Functions](https://aws.amazon.com/step-functions/) state machine to optimize your AWS Lambda Function memory/power configuration. | [alexcasalboni](http://github.com/alexcasalboni) | -| **[Amazon Kinesis Streams Fan Out Via Kinesis Analytics](https://github.com/alexcasalboni/kinesis-streams-fan-out-kinesis-analytics)**
Use [Amazon Kinesis Analytics](https://aws.amazon.com/kinesis/analytics/) to fan-out your Kinesis Streams and avoid read throttling. | [alexcasalboni](http://github.com/alexcasalboni) | -| **[Grants Api Serverless](https://github.com/comicrelief/grants-api-serverless)**
ES6 API to consume data from an external API, ingest into Elasticsearch and return a queryable endpoint on top of Elasticsearch | [comicrelief](http://github.com/comicrelief) | -| **[Honey Lambda](https://github.com/0x4D31/honeyLambda)**
a simple, serverless application designed to create and monitor URL {honey}tokens, on top of AWS Lambda and Amazon API Gateway | [0x4D31](http://github.com/0x4D31) | -| **[Faultline](https://github.com/faultline/faultline)**
Error tracking tool on AWS managed services. | [faultline](http://github.com/faultline) | -| **[Stack Overflow Monitor](https://github.com/picsoung/stackoverflowmonitor)**
Monitor Stack Overflow questions and post them in a Slack channel | [picsoung](http://github.com/picsoung) | -| **[Serverless Analytics](https://github.com/sbstjn/serverless-analytics)**
Write your own Google Analytics clone and track website visitors serverless with API Gateway, Kinesis, Lambda, and DynamoDB. | [sbstjn](http://github.com/sbstjn) | -| **[React Stripe Serverless Ecommerce](https://github.com/patrick-michelberger/serverless-shop)**
Serverless E-Commerce App with AWS Lambda, Stripe and React | [patrick-michelberger](http://github.com/patrick-michelberger) | -| **[Serverless Medium Text To Speech](https://github.com/RafalWilinski/serverless-medium-text-to-speech)**
Serverless-based, text-to-speech service for Medium articles | [RafalWilinski](http://github.com/RafalWilinski) | -| **[Serverless Java Dynamo DB Implementation Example](https://github.com/igorbakman/java-lambda-dynamodb)**
example for java programmers that want to work with AWS-Lambda and DynamoDB | [igorbakman](http://github.com/igorbakman) | -| **[AWS Cognito Custom User Pool Example](https://github.com/bsdkurt/aws-node-custom-user-pool)**
Example CloudFormation custom resource backed by a lambda using Cognito User Pools | [bsdkurt](http://github.com/bsdkurt) | -| **[Serverless Lambda Protobuf Responses](https://github.com/theburningmonk/lambda-protobuf-demo)**
Demo using API Gateway and Lambda with Protocol Buffer | [theburningmonk](http://github.com/theburningmonk) | -| **[Serverless Telegram Bot](https://github.com/jonatasbaldin/serverless-telegram-bot)**
This example demonstrates how to setup an echo Telegram Bot using the Serverless Framework ⚡🤖 | [jonatasbaldin](http://github.com/jonatasbaldin) | -| **[Serverless Line Bot](https://github.com/jiyeonseo/azure-line-bot-example)**
Echo Line bot using Azure Function with Node.js | [jiyeonseo](http://github.com/jiyeonseo) | -| **[Serverless Dashboard For Atom Editor](https://github.com/horike37/serverless-dashboard-for-atom)**
Atom editor package which allows you to deploy and visualize your serverless services with Serverless Framework on your editor. | [horike37](http://github.com/horike37) | -| **[Serverless Lambda Vpc Nat Redis](https://github.com/ittus/aws-lambda-vpc-nat-examples)**
Demo using API Gateway and Lambda with VPC and NAT to access Internet and AWS Resource | [ittus](http://github.com/ittus) | -| **[Serverless Gitlab CI](https://github.com/bvincent1/serverless-gitlab-ci)**
Simple Gitlab CI template for automatic testing and deployments | [bvincent1](http://github.com/bvincent1) | -| **[Serverless Ffmpeg](https://github.com/kvaggelakos/serverless-ffmpeg)**
Bucket event driven FFMPEG using serverless. Input bucket => Serverless ffmpeg => Output bucket. | [kvaggelakos](http://github.com/kvaggelakos) | -| **[Serverless SSH Command](https://github.com/upgle/serverless-openwhisk-ssh)**
Example of executing ssh command with OpenWhisk | [upgle](http://github.com/upgle) | -| **[Realtime WW 2 Alexa Skill](https://github.com/ceilfors/realtime-ww2-alexa)**
An alexa skill project that's using Alexa SDK. Can also be used for a working example of serverless-webpack (with use of async/await via babel). | [ceilfors](http://github.com/ceilfors) | -| **[Serverless Kakao Bot](https://github.com/JisuPark/serverless-kakao-bot)**
Easy development for Kakaotalk Bot with Serverless | [JisuPark](http://github.com/JisuPark) | -| **[Serverless Q A Example](https://github.com/jacksoncharles/serverless-qa-template-api)**
Inspired by the AWS example forum. A multitenancy Q&A template for surveys, forums and more | [jacksoncharles](http://github.com/jacksoncharles) | -| **[Personal Access Tokens Cron Check](https://github.com/madtrick/cfpat-audit)**
Audit for leaked PAT in your Contentful organization. How to use serverless as cronjobs to keep your Personal Access Tokens secure | [madtrick](http://github.com/madtrick) | -| **[Serverless Sns Api](https://github.com/eddielisc/serverless-sns-api)**
Build a SNS service on AWS, support backend API for SNS by device, by group and by user | [eddielisc](http://github.com/eddielisc) | -| **[Daily Instance Backups With AMI Rotation](https://github.com/AndrewFarley/AWSAutomatedDailyInstanceAMISnapshots)**
A simple Python application which scans through your entire AWS account for tagged instances, makes daily AMIs of them, and rotates their backups automatically | [AndrewFarley](http://github.com/AndrewFarley) | -| **[Serverless Instagram Crawler](https://github.com/kimcoder/serverless-instagram-crawler)**
Instagram hashtag Crawler with Lambda & DynamoDB. | [kimcoder](http://github.com/kimcoder) | -| **[Serving Binary Files](https://github.com/thomastoye/serverless-binary-files-xlsx)**
Small example showing how to serve binary files using Serverless on AWS with the serverless-apigw-binary plugin, using generated Excel files as an example | [thomastoye](http://github.com/thomastoye) | -| **[Serverless CloudWatch Proxy](https://github.com/abbasdgr8/cloudwatch-proxy)**
Logging adapter that consumes log streams from AWS CloudWatch, streams them to other log destinations | [abbasdgr8](http://github.com/abbasdgr8) | - +- Supports Node.js, Python, Java, Go, C#, Ruby, Swift, Kotlin, PHP, Scala, & F# +- Manages the lifecycle of your serverless architecture (build, deploy, update, delete). +- Safely deploy functions, events and their required resources together via provider resource managers (e.g., AWS CloudFormation). +- Functions can be grouped ("serverless services") for easy management of code, resources & processes, across large projects & teams. +- Minimal configuration and scaffolding. +- Built-in support for multiple stages. +- Optimized for CI/CD workflows. +- Loaded with automation, optimization and best practices. +- 100% Extensible: Extend or modify the Framework and its operations via Plugins. +- An ecosystem of serverless services and plugins. +- A passionate and welcoming community! ## Contributing + We love our contributors! Please read our [Contributing Document](CONTRIBUTING.md) to learn how you can start working on the Framework yourself. Check out our [help wanted](https://github.com/serverless/serverless/labels/help%20wanted) or [good first issue](https://github.com/serverless/serverless/labels/good%20first%20issue) labels to find issues we want to move forward on with your help. ## Community -* [Email Updates](http://eepurl.com/b8dv4P) -* [Serverless Forum](http://forum.serverless.com) -* [Gitter Chatroom](https://gitter.im/serverless/serverless) -* [Serverless Meetups](http://www.meetup.com/serverless/) -* [Stackoverflow](http://stackoverflow.com/questions/tagged/serverless-framework) -* [Facebook](https://www.facebook.com/serverless) -* [Twitter](https://twitter.com/goserverless) -* [Contact Us](mailto:hello@serverless.com) +- [Email Updates](http://eepurl.com/b8dv4P) +- [Serverless Forum](http://forum.serverless.com) +- [Gitter Chatroom](https://gitter.im/serverless/serverless) +- [Serverless Meetups](http://www.meetup.com/serverless/) +- [Stackoverflow](http://stackoverflow.com/questions/tagged/serverless-framework) +- [Facebook](https://www.facebook.com/serverless) +- [Twitter](https://twitter.com/goserverless) +- [Contact Us](mailto:hello@serverless.com) ## Consultants + These consultants use the Serverless Framework and can help you build your serverless projects. -* [Andrew Griffiths](https://www.andrewgriffithsonline.com/) - Independent consultant specialising in serverless technology -* [Trek10](https://www.trek10.com/) -* [Parallax](https://parall.ax/) – they also built the [David Guetta Campaign](https://serverlesscode.com/post/david-guetta-online-recording-with-lambda/) -* [Geniusee](https://geniusee.com) -* [SC5 Online](https://sc5.io) -* [Carrot Creative](https://carrot.is) -* [microapps](http://microapps.com) -* [Apiwise](http://www.apiwise.nl) -* [Useful IO](http://useful.io) - and [Hail Messaging](http://hail.io) -* [WhaleTech](https://whaletech.co/) -* [Hop Labs](http://www.hoplabs.com) -* [Webscale](https://webscale.fi/briefly-in-english/) -* [API talent](http://www.apitalent.co.nz) - who also run [Serverless-Auckland Meetup](http://www.meetup.com/Serverless-Auckland) -* [Branded Crate](https://www.brandedcrate.com/) -* [cloudonaut](https://cloudonaut.io/serverless-consulting/) -* [PromptWorks](https://www.promptworks.com/serverless/) -* [Craftship](https://craftship.io) -* [EPX Labs](http://www.epxlabs.com/) - runs [Serverless NYC Meetup](https://www.meetup.com/Serverless-NYC/) -* [Red Badger](https://red-badger.com) -* [Langa](http://langa.io/?utm_source=gh-serverless&utm_medium=github) - They built [Trails.js](http://github.com/trailsjs/trails) -* [Emerging Technology Advisors](https://www.emergingtechnologyadvisors.com) -* [OneSpeed](https://onespeed.io/) -* [Seraro](http://www.seraro.com/) - Who also runs Atlanta Serverless Meetup (https://www.meetup.com/Atlanta-CABI-Camp-Cloud-AI-Blockchain-IOT) and Delhi Serverless Meetup (https://www.meetup.com/Delhi-NCR-Serverless-Architecture-Meetup/) -* [superluminar](https://superluminar.io) - runs serverlessdays Hamburg and Serverless Meetup Hamburg ----- + +- [Andrew Griffiths](https://www.andrewgriffithsonline.com/) - Independent consultant specialising in serverless technology +- [Trek10](https://www.trek10.com/) +- [Parallax](https://parall.ax/) – they also built the [David Guetta Campaign](https://serverlesscode.com/post/david-guetta-online-recording-with-lambda/) +- [Geniusee](https://geniusee.com) +- [SC5 Online](https://sc5.io) +- [Carrot Creative](https://carrot.is) +- [microapps](http://microapps.com) +- [Apiwise](http://www.apiwise.nl) +- [Useful IO](http://useful.io) - and [Hail Messaging](http://hail.io) +- [WhaleTech](https://whaletech.co/) +- [Hop Labs](http://www.hoplabs.com) +- [Webscale](https://webscale.fi/briefly-in-english/) +- [API talent](http://www.apitalent.co.nz) - who also run [Serverless-Auckland Meetup](http://www.meetup.com/Serverless-Auckland) +- [Branded Crate](https://www.brandedcrate.com/) +- [cloudonaut](https://cloudonaut.io/serverless-consulting/) +- [PromptWorks](https://www.promptworks.com/serverless/) +- [Craftship](https://craftship.io) +- [EPX Labs](http://www.epxlabs.com/) - runs [Serverless NYC Meetup](https://www.meetup.com/Serverless-NYC/) +- [Red Badger](https://red-badger.com) +- [Langa](http://langa.io/?utm_source=gh-serverless&utm_medium=github) - They built [Trails.js](http://github.com/trailsjs/trails) +- [Emerging Technology Advisors](https://www.emergingtechnologyadvisors.com) +- [OneSpeed](https://onespeed.io/) +- [Seraro](http://www.seraro.com/) - Who also runs Atlanta Serverless Meetup (https://www.meetup.com/Atlanta-CABI-Camp-Cloud-AI-Blockchain-IOT) and Delhi Serverless Meetup (https://www.meetup.com/Delhi-NCR-Serverless-Architecture-Meetup/) +- [superluminar](https://superluminar.io) - runs serverlessdays Hamburg and Serverless Meetup Hamburg +- [Onica](https://www.onica.com/aws-cloud-native-developers/) - AWS Premier Consulting Partner for Cloud Native Development and host of [eleven regional Meetup groups](https://www.onica.com/events/). +- [null](https://null.tc/) - maintains [Bref](https://bref.sh/) to create serverless PHP applications + +--- ## Licensing diff --git a/RELEASE_CHECKLIST.md b/RELEASE_CHECKLIST.md index 672996109..4a3ebd11f 100644 --- a/RELEASE_CHECKLIST.md +++ b/RELEASE_CHECKLIST.md @@ -7,31 +7,27 @@ More info about our release process can be found in the [`RELEASE_PROCESS.md`](. ## Pre-Release - [ ] Look through all open issues and PRs (if any) of that milestone and close them / move them to another -milestone if still open -- [ ] Look through all closed issues and PRs of that milestone to see what has changed. Run `./scripts/prs-since-last-tag` or if you want to run against a specific tag `./scripts/prs-since-last-tag v1.20.0` to get a list of all merged PR's since a specific tag -- [ ] Close milestone on GitHub -- [ ] Create a new draft release in GitHub - -# Testing - -- [ ] Create a Serverless service (with some events), deploy and test it intensively -- [ ] Look through the milestone and test all of the new major changes -- [ ] Run `npm test` -- [ ] Run `npm run simple-integration-test` -- [ ] Run `npm run complex-integration-test` + milestone if still open +- [ ] Create a new branch for the release +- [ ] Bump the version number in `package.json` +- [ ] Run `./scripts/prs-since-last-tag ` +- [ ] Save the terminal output to your clipboard +- [ ] Close the milestone on GitHub +- [ ] Create a new [**draft** release](https://github.com/serverless/serverless/releases/new) in GitHub + - [ ] Use the content in your clipboard as a description (without the heading) + - [ ] Ensure that the "Tag version" follows our naming convention ## Prepare Package -- [ ] Create a new branch to bump version in `package.json` -- [ ] Install the latest `npm` version or Docker container with latest `node` and `npm` -- [ ] Bump version in `package.json`, remove `node_modules` folder and run `npm install` and `npm prune --production && npm shrinkwrap` -- [ ] Look through closed PRs and update `CHANGELOG.md` +- [ ] Install the latest `npm` version or Docker container with latest `node` and `npm` (Ensure to work with an `npm` version which is distributed with latest `node` version) +- [ ] Update `CHANGELOG.md` with the content from your clipboard - [ ] Make sure all files that need to be pushed are included in `package.json -> files` -- [ ] Send PR and merge PR with new version to be released -- [ ] Add the changes you made to `CHANGELOG.md` to the description of the GitHub release draft -- [ ] Go back to branch you want to release from (e.g. `master`) and pull bumped version changes from GitHub +- [ ] Commit your changes (make sure that `package.json` and `CHANGELOG.md` are updated) +- [ ] Push your branch and open up a new PR +- [ ] Await approval and merge the PR into `master` +- [ ] Go back to the branch you want to release from (e.g. `master`) and pull the changes from GitHub - [ ] Make sure there are no local changes to your repository (or reset with `git reset --hard HEAD`) -- [ ] Check `package.json`, `package-lock.json` and `npm-shrinkwrap.json` version config to make sure it fits what we want to release +- [ ] Check `package.json` version config to make sure it fits what we want to release ## Releasing @@ -41,7 +37,3 @@ milestone if still open ## Validate Release - [ ] Validate that `npm install` works (`npm install -g serverless@` or `npm install -g serverless` if latest is released) - -## Post-Release - -- [ ] Run `./scripts/generate-release-contributors-list ` and hand the generated list over to the release blog post author diff --git a/VERSIONING.md b/VERSIONING.md index b0162bf8d..8254d943a 100644 --- a/VERSIONING.md +++ b/VERSIONING.md @@ -35,20 +35,24 @@ Any non-backward compatible changes leads to a major version bump. This includes #### What is considered a breaking change? - Everything which touches the public facing API - + CLI commands - + CLI options - + Methods accessible through `this.serverless` - + ... + - CLI commands + - CLI options + - Methods accessible through `this.serverless` + - ... - Output Serverless produces - + Files and their names - + Transient data which is available during runtime - + Formatted CLI outputs (e.g. via `--json`) **NOT:** standard outputs - + ... + - Files and their names + - Transient data which is available during runtime + - Formatted CLI outputs (e.g. via `--json`) **NOT:** standard outputs + - ... #### Example of a Breaking Change If we remove a helper function from the serverless object passed down to a plugin then this is a breaking change since some people might rely on it in custom made plugins. +### Node.js versions + +The Serverless Framework supports the major cloud providers Node.js runtime versions. Support for old Node.js versions will be removed once Cloud providers announce that such runtimes are not supported anymore. + ### FAQ 1. Is it okay to mark a feature as deprecated in version 1.4.0 and then remove it in 1.8.0 diff --git a/bin/serverless b/bin/serverless index b8cff829c..a0afa2c22 100755 --- a/bin/serverless +++ b/bin/serverless @@ -1,65 +1,9 @@ #!/usr/bin/env node +// TODO: Remove with file with next major (breaking) release +// Left as a fallback placeholder to not accidentally break things +// https://github.com/search?q=%22bin%2Fserverless%22&type=Code + 'use strict'; -const autocomplete = require('../lib/utils/autocomplete'); -const BbPromise = require('bluebird'); -const logError = require('../lib/classes/Error').logError; -const uuid = require('uuid'); -const initializeErrorReporter = require('../lib/utils/sentry').initializeErrorReporter; - -Error.stackTraceLimit = Infinity; - -if (process.env.SLS_DEBUG) { - // For performance reasons enabled only in SLS_DEBUG mode - BbPromise.config({ - longStackTraces: true, - }); -} - -process.on('unhandledRejection', (e) => { - logError(e); -}); -process.noDeprecation = true; - -const invocationId = uuid.v4(); - -// boot up error reporting via sentry before anything -(() => initializeErrorReporter(invocationId).then(() => { - if (process.argv[2] === 'completion') { - return autocomplete(); - } - // requiring here so that if anything went wrong, - // during require, it will be caught. - const Serverless = require('../lib/Serverless'); // eslint-disable-line global-require - - const serverless = new Serverless({ - interactive: typeof process.env.CI === 'undefined', - }); - - serverless.invocationId = invocationId; - - return serverless.init() - .then(() => serverless.run()) - .then(() => process.exit(0)) - .catch((err) => { - // If Enterprise Plugin, capture error - let enterpriseErrorHandler = null; - serverless.pluginManager.plugins.forEach((p) => { - if (p.enterprise && p.enterprise.errorHandler) { - enterpriseErrorHandler = p.enterprise.errorHandler; - } - }); - if (!enterpriseErrorHandler) { throw err; } - return enterpriseErrorHandler(err, invocationId) - .catch((error) => { - console.log(error) - }) - .then(() => { - throw err - }); - }) -}).catch(e => { - process.exitCode = 1; - logError(e); -}))(); +require('./serverless.js'); diff --git a/bin/serverless.js b/bin/serverless.js new file mode 100755 index 000000000..31273f1e9 --- /dev/null +++ b/bin/serverless.js @@ -0,0 +1,71 @@ +#!/usr/bin/env node + +'use strict'; + +const autocomplete = require('../lib/utils/autocomplete'); +const BbPromise = require('bluebird'); +const logError = require('../lib/classes/Error').logError; +const uuid = require('uuid'); +const initializeErrorReporter = require('../lib/utils/sentry').initializeErrorReporter; + +Error.stackTraceLimit = Infinity; + +if (process.env.SLS_DEBUG) { + // For performance reasons enabled only in SLS_DEBUG mode + BbPromise.config({ + longStackTraces: true, + }); +} + +process.on('unhandledRejection', e => { + logError(e); +}); +process.noDeprecation = true; + +const invocationId = uuid.v4(); + +// boot up error reporting via sentry before anything +(() => + initializeErrorReporter(invocationId) + .then(() => { + if (process.argv[2] === 'completion') { + return autocomplete(); + } + // requiring here so that if anything went wrong, + // during require, it will be caught. + const Serverless = require('../lib/Serverless'); + + const serverless = new Serverless(); + + serverless.invocationId = invocationId; + + return serverless + .init() + .then(() => serverless.run()) + .catch(err => { + // If Enterprise Plugin, capture error + let enterpriseErrorHandler = null; + serverless.pluginManager.plugins.forEach(p => { + if (p.enterprise && p.enterprise.errorHandler) { + enterpriseErrorHandler = p.enterprise.errorHandler; + } + }); + if (!enterpriseErrorHandler) { + throw err; + } + return enterpriseErrorHandler(err, invocationId) + .catch(error => { + process.stdout.write(`${error.stack}\n`); + }) + .then(() => { + throw err; + }); + }); + }) + .then( + () => process.exit(0), + e => { + process.exitCode = 1; + logError(e); + } + ))(); diff --git a/bin/test b/bin/test deleted file mode 100755 index 0a6536f35..000000000 --- a/bin/test +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env node - -'use strict'; - -process.on('unhandledRejection', err => { - throw err; -}); - -if (process.argv.length <= 2) { - process.argv.push( - '!(node_modules)/**/*.test.js', - '--require=sinon-bluebird', - '-R', - 'spec', - '--recursive', - '--no-exit' - ); -} - -require('mocha/bin/_mocha'); diff --git a/docker-compose.yml b/docker-compose.yml index 8903c00f2..d1858994b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -84,8 +84,8 @@ services: aws-go-mod: image: golang:1.11 volumes: - - ./tmp/serverless-integration-test-aws-go-mod:/app - - ./tmp/serverless-integration-test-aws-go-mod:/go/src/app + - ./tmp/serverless-integration-test-aws-go-mod:/app + - ./tmp/serverless-integration-test-aws-go-mod:/go/src/app aws-nodejs-typescript: image: node:6.10.3 volumes: diff --git a/docs/README.md b/docs/README.md index f79ac8956..931dcb430 100644 --- a/docs/README.md +++ b/docs/README.md @@ -17,7 +17,9 @@ menuItems: --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/) + # Documentation diff --git a/docs/providers/README.md b/docs/providers/README.md index b4b86e631..377843edc 100644 --- a/docs/providers/README.md +++ b/docs/providers/README.md @@ -5,7 +5,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/) + # Serverless Infrastructure Providers diff --git a/docs/providers/aws/README.md b/docs/providers/aws/README.md index 77374cacb..63ed9f440 100644 --- a/docs/providers/aws/README.md +++ b/docs/providers/aws/README.md @@ -5,7 +5,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/) + # AWS Provider Documentation @@ -93,6 +95,7 @@ If you have any questions, [search the forums](https://forum.serverless.com?utm_
  • Schedule
  • SNS
  • SQS
  • +
  • ALB
  • Alexa Skill
  • Alexa Smart Home
  • IoT
  • diff --git a/docs/providers/aws/cli-reference/README.md b/docs/providers/aws/cli-reference/README.md index cddf4206f..29a771ecc 100644 --- a/docs/providers/aws/cli-reference/README.md +++ b/docs/providers/aws/cli-reference/README.md @@ -5,12 +5,14 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/cli-reference/) + # Serverless AWS Lambda CLI Reference -Welcome to the Serverless AWS Lambda CLI Reference! Please select a section on the left to get started. +Welcome to the Serverless AWS Lambda CLI Reference! Please select a section on the left to get started. **Note:** Before continuing [AWS system credentials](../guide/credentials.md) are required for using the CLI. diff --git a/docs/providers/aws/cli-reference/config-credentials.md b/docs/providers/aws/cli-reference/config-credentials.md index d4736c29b..d7eb8168c 100644 --- a/docs/providers/aws/cli-reference/config-credentials.md +++ b/docs/providers/aws/cli-reference/config-credentials.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/cli-reference/config-credentials) + # AWS - Config Credentials diff --git a/docs/providers/aws/cli-reference/create.md b/docs/providers/aws/cli-reference/create.md index c4be41434..e197045a0 100644 --- a/docs/providers/aws/cli-reference/create.md +++ b/docs/providers/aws/cli-reference/create.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/cli-reference/create) + # AWS - Create @@ -33,6 +35,7 @@ serverless create --template-url https://github.com/serverless/serverless/tree/m ``` ## Options + - `--template` or `-t` The name of one of the available templates. **Required if --template-url and --template-path are not present**. - `--template-url` or `-u` The name of one of the available templates. **Required if --template and --template-path are not present**. - `--template-path` The local path of your template. **Required if --template and --template-url are not present**. @@ -40,6 +43,7 @@ serverless create --template-url https://github.com/serverless/serverless/tree/m - `--name` or `-n` the name of the service in `serverless.yml`. ## Provided lifecycle events + - `create:create` ## Available Templates diff --git a/docs/providers/aws/cli-reference/deploy-function.md b/docs/providers/aws/cli-reference/deploy-function.md index 8b06da246..c9a22f4b2 100644 --- a/docs/providers/aws/cli-reference/deploy-function.md +++ b/docs/providers/aws/cli-reference/deploy-function.md @@ -7,12 +7,14 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/cli-reference/deploy-function) + # AWS - Deploy Function -The `sls deploy function` command deploys an individual function without AWS CloudFormation. This command simply swaps out the zip file that your CloudFormation stack is pointing toward. This is a much faster way of deploying changes in code. +The `sls deploy function` command deploys an individual function without AWS CloudFormation. This command simply swaps out the zip file that your CloudFormation stack is pointing toward. This is a much faster way of deploying changes in code. ```bash serverless deploy function -f functionName @@ -24,6 +26,7 @@ is out of sync with your CloudFormation stack. Use this for faster development cycles and not production deployments ## Options + - `--function` or `-f` The name of the function which should be deployed - `--stage` or `-s` The stage in your service that you want to deploy to. - `--region` or `-r` The region in that stage that you want to deploy to. diff --git a/docs/providers/aws/cli-reference/deploy-list.md b/docs/providers/aws/cli-reference/deploy-list.md index 2c61737aa..8382b356b 100644 --- a/docs/providers/aws/cli-reference/deploy-list.md +++ b/docs/providers/aws/cli-reference/deploy-list.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/cli-reference/deploy-list) + # AWS - Deploy List diff --git a/docs/providers/aws/cli-reference/deploy.md b/docs/providers/aws/cli-reference/deploy.md index 305a5d1cb..96bab909d 100644 --- a/docs/providers/aws/cli-reference/deploy.md +++ b/docs/providers/aws/cli-reference/deploy.md @@ -7,18 +7,22 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/cli-reference/deploy) + # AWS - deploy -The `sls deploy` command deploys your entire service via CloudFormation. Run this command when you have made infrastructure changes (i.e., you edited `serverless.yml`). Use `serverless deploy function -f myFunction` when you have made code changes and you want to quickly upload your updated code to AWS Lambda or just change function configuration. +The `sls deploy` command deploys your entire service via CloudFormation. Run this command when you have made infrastructure changes (i.e., you edited `serverless.yml`). Use `serverless deploy function -f myFunction` when you have made code changes and you want to quickly upload your updated code to AWS Lambda or just change function configuration. ```bash serverless deploy ``` ## Options + +- `--config` or `-c` Path to your conifguration file, if other than `serverless.yml|.yaml|.js|.json`. - `--stage` or `-s` The stage in your service that you want to deploy to. - `--region` or `-r` The region in that stage that you want to deploy to. - `--package` or `-p` path to a pre-packaged directory and skip packaging step. diff --git a/docs/providers/aws/cli-reference/info.md b/docs/providers/aws/cli-reference/info.md index 665f8ccc8..35af22dbd 100644 --- a/docs/providers/aws/cli-reference/info.md +++ b/docs/providers/aws/cli-reference/info.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/cli-reference/info) + # AWS - Info @@ -19,11 +21,13 @@ serverless info ``` ## Options + - `--stage` or `-s` The stage in your service you want to display information about. - `--region` or `-r` The region in your stage that you want to display information about. - `--verbose` or `-v` Shows displays any Stack Output. ## Provided lifecycle events + - `info:info` ## Examples diff --git a/docs/providers/aws/cli-reference/install.md b/docs/providers/aws/cli-reference/install.md index ddc4c3a68..167dab1ab 100644 --- a/docs/providers/aws/cli-reference/install.md +++ b/docs/providers/aws/cli-reference/install.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/cli-reference/install) + # AWS - Install @@ -19,12 +21,21 @@ serverless install --url https://github.com/some/service ``` ## Options + - `--url` or `-u` The services GitHub URL. **Required**. - `--name` or `-n` Name for the service. ## Provided lifecycle events + - `install:install` +## Supported Code Hosting Platforms + +- GitHub +- GitHub Enterprise +- GitLab +- BitBucket + ## Examples ### Installing a service from a GitHub URL diff --git a/docs/providers/aws/cli-reference/invoke-local.md b/docs/providers/aws/cli-reference/invoke-local.md index e9a51687d..f11ec6b02 100644 --- a/docs/providers/aws/cli-reference/invoke-local.md +++ b/docs/providers/aws/cli-reference/invoke-local.md @@ -7,12 +7,14 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/cli-reference/invoke-local) + # AWS - Invoke Local -This runs your code locally by emulating the AWS Lambda environment. Please keep in mind, it's not a 100% perfect emulation, there may be some differences, but it works for the vast majority of users. We mock the `context` with simple mock data. +This runs your code locally by emulating the AWS Lambda environment. Please keep in mind, it's not a 100% perfect emulation, there may be some differences, but it works for the vast majority of users. We mock the `context` with simple mock data. ```bash serverless invoke local --function functionName @@ -28,6 +30,7 @@ serverless invoke local --function functionName - `--raw` Pass data as a raw string even if it is JSON. If not set, JSON data are parsed and passed as an object. - `--contextPath` or `-x`, The path to a json file holding input context to be passed to the invoked function. This path is relative to the root directory of the service. - `--context` or `-c`, String data to be passed as a context to your function. Same like with `--data`, context included in `--contextPath` will overwrite the context you passed with `--context` flag. + * `--env` or `-e` String representing an environment variable to set when invoking your function, in the form `=`. Can be repeated for more than one environment variable. * `--docker` Enable docker support for NodeJS/Python/Ruby/Java. Enabled by default for other runtimes. @@ -80,7 +83,7 @@ This example will pass the json data in the `lib/data.json` file (relative to th { "resource": "/", "path": "/", - "httpMethod": "GET", + "httpMethod": "GET" // etc. // } ``` @@ -96,6 +99,7 @@ serverless invoke local --function functionName --context "hello world" ```bash serverless invoke local --function functionName --contextPath lib/context.json ``` + This example will pass the json context in the `lib/context.json` file (relative to the root of the service) while invoking the specified/deployed function. ### Local function invocation, setting environment variables @@ -120,7 +124,7 @@ Use of the `--docker` flag and runtimes other than NodeJs, Python, Java, & Ruby ## Resource permissions -Lambda functions assume an *IAM role* during execution: the framework creates this role, and set all the permission provided in the `iamRoleStatements` section of `serverless.yml`. +Lambda functions assume an _IAM role_ during execution: the framework creates this role, and set all the permission provided in the `iamRoleStatements` section of `serverless.yml`. Unless you explicitly state otherwise, every call to the AWS SDK inside the lambda function is made using this role (a temporary pair of key / secret is generated and set by AWS as environment variables, `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`). @@ -131,4 +135,4 @@ Take a look to the official AWS documentation (in this particular instance, for - [http://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/loading-node-credentials-shared.html](http://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/loading-node-credentials-shared.html) - [http://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/loading-node-credentials-lambda.html](http://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/loading-node-credentials-lambda.html) -Whatever approach you decide to implement, **be aware**: the set of permissions might be (and probably is) different, so you won't have an exact simulation of the *real* IAM policy in place. +Whatever approach you decide to implement, **be aware**: the set of permissions might be (and probably is) different, so you won't have an exact simulation of the _real_ IAM policy in place. diff --git a/docs/providers/aws/cli-reference/invoke.md b/docs/providers/aws/cli-reference/invoke.md index 515df23af..f3ab278a0 100644 --- a/docs/providers/aws/cli-reference/invoke.md +++ b/docs/providers/aws/cli-reference/invoke.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/cli-reference/invoke) + # AWS - Invoke @@ -21,6 +23,7 @@ serverless invoke [local] --function functionName **Note:** Please refer to [this guide](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-set-up-simple-proxy.html#api-gateway-simple-proxy-for-lambda-input-format) for event data passing when your function uses the `http` event with a Lambda Proxy integration. ## Options + - `--function` or `-f` The name of the function in your service that you want to invoke. **Required**. - `--stage` or `-s` The stage in your service you want to invoke your function in. - `--region` or `-r` The region in your stage that you want to invoke your function in. @@ -31,6 +34,7 @@ serverless invoke [local] --function functionName - `--log` or `-l` If set to `true` and invocation type is `RequestResponse`, it will output logging data of the invocation. Default is `false`. ## Provided lifecycle events + - `invoke:invoke` # Invoke Local @@ -45,7 +49,7 @@ serverless invoke local --function functionName - `--function` or `-f` The name of the function in your service that you want to invoke locally. **Required**. - `--path` or `-p` The path to a json file holding input data to be passed to the invoked function as the `event`. This path is relative to the -root directory of the service. + root directory of the service. - `--data` or `-d` String data to be passed as an event to your function. Keep in mind that if you pass both `--path` and `--data`, the data included in the `--path` file will overwrite the data you passed with the `--data` flag. - `--raw` Pass data as a raw string even if it is JSON. If not set, JSON data are parsed and passed as an object. - `--contextPath` or `-x`, The path to a json file holding input context to be passed to the invoked function. This path is relative to the root directory of the service. @@ -97,7 +101,7 @@ the specified/deployed function. { "resource": "/", "path": "/", - "httpMethod": "GET", + "httpMethod": "GET" // etc. // } ``` @@ -109,9 +113,11 @@ serverless invoke local --function functionName --context "hello world" ``` ### Local function invocation with context passing + ```bash serverless invoke local --function functionName --contextPath lib/context.json ``` + This example will pass the json context in the `lib/context.json` file (relative to the root of the service) while invoking the specified/deployed function. ### Limitations @@ -120,7 +126,7 @@ Currently, `invoke local` only supports the NodeJs and Python runtimes. ## Resource permissions -Lambda functions assume an *IAM role* during execution: the framework creates this role, and set all the permission provided in the `iamRoleStatements` section of `serverless.yml`. +Lambda functions assume an _IAM role_ during execution: the framework creates this role, and set all the permission provided in the `iamRoleStatements` section of `serverless.yml`. Unless you explicitly state otherwise, every call to the AWS SDK inside the lambda function is made using this role (a temporary pair of key / secret is generated and set by AWS as environment variables, `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`). @@ -131,4 +137,4 @@ Take a look to the official AWS documentation (in this particular instance, for - [http://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/loading-node-credentials-shared.html](http://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/loading-node-credentials-shared.html) - [http://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/loading-node-credentials-lambda.html](http://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/loading-node-credentials-lambda.html) -Whatever approach you decide to implement, **be aware**: the set of permissions might be (and probably is) different, so you won't have an exact simulation of the *real* IAM policy in place. +Whatever approach you decide to implement, **be aware**: the set of permissions might be (and probably is) different, so you won't have an exact simulation of the _real_ IAM policy in place. diff --git a/docs/providers/aws/cli-reference/login.md b/docs/providers/aws/cli-reference/login.md index 81f42e851..ba6fc8e5e 100644 --- a/docs/providers/aws/cli-reference/login.md +++ b/docs/providers/aws/cli-reference/login.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/cli-reference/login) + # Login diff --git a/docs/providers/aws/cli-reference/logs.md b/docs/providers/aws/cli-reference/logs.md index 31e9b2b6e..8cae90f84 100644 --- a/docs/providers/aws/cli-reference/logs.md +++ b/docs/providers/aws/cli-reference/logs.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/cli-reference/logs) + # AWS - Logs @@ -65,16 +67,19 @@ This command returns as many log events as can fit in 1MB (up to 10,000 log even ```bash serverless logs -f hello ``` + This will fetch the logs from last 10 minutes as startTime was not given. ```bash serverless logs -f hello --startTime 5h ``` + This will fetch the logs that happened in the past 5 hours. ```bash serverless logs -f hello --startTime 1469694264 ``` + This will fetch the logs that happened starting at epoch `1469694264`. ```bash @@ -86,4 +91,5 @@ Serverless will tail the CloudWatch log output and print new log messages coming ```bash serverless logs -f hello --filter serverless ``` + This will fetch only the logs that contain the string `serverless` diff --git a/docs/providers/aws/cli-reference/metrics.md b/docs/providers/aws/cli-reference/metrics.md index d4b57f2ea..1ea7cad93 100644 --- a/docs/providers/aws/cli-reference/metrics.md +++ b/docs/providers/aws/cli-reference/metrics.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/cli-reference/metrics) + # AWS - Metrics diff --git a/docs/providers/aws/cli-reference/package.md b/docs/providers/aws/cli-reference/package.md index 665b80178..ed5e8ccd5 100644 --- a/docs/providers/aws/cli-reference/package.md +++ b/docs/providers/aws/cli-reference/package.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/cli-reference/package) + # AWS - package @@ -19,6 +21,7 @@ serverless package ``` ## Options + - `--stage` or `-s` The stage in your service that you want to deploy to. - `--region` or `-r` The region in that stage that you want to deploy to. - `--package` or `-p` path to the custom packaging directory you want. diff --git a/docs/providers/aws/cli-reference/plugin-install.md b/docs/providers/aws/cli-reference/plugin-install.md index 1c1f537ac..f57cad05d 100644 --- a/docs/providers/aws/cli-reference/plugin-install.md +++ b/docs/providers/aws/cli-reference/plugin-install.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/cli-reference/plugin-install) + # Plugin Install @@ -22,9 +24,11 @@ serverless plugin install --name pluginName ``` ## Options + - `--name` or `-n` The plugins name. **Required**. ## Provided lifecycle events + - `plugin:install:install` ## Examples diff --git a/docs/providers/aws/cli-reference/plugin-list.md b/docs/providers/aws/cli-reference/plugin-list.md index 51881f451..4d0c945fc 100644 --- a/docs/providers/aws/cli-reference/plugin-list.md +++ b/docs/providers/aws/cli-reference/plugin-list.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/cli-reference/plugin-list) + # Plugin List @@ -19,7 +21,9 @@ serverless plugin list ``` ## Options -- *None* + +- _None_ ## Provided lifecycle events + - `plugin:list:list` diff --git a/docs/providers/aws/cli-reference/plugin-search.md b/docs/providers/aws/cli-reference/plugin-search.md index 2837b623c..af21b6da3 100644 --- a/docs/providers/aws/cli-reference/plugin-search.md +++ b/docs/providers/aws/cli-reference/plugin-search.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/cli-reference/plugin-search) + # Plugin Search @@ -19,9 +21,11 @@ serverless plugin search --query query ``` ## Options + - `--query` or `-q` The query you want to use for your search. **Required**. ## Provided lifecycle events + - `plugin:search:search` ## Examples diff --git a/docs/providers/aws/cli-reference/plugin-uninstall.md b/docs/providers/aws/cli-reference/plugin-uninstall.md index 9e40cd118..8516f7a2d 100644 --- a/docs/providers/aws/cli-reference/plugin-uninstall.md +++ b/docs/providers/aws/cli-reference/plugin-uninstall.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/cli-reference/plugin-uninstall) + # Plugin Uninstall @@ -19,9 +21,11 @@ serverless plugin uninstall --name pluginName ``` ## Options + - `--name` or `-n` The plugins name. **Required**. ## Provided lifecycle events + - `plugin:uninstall:uninstall` ## Examples diff --git a/docs/providers/aws/cli-reference/print.md b/docs/providers/aws/cli-reference/print.md index 4009dac29..8fa979d95 100644 --- a/docs/providers/aws/cli-reference/print.md +++ b/docs/providers/aws/cli-reference/print.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/cli-reference/print) + # Print @@ -42,7 +44,7 @@ custom: provider: name: aws - runtime: nodejs6.10 + runtime: nodejs10.x stage: ${opt:stage, "dev"} functions: @@ -66,7 +68,7 @@ custom: bucketName: test provider: name: aws - runtime: nodejs6.10 + runtime: nodejs10.x stage: dev # <-- Resolved functions: hello: diff --git a/docs/providers/aws/cli-reference/remove.md b/docs/providers/aws/cli-reference/remove.md index ccb5db9d4..020705613 100644 --- a/docs/providers/aws/cli-reference/remove.md +++ b/docs/providers/aws/cli-reference/remove.md @@ -7,23 +7,27 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/cli-reference/remove) + # AWS - Remove -The `sls remove` command will remove the deployed service, defined in your current working directory, from the provider. +The `sls remove` command will remove the deployed service, defined in your current working directory, from the provider. ```bash serverless remove ``` ## Options + - `--stage` or `-s` The name of the stage in service. - `--region` or `-r` The name of the region in stage. - `--verbose` or `-v` Shows all stack events during deployment. ## Provided lifecycle events + - `remove:remove` ## Examples diff --git a/docs/providers/aws/cli-reference/rollback-function.md b/docs/providers/aws/cli-reference/rollback-function.md index e8e6fff80..6a398bb64 100644 --- a/docs/providers/aws/cli-reference/rollback-function.md +++ b/docs/providers/aws/cli-reference/rollback-function.md @@ -7,9 +7,10 @@ layout: Doc --> -### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/cli-reference/rollback-function) - +### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/cli-reference/rollback-function) + + # AWS - Rollback Function diff --git a/docs/providers/aws/cli-reference/rollback.md b/docs/providers/aws/cli-reference/rollback.md index de4a88eec..52d7d98be 100644 --- a/docs/providers/aws/cli-reference/rollback.md +++ b/docs/providers/aws/cli-reference/rollback.md @@ -7,9 +7,10 @@ layout: Doc --> -### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/cli-reference/rollback) - +### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/cli-reference/rollback) + + # AWS - Rollback @@ -22,10 +23,12 @@ 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. ## Provided lifecycle events + - `rollback:initialize` - `rollback:rollback` diff --git a/docs/providers/aws/cli-reference/slstats.md b/docs/providers/aws/cli-reference/slstats.md index 2dc6e5457..be607109d 100644 --- a/docs/providers/aws/cli-reference/slstats.md +++ b/docs/providers/aws/cli-reference/slstats.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/cli-reference/slstats) + # Serverless Statistics and Usage Tracking @@ -46,8 +48,8 @@ The following is a list of the events that we collect: - service_pluginUninstalled - service_installed - user_awsCredentialsConfigured -- user_enabledTracking -- user_disabledTracking +- user_enabledTracking +- user_disabledTracking - user_loggedIn - user_loggedOut diff --git a/docs/providers/aws/events/README.md b/docs/providers/aws/events/README.md index 8f2403e58..ec19c71f0 100644 --- a/docs/providers/aws/events/README.md +++ b/docs/providers/aws/events/README.md @@ -5,7 +5,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/events/) + # Serverless AWS Lambda Events diff --git a/docs/providers/aws/events/alb.md b/docs/providers/aws/events/alb.md new file mode 100644 index 000000000..1d7fc6c94 --- /dev/null +++ b/docs/providers/aws/events/alb.md @@ -0,0 +1,63 @@ + + + + +### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/events/alb) + + + +# Application Load Balancer + +[Application Load Balancers](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/introduction.html) can be used to re-route requests when certain traffic patterns are met. While traffic can be routed to services such as EC2 it [can also be routed to Lambda functions](https://aws.amazon.com/de/blogs/networking-and-content-delivery/lambda-functions-as-targets-for-application-load-balancers/) which can in turn be used process incoming requests. + +The Serverless Framework makes it possible to setup the connection between Application Load Balancers and Lambda functions with the help of the `alb` event. + +## Event definition + +```yml +functions: + albEventConsumer: + handler: handler.hello + events: + - alb: + listenerArn: arn:aws:elasticloadbalancing:us-east-1:12345:listener/app/my-load-balancer/50dc6c495c0c9188/ + priority: 1 + conditions: + path: /hello +``` + +## Using different conditions + +```yml +functions: + albEventConsumer: + handler: handler.hello + events: + - alb: + listenerArn: arn:aws:elasticloadbalancing:us-east-1:12345:listener/app/my-load-balancer/50dc6c495c0c9188/ + priority: 1 + conditions: + host: example.com + path: /hello + method: + - POST + - PATCH + host: + - example.com + - example2.com + header: + name: foo + values: + - bar + query: + bar: true + ip: + - fe80:0000:0000:0000:0204:61ff:fe9d:f156/6 + - 192.168.0.1/0 +``` diff --git a/docs/providers/aws/events/alexa-skill.md b/docs/providers/aws/events/alexa-skill.md index b7355ba09..092835613 100644 --- a/docs/providers/aws/events/alexa-skill.md +++ b/docs/providers/aws/events/alexa-skill.md @@ -1,13 +1,15 @@ + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/events/alexa-skill) + # Alexa Skill diff --git a/docs/providers/aws/events/alexa-smart-home.md b/docs/providers/aws/events/alexa-smart-home.md index a451a4b2e..92f52c086 100644 --- a/docs/providers/aws/events/alexa-smart-home.md +++ b/docs/providers/aws/events/alexa-smart-home.md @@ -1,13 +1,15 @@ + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/events/alexa-smart-home) + # Alexa Smart Home diff --git a/docs/providers/aws/events/apigateway.md b/docs/providers/aws/events/apigateway.md index ad8ff969d..a7cf95d01 100644 --- a/docs/providers/aws/events/apigateway.md +++ b/docs/providers/aws/events/apigateway.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/events/apigateway) + # API Gateway @@ -62,15 +64,16 @@ _Are you looking for tutorials on using API Gateway? Check out the following res To create HTTP endpoints as Event sources for your AWS Lambda Functions, use the Serverless Framework's easy AWS API Gateway Events syntax. There are five ways you can configure your HTTP endpoints to integrate with your AWS Lambda Functions: -* `lambda-proxy` / `aws-proxy` / `aws_proxy` (Recommended) -* `lambda` / `aws` -* `http` -* `http-proxy` / `http_proxy` -* `mock` + +- `lambda-proxy` / `aws-proxy` / `aws_proxy` (Recommended) +- `lambda` / `aws` +- `http` +- `http-proxy` / `http_proxy` +- `mock` **The Framework uses the `lambda-proxy` method (i.e., everything is passed into your Lambda) by default unless another method is supplied by the user** -The difference between these is `lambda-proxy` (alternative writing styles are `aws-proxy` and `aws_proxy` for compatibility with the standard AWS integration type naming) automatically passes the content of the HTTP request into your AWS Lambda function (headers, body, etc.) and allows you to configure your response (headers, status code, body) in the code of your AWS Lambda Function. Whereas, the `lambda` method makes you explicitly define headers, status codes, and more in the configuration of each API Gateway Endpoint (not in code). We highly recommend using the `lambda-proxy` method if it supports your use-case, since the `lambda` method is highly tedious. +The difference between these is `lambda-proxy` (alternative writing styles are `aws-proxy` and `aws_proxy` for compatibility with the standard AWS integration type naming) automatically passes the content of the HTTP request into your AWS Lambda function (headers, body, etc.) and allows you to configure your response (headers, status code, body) in the code of your AWS Lambda Function. Whereas, the `lambda` method makes you explicitly define headers, status codes, and more in the configuration of each API Gateway Endpoint (not in code). We highly recommend using the `lambda-proxy` method if it supports your use-case, since the `lambda` method is highly tedious. Use `http` for integrating with an HTTP back end, `http-proxy` for integrating with the HTTP proxy integration or `mock` for testing without actually invoking the back end. @@ -99,22 +102,22 @@ functions: 'use strict'; module.exports.hello = function(event, context, callback) { + console.log(event); // Contains incoming request data (e.g., query params, headers and more) - console.log(event); // Contains incoming request data (e.g., query params, headers and more) + const response = { + statusCode: 200, + headers: { + 'x-custom-header': 'My Header Value', + }, + body: JSON.stringify({ message: 'Hello World!' }), + }; - const response = { - statusCode: 200, - headers: { - "x-custom-header" : "My Header Value" - }, - body: JSON.stringify({ "message": "Hello World!" }) - }; - - callback(null, response); + callback(null, response); }; ``` **Note:** When the body is a JSON-Document, you must parse it yourself: + ``` JSON.parse(event.body); ``` @@ -123,62 +126,62 @@ JSON.parse(event.body); ```json { - "resource": "/", - "path": "/", + "resource": "/", + "path": "/", + "httpMethod": "POST", + "headers": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, br", + "Accept-Language": "en-GB,en-US;q=0.8,en;q=0.6,zh-CN;q=0.4", + "cache-control": "max-age=0", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "GB", + "content-type": "application/x-www-form-urlencoded", + "Host": "j3ap25j034.execute-api.eu-west-2.amazonaws.com", + "origin": "https://j3ap25j034.execute-api.eu-west-2.amazonaws.com", + "Referer": "https://j3ap25j034.execute-api.eu-west-2.amazonaws.com/dev/", + "upgrade-insecure-requests": "1", + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36", + "Via": "2.0 a3650115c5e21e2b5d133ce84464bea3.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "0nDeiXnReyHYCkv8cc150MWCFCLFPbJoTs1mexDuKe2WJwK5ANgv2A==", + "X-Amzn-Trace-Id": "Root=1-597079de-75fec8453f6fd4812414a4cd", + "X-Forwarded-For": "50.129.117.14, 50.112.234.94", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "queryStringParameters": null, + "pathParameters": null, + "stageVariables": null, + "requestContext": { + "path": "/dev/", + "accountId": "125002137610", + "resourceId": "qdolsr1yhk", + "stage": "dev", + "requestId": "0f2431a2-6d2f-11e7-b799-5152aa497861", + "identity": { + "cognitoIdentityPoolId": null, + "accountId": null, + "cognitoIdentityId": null, + "caller": null, + "apiKey": "", + "sourceIp": "50.129.117.14", + "accessKey": null, + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": null, + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36", + "user": null + }, + "resourcePath": "/", "httpMethod": "POST", - "headers": { - "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", - "Accept-Encoding": "gzip, deflate, br", - "Accept-Language": "en-GB,en-US;q=0.8,en;q=0.6,zh-CN;q=0.4", - "cache-control": "max-age=0", - "CloudFront-Forwarded-Proto": "https", - "CloudFront-Is-Desktop-Viewer": "true", - "CloudFront-Is-Mobile-Viewer": "false", - "CloudFront-Is-SmartTV-Viewer": "false", - "CloudFront-Is-Tablet-Viewer": "false", - "CloudFront-Viewer-Country": "GB", - "content-type": "application/x-www-form-urlencoded", - "Host": "j3ap25j034.execute-api.eu-west-2.amazonaws.com", - "origin": "https://j3ap25j034.execute-api.eu-west-2.amazonaws.com", - "Referer": "https://j3ap25j034.execute-api.eu-west-2.amazonaws.com/dev/", - "upgrade-insecure-requests": "1", - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36", - "Via": "2.0 a3650115c5e21e2b5d133ce84464bea3.cloudfront.net (CloudFront)", - "X-Amz-Cf-Id": "0nDeiXnReyHYCkv8cc150MWCFCLFPbJoTs1mexDuKe2WJwK5ANgv2A==", - "X-Amzn-Trace-Id": "Root=1-597079de-75fec8453f6fd4812414a4cd", - "X-Forwarded-For": "50.129.117.14, 50.112.234.94", - "X-Forwarded-Port": "443", - "X-Forwarded-Proto": "https" - }, - "queryStringParameters": null, - "pathParameters": null, - "stageVariables": null, - "requestContext": { - "path": "/dev/", - "accountId": "125002137610", - "resourceId": "qdolsr1yhk", - "stage": "dev", - "requestId": "0f2431a2-6d2f-11e7-b799-5152aa497861", - "identity": { - "cognitoIdentityPoolId": null, - "accountId": null, - "cognitoIdentityId": null, - "caller": null, - "apiKey": "", - "sourceIp": "50.129.117.14", - "accessKey": null, - "cognitoAuthenticationType": null, - "cognitoAuthenticationProvider": null, - "userArn": null, - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36", - "user": null - }, - "resourcePath": "/", - "httpMethod": "POST", - "apiId": "j3azlsj0c4" - }, - "body": "postcode=LS17FR", - "isBase64Encoded": false + "apiId": "j3azlsj0c4" + }, + "body": "postcode=LS17FR", + "isBase64Encoded": false } ``` @@ -199,6 +202,7 @@ functions: ``` ### Enabling CORS + To set CORS configurations for your HTTP endpoints, simply modify your event configurations as follows: ```yml @@ -263,10 +267,10 @@ functions: Wildcards are accepted. The following example will match all sub-domains of example.com over http: ```yml - cors: - origins: - - http://*.example.com - - http://example2.com +cors: + origins: + - http://*.example.com + - http://example2.com ``` Please note that since you can't send multiple values for [Access-Control-Allow-Origin](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin), this configuration uses a response template to check if the request origin matches one of your provided `origins` and overrides the header with the following code: @@ -276,7 +280,7 @@ Please note that since you can't send multiple values for [Access-Control-Allow- #if($origin == "http://example.com" || $origin == "http://*.amazonaws.com") #set($context.responseOverride.header.Access-Control-Allow-Origin = $origin) #end ``` -Configuring the `cors` property sets [Access-Control-Allow-Origin](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin), [Access-Control-Allow-Headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers), [Access-Control-Allow-Methods](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods),[Access-Control-Allow-Credentials](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials) headers in the CORS preflight response. +Configuring the `cors` property sets [Access-Control-Allow-Origin](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin), [Access-Control-Allow-Headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers), [Access-Control-Allow-Methods](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods),[Access-Control-Allow-Credentials](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials) headers in the CORS preflight response. To enable the `Access-Control-Max-Age` preflight response header, set the `maxAge` property in the `cors` object: @@ -326,17 +330,16 @@ If you want to use CORS with the lambda-proxy integration, remember to include t 'use strict'; module.exports.hello = function(event, context, callback) { + const response = { + statusCode: 200, + headers: { + 'Access-Control-Allow-Origin': '*', // Required for CORS support to work + 'Access-Control-Allow-Credentials': true, // Required for cookies, authorization headers with HTTPS + }, + body: JSON.stringify({ message: 'Hello World!' }), + }; - const response = { - statusCode: 200, - headers: { - "Access-Control-Allow-Origin" : "*", // Required for CORS support to work - "Access-Control-Allow-Credentials" : true // Required for cookies, authorization headers with HTTPS - }, - body: JSON.stringify({ "message": "Hello World!" }) - }; - - callback(null, response); + callback(null, response); }; ``` @@ -371,7 +374,7 @@ functions: ### HTTP Endpoints with Custom Authorizers -Custom Authorizers allow you to run an AWS Lambda Function before your targeted AWS Lambda Function. This is useful for Microservice Architectures or when you simply want to do some Authorization before running your business logic. +Custom Authorizers allow you to run an AWS Lambda Function before your targeted AWS Lambda Function. This is useful for Microservice Architectures or when you simply want to do some Authorization before running your business logic. You can enable Custom Authorizers for your HTTP endpoint by setting the Authorizer in your `http` event to another function in the same service, as shown in the following example: @@ -388,6 +391,7 @@ functions: authorizerFunc: handler: handler.authorizerFunc ``` + Or, if you want to configure the Authorizer with more options, you can turn the `authorizer` property into an object as shown in the following example: @@ -499,9 +503,9 @@ functions: - nickname ``` -### Using asyncronous integration +### Using asynchronous integration -Use `async: true` when integrating a lambda function using [event invocation](https://docs.aws.amazon.com/lambda/latest/dg/API_Invoke.html#SSS-Invoke-request-InvocationType). This lets API Gateway to return immediately with a 200 status code while the lambda continues running. If not othewise speficied integration type will be `AWS`. +Use `async: true` when integrating a lambda function using [event invocation](https://docs.aws.amazon.com/lambda/latest/dg/API_Invoke.html#SSS-Invoke-request-InvocationType). This lets API Gateway to return immediately with a 200 status code while the lambda continues running. If not otherwise specified integration type will be `AWS`. ```yml functions: @@ -511,7 +515,7 @@ functions: - http: path: posts/create method: post - async: true # default is false + async: true # default is false ``` ### Catching Exceptions In Your Lambda Function @@ -574,11 +578,11 @@ provider: name: aws apiKeys: - free: - - myFreeKey - - ${opt:stage}-myFreeKey + - myFreeKey + - ${opt:stage}-myFreeKey - paid: - - myPaidKey - - ${opt:stage}-myPaidKey + - myPaidKey + - ${opt:stage}-myPaidKey usagePlan: - free: quota: @@ -691,9 +695,7 @@ A sample schema contained in `create_request.json` might look something like thi "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "title": "The Root Schema", - "required": [ - "username" - ], + "required": ["username"], "properties": { "username": { "type": "string", @@ -752,51 +754,51 @@ This method is more complicated and involves a lot more configuration of the `ht ```json { - "body": {}, - "method": "GET", - "principalId": "", - "stage": "dev", - "cognitoPoolClaims": { - "sub": "" - }, - "enhancedAuthContext": {}, - "headers": { - "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", - "Accept-Encoding": "gzip, deflate, br", - "Accept-Language": "en-GB,en-US;q=0.8,en;q=0.6,zh-CN;q=0.4", - "CloudFront-Forwarded-Proto": "https", - "CloudFront-Is-Desktop-Viewer": "true", - "CloudFront-Is-Mobile-Viewer": "false", - "CloudFront-Is-SmartTV-Viewer": "false", - "CloudFront-Is-Tablet-Viewer": "false", - "CloudFront-Viewer-Country": "GB", - "Host": "ec5ycylws8.execute-api.us-east-1.amazonaws.com", - "upgrade-insecure-requests": "1", - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36", - "Via": "2.0 f165ce34daf8c0da182681179e863c24.cloudfront.net (CloudFront)", - "X-Amz-Cf-Id": "l06CAg2QsrALeQcLAUSxGXbm8lgMoMIhR2AjKa4AiKuaVnnGsOFy5g==", - "X-Amzn-Trace-Id": "Root=1-5970ef20-3e249c0321b2eef14aa513ae", - "X-Forwarded-For": "94.117.120.169, 116.132.62.73", - "X-Forwarded-Port": "443", - "X-Forwarded-Proto": "https" - }, - "query": {}, - "path": {}, - "identity": { - "cognitoIdentityPoolId": "", - "accountId": "", - "cognitoIdentityId": "", - "caller": "", - "apiKey": "", - "sourceIp": "94.197.120.169", - "accessKey": "", - "cognitoAuthenticationType": "", - "cognitoAuthenticationProvider": "", - "userArn": "", - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36", - "user": "" - }, - "stageVariables": {} + "body": {}, + "method": "GET", + "principalId": "", + "stage": "dev", + "cognitoPoolClaims": { + "sub": "" + }, + "enhancedAuthContext": {}, + "headers": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, br", + "Accept-Language": "en-GB,en-US;q=0.8,en;q=0.6,zh-CN;q=0.4", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "GB", + "Host": "ec5ycylws8.execute-api.us-east-1.amazonaws.com", + "upgrade-insecure-requests": "1", + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36", + "Via": "2.0 f165ce34daf8c0da182681179e863c24.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "l06CAg2QsrALeQcLAUSxGXbm8lgMoMIhR2AjKa4AiKuaVnnGsOFy5g==", + "X-Amzn-Trace-Id": "Root=1-5970ef20-3e249c0321b2eef14aa513ae", + "X-Forwarded-For": "94.117.120.169, 116.132.62.73", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "query": {}, + "path": {}, + "identity": { + "cognitoIdentityPoolId": "", + "accountId": "", + "cognitoIdentityId": "", + "caller": "", + "apiKey": "", + "sourceIp": "94.197.120.169", + "accessKey": "", + "cognitoAuthenticationType": "", + "cognitoAuthenticationProvider": "", + "userArn": "", + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36", + "user": "" + }, + "stageVariables": {} } ``` @@ -866,9 +868,10 @@ You can then access the query string `https://example.com/dev/whatever?bar=123` If you want to spread a string into multiple lines, you can use the `>` or `|` syntax, but the following strings have to be all indented with the same amount, [read more about `>` syntax](http://stackoverflow.com/questions/3790454/in-yaml-how-do-i-break-a-string-over-multiple-lines). #### Pass Through Behavior -API Gateway provides multiple ways to handle requests where the Content-Type header does not match any of the specified mapping templates. When this happens, the request payload will either be passed through the integration request *without transformation* or rejected with a `415 - Unsupported Media Type`, depending on the configuration. -You can define this behavior as follows (if not specified, a value of **NEVER** will be used): +API Gateway provides multiple ways to handle requests where the Content-Type header does not match any of the specified mapping templates. When this happens, the request payload will either be passed through the integration request _without transformation_ or rejected with a `415 - Unsupported Media Type`, depending on the configuration. + +You can define this behaviour as follows (if not specified, a value of **NEVER** will be used): ```yml functions: @@ -885,11 +888,11 @@ functions: There are 3 available options: -|Value | Passed Through When | Rejected When | -|----------------- | --------------------------------------------- | ----------------------------------------------------------------------- | -|NEVER | Never | No templates defined or Content-Type does not match a defined template | -|WHEN_NO_MATCH | Content-Type does not match defined template | Never | -|WHEN_NO_TEMPLATES | No templates were defined | One or more templates defined, but Content-Type does not match | +| Value | Passed Through When | Rejected When | +| ----------------- | -------------------------------------------- | ---------------------------------------------------------------------- | +| NEVER | Never | No templates defined or Content-Type does not match a defined template | +| WHEN_NO_MATCH | Content-Type does not match defined template | Never | +| WHEN_NO_TEMPLATES | No templates were defined | One or more templates defined, but Content-Type does not match | See the [api gateway documentation](https://docs.aws.amazon.com/apigateway/latest/developerguide/integration-passthrough-behaviors.html) for detailed descriptions of these options. @@ -953,26 +956,26 @@ the `${file(templatefile)}` syntax. Serverless ships with default status codes you can use to e.g. signal that a resource could not be found (404) or that the user is not authorized to perform the action (401). Those status codes are regex definitions that will be added to your API Gateway configuration. -***Note:*** Status codes as documented in this chapter relate to `lambda` integration method (as documented at the top of this page). If using default integration method `lambda-proxy` object with status code and message should be returned as in the example below: +**_Note:_** Status codes as documented in this chapter relate to `lambda` integration method (as documented at the top of this page). If using default integration method `lambda-proxy` object with status code and message should be returned as in the example below: ```javascript module.exports.hello = (event, context, callback) => { - callback(null, { statusCode: 404, body: "Not found", headers: { "Content-Type": "text/plain" } }); -} + callback(null, { statusCode: 404, body: 'Not found', headers: { 'Content-Type': 'text/plain' } }); +}; ``` #### Available Status Codes -| Status Code | Meaning | -| --- | --- | -| 400 | Bad Request | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not Found | -| 422 | Unprocessable Entity | -| 500 | Internal Server Error | -| 502 | Bad Gateway | -| 504 | Gateway Timeout | +| Status Code | Meaning | +| ----------- | --------------------- | +| 400 | Bad Request | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not Found | +| 422 | Unprocessable Entity | +| 500 | Internal Server Error | +| 502 | Bad Gateway | +| 504 | Gateway Timeout | #### Using Status Codes @@ -984,7 +987,7 @@ Here's an example which shows you how you can raise a 404 HTTP status from withi ```javascript module.exports.hello = (event, context, callback) => { callback(new Error('[404] Not found')); -} +}; ``` #### Custom Status Codes @@ -1009,13 +1012,13 @@ functions: Content-Type: "'text/html'" template: $input.path('$') statusCodes: - 201: - pattern: '' # Default response method - 409: - pattern: '.*"statusCode":409,.*' # JSON response - template: $input.path("$.errorMessage") # JSON return object - headers: - Content-Type: "'application/json+hal'" + 201: + pattern: '' # Default response method + 409: + pattern: '.*"statusCode":409,.*' # JSON response + template: $input.path("$.errorMessage") # JSON return object + headers: + Content-Type: "'application/json+hal'" ``` You can also create varying response templates for each code and content type by creating an object with the key as the content type @@ -1034,15 +1037,15 @@ functions: Content-Type: "'text/html'" template: $input.path('$') statusCodes: - 201: - pattern: '' # Default response method - 409: - pattern: '.*"statusCode":409,.*' # JSON response - template: - application/json: $input.path("$.errorMessage") # JSON return object - application/xml: $input.path("$.body.errorMessage") # XML return object - headers: - Content-Type: "'application/json+hal'" + 201: + pattern: '' # Default response method + 409: + pattern: '.*"statusCode":409,.*' # JSON response + template: + application/json: $input.path("$.errorMessage") # JSON return object + application/xml: $input.path("$.body.errorMessage") # XML return object + headers: + Content-Type: "'application/json+hal'" ``` ## Setting an HTTP Proxy on API Gateway @@ -1053,8 +1056,7 @@ one for method. These two templates will work together to construct your proxy. ```yml service: service-name provider: aws -functions: - ... +functions: ... resources: Resources: @@ -1103,14 +1105,12 @@ provider: apiGateway: restApiId: xxxxxxxxxx # REST API resource ID. Default is generated by the framework restApiRootResourceId: xxxxxxxxxx # Root resource, represent as / path + websocketApiId: xxxxxxxxxx # Websocket API resource ID. Default is generated by the framewok description: Some Description # optional - description of deployment history -functions: - ... - +functions: ... ``` - If your application has many nested paths, you might also want to break them out into smaller services. ```yml @@ -1119,6 +1119,7 @@ provider: apiGateway: restApiId: xxxxxxxxxx restApiRootResourceId: xxxxxxxxxx + websocketApiId: xxxxxxxxxx description: Some Description functions: @@ -1136,6 +1137,7 @@ provider: apiGateway: restApiId: xxxxxxxxxx restApiRootResourceId: xxxxxxxxxx + websocketApiId: xxxxxxxxxx description: Some Description functions: @@ -1155,13 +1157,12 @@ provider: apiGateway: restApiId: xxxxxxxxxx restApiRootResourceId: xxxxxxxxxx + websocketApiId: xxxxxxxxxx description: Some Description restApiResources: - /posts: xxxxxxxxxx - -functions: - ... + posts: xxxxxxxxxx +functions: ... ``` ```yml @@ -1170,13 +1171,12 @@ provider: apiGateway: restApiId: xxxxxxxxxx restApiRootResourceId: xxxxxxxxxx + websocketApiId: xxxxxxxxxx description: Some Description restApiResources: /posts: xxxxxxxxxx -functions: - ... - +functions: ... ``` You can define more than one path resource, but by default, Serverless will generate them from the root resource. @@ -1188,12 +1188,12 @@ provider: apiGateway: restApiId: xxxxxxxxxx # restApiRootResourceId: xxxxxxxxxx # Optional + websocketApiId: xxxxxxxxxx description: Some Description restApiResources: /posts: xxxxxxxxxx /categories: xxxxxxxxx - functions: listPosts: handler: posts.list @@ -1208,19 +1208,18 @@ functions: - http: method: get path: /categories - ``` ### Easiest and CI/CD friendly example of using shared API Gateway and API Resources. -You can define your API Gateway resource in its own service and export the `restApiId` and `restApiRootResourceId` using cloudformation cross-stack references. +You can define your API Gateway resource in its own service and export the `restApiId`, `restApiRootResourceId` and `websocketApiId` using cloudformation cross-stack references. ```yml service: my-api provider: name: aws - runtime: nodejs8.10 + runtime: nodejs10.x stage: dev region: eu-west-2 @@ -1231,6 +1230,13 @@ resources: Properties: Name: MyApiGW + MyWebsocketApi: + Type: AWS::ApiGatewayV2::Api + Properties: + Name: MyWebsocketApi + ProtocolType: WEBSOCKET + RouteSelectionExpression: '$request.body.action' + Outputs: apiGatewayRestApiId: Value: @@ -1245,9 +1251,15 @@ resources: - RootResourceId Export: Name: MyApiGateway-rootResourceId + + websocketApiId: + Value: + Ref: MyWebsocketApi + Export: + Name: MyApiGateway-websocketApiId ``` -This creates API gateway and then exports the `restApiId` and `rootResourceId` values using cloudformation cross stack output. +This creates API gateway and then exports the `restApiId`, `rootResourceId` and `websocketApiId` values using cloudformation cross stack output. We will import this and reference in future services. ```yml @@ -1259,10 +1271,12 @@ provider: 'Fn::ImportValue': MyApiGateway-restApiId restApiRootResourceId: 'Fn::ImportValue': MyApiGateway-rootResourceId + websocketApiId: + 'Fn::ImportValue': MyApiGateway-websocketApiId -functions: - service-a-functions +functions: service-a-functions ``` + ```yml service: service-b @@ -1272,13 +1286,15 @@ provider: 'Fn::ImportValue': MyApiGateway-restApiId restApiRootResourceId: 'Fn::ImportValue': MyApiGateway-rootResourceId + websocketApiId: + 'Fn::ImportValue': MyApiGateway-websocketApiId -functions: - service-b-functions +functions: service-b-functions ``` You can use this method to share your API Gateway across services in same region. Read about this limitation [here](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-importvalue.html). +**Note:** We've noticed you can't use provider.tags together with `Fn::ImportValue` for `restApiId` and `restApiRootResourceId`. Doing so won't resolve the imported value, and therefore returns an error. ### Manually Configuring shared API Gateway @@ -1314,7 +1330,6 @@ functions: arn: xxxxxxxxxxxxxxxxx #cognito/custom authorizer arn ``` - ```yml service: service-d @@ -1355,6 +1370,8 @@ functions: type: COGNITO_USER_POOLS # TOKEN or REQUEST or COGNITO_USER_POOLS, same as AWS Cloudformation documentation authorizerId: Ref: ApiGatewayAuthorizer # or hard-code Authorizer ID + scopes: # Optional - List of Oauth2 scopes when type is COGNITO_USER_POOLS + - myapp/myscope deleteUser: ... @@ -1390,19 +1407,18 @@ Resource policies are policy documents that are used to control the invocation o ```yml provider: name: aws - runtime: nodejs6.10 + runtime: nodejs10.x resourcePolicy: - Effect: Allow - Principal: "*" + Principal: '*' Action: execute-api:Invoke Resource: - execute-api:/*/*/* Condition: IpAddress: aws:SourceIp: - - "123.123.123.123" - + - '123.123.123.123' ``` ## Compression @@ -1472,3 +1488,20 @@ provider: ``` The log streams will be generated in a dedicated log group which follows the naming schema `/aws/api-gateway/{service}-{stage}`. + +By default, API Gateway access logs will use the following format: + +``` +'requestId: $context.requestId, ip: $context.identity.sourceIp, caller: $context.identity.caller, user: $context.identity.user, requestTime: $context.requestTime, httpMethod: $context.httpMethod, resourcePath: $context.resourcePath, status: $context.status, protocol: $context.protocol, responseLength: $context.responseLength' +``` + +You can specify your own [format for API Gateway Access Logs](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-logging.html#apigateway-cloudwatch-log-formats) by including your preferred string in the `format` property: + +```yml +# serverless.yml +provider: + name: aws + logs: + restApi: + format: '{ "requestId":"$context.requestId", "ip": "$context.identity.sourceIp" }' +``` diff --git a/docs/providers/aws/events/cloudwatch-event.md b/docs/providers/aws/events/cloudwatch-event.md index fc9d32dfb..d4acdc254 100644 --- a/docs/providers/aws/events/cloudwatch-event.md +++ b/docs/providers/aws/events/cloudwatch-event.md @@ -1,13 +1,15 @@ + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/events/cloudwatch-event) + # CloudWatch Event @@ -25,9 +27,9 @@ functions: - cloudwatchEvent: event: source: - - "aws.ec2" + - 'aws.ec2' detail-type: - - "EC2 Instance State-change Notification" + - 'EC2 Instance State-change Notification' detail: state: - pending @@ -47,9 +49,9 @@ functions: - cloudwatchEvent: event: source: - - "aws.ec2" + - 'aws.ec2' detail-type: - - "EC2 Instance State-change Notification" + - 'EC2 Instance State-change Notification' detail: state: - pending @@ -68,9 +70,9 @@ functions: - cloudwatchEvent: event: source: - - "aws.ec2" + - 'aws.ec2' detail-type: - - "EC2 Instance State-change Notification" + - 'EC2 Instance State-change Notification' detail: state: - pending @@ -82,9 +84,9 @@ functions: - cloudwatchEvent: event: source: - - "aws.ec2" + - 'aws.ec2' detail-type: - - "EC2 Instance State-change Notification" + - 'EC2 Instance State-change Notification' detail: state: - pending @@ -92,9 +94,9 @@ functions: - cloudwatchEvent: event: source: - - "aws.ec2" + - 'aws.ec2' detail-type: - - "EC2 Instance State-change Notification" + - 'EC2 Instance State-change Notification' detail: state: - pending @@ -117,9 +119,9 @@ functions: description: 'CloudWatch Event triggered on EC2 Instance pending state' event: source: - - "aws.ec2" + - 'aws.ec2' detail-type: - - "EC2 Instance State-change Notification" + - 'EC2 Instance State-change Notification' detail: state: - pending @@ -138,9 +140,9 @@ functions: name: 'my-cloudwatch-event-name' event: source: - - "aws.ec2" + - 'aws.ec2' detail-type: - - "EC2 Instance State-change Notification" + - 'EC2 Instance State-change Notification' detail: state: - pending diff --git a/docs/providers/aws/events/cloudwatch-log.md b/docs/providers/aws/events/cloudwatch-log.md index 7942545f0..d5021973b 100644 --- a/docs/providers/aws/events/cloudwatch-log.md +++ b/docs/providers/aws/events/cloudwatch-log.md @@ -1,13 +1,15 @@ + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/events/cloudwatch-log) + # CloudWatch Log diff --git a/docs/providers/aws/events/cognito-user-pool.md b/docs/providers/aws/events/cognito-user-pool.md index 0d2662848..708d72aeb 100644 --- a/docs/providers/aws/events/cognito-user-pool.md +++ b/docs/providers/aws/events/cognito-user-pool.md @@ -1,13 +1,15 @@ + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/events/cognito-user-pool) + # Cognito User Pool @@ -114,5 +116,4 @@ resources: ``` [aws-triggers-guide]: http://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools-working-with-aws-lambda-triggers.html -[aws-triggers-list]: -https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cognito-userpool-lambdaconfig.html +[aws-triggers-list]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cognito-userpool-lambdaconfig.html diff --git a/docs/providers/aws/events/iot.md b/docs/providers/aws/events/iot.md index c927ff832..cc9d06283 100644 --- a/docs/providers/aws/events/iot.md +++ b/docs/providers/aws/events/iot.md @@ -1,13 +1,15 @@ + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/events/iot) + # IoT @@ -51,9 +53,9 @@ functions: handler: myIoT.handler events: - iot: - name: "myIotEvent" + name: 'myIotEvent' sql: "SELECT * FROM 'some_topic'" - description: "My IoT Event Description" + description: 'My IoT Event Description' ``` ## Specify SQL Versions @@ -67,5 +69,5 @@ functions: events: - iot: sql: "SELECT * FROM 'some_topic'" - sqlVersion: "beta" + sqlVersion: 'beta' ``` diff --git a/docs/providers/aws/events/s3.md b/docs/providers/aws/events/s3.md index 16689ca45..76495866d 100644 --- a/docs/providers/aws/events/s3.md +++ b/docs/providers/aws/events/s3.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/events/s3) + # S3 @@ -78,6 +80,7 @@ functions: If you need to configure the bucket itself, you'll need to create the bucket and the Lambda Permission manually in the Resources section while paying attention to some of the logical IDs. This relies on the Serverless naming convention. See the [Serverless Resource Reference](../guide/resources/#aws-cloudformation-resource-reference) for details. These are the logical IDs that require your attention: + - The logical ID of the custom bucket in the Resources section needs to match the bucket name in the S3 event after the Serverless naming convention is applied to it. - The Lambda Permission's logical ID needs to match the Serverless naming convention for Lambda Permissions for S3 events. - The `FunctionName` in the Lambda Permission configuration needs to match the logical ID generated for the target Lambda function as determined by the Serverless naming convention. @@ -99,15 +102,35 @@ resources: BucketName: my-custom-bucket-name # add additional custom bucket configuration here ResizeLambdaPermissionPhotosS3: - Type: "AWS::Lambda::Permission" + Type: 'AWS::Lambda::Permission' Properties: FunctionName: - "Fn::GetAtt": + 'Fn::GetAtt': - ResizeLambdaFunction - Arn - Principal: "s3.amazonaws.com" - Action: "lambda:InvokeFunction" + Principal: 's3.amazonaws.com' + Action: 'lambda:InvokeFunction' SourceAccount: Ref: AWS::AccountId - SourceArn: "arn:aws:s3:::my-custom-bucket-name" + SourceArn: 'arn:aws:s3:::my-custom-bucket-name' +``` + +## Using existing buckets + +Sometimes you might want to attach Lambda functions to existing S3 buckets. In that case you just need to set the `existing` event configuration property to `true`. All the other config parameters can also be used on existing buckets: + +**NOTE:** Using the `existing` config will add an additional Lambda function and IAM Role to your stack. The Lambda function backs-up the Custom S3 Resource which is used to support existing S3 buckets. + +```yaml +functions: + users: + handler: users.handler + events: + - s3: + bucket: legacy-photos + event: s3:ObjectCreated:* + rules: + - prefix: uploads/ + - suffix: .jpg + existing: true ``` diff --git a/docs/providers/aws/events/schedule.md b/docs/providers/aws/events/schedule.md index 162c276a9..5c3a09774 100644 --- a/docs/providers/aws/events/schedule.md +++ b/docs/providers/aws/events/schedule.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/events/schedule) + # Schedule diff --git a/docs/providers/aws/events/sns.md b/docs/providers/aws/events/sns.md index 9e4ab66ba..738f5e9b4 100644 --- a/docs/providers/aws/events/sns.md +++ b/docs/providers/aws/events/sns.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/events/sns) + # SNS @@ -69,11 +71,11 @@ functions: - sns: arn: Fn::Join: - - ":" - - - "arn:aws:sns" - - Ref: "AWS::Region" - - Ref: "AWS::AccountId" - - "MyCustomTopic" + - ':' + - - 'arn:aws:sns' + - Ref: 'AWS::Region' + - Ref: 'AWS::AccountId' + - 'MyCustomTopic' topicName: MyCustomTopic ``` diff --git a/docs/providers/aws/events/sqs.md b/docs/providers/aws/events/sqs.md index 77c5041f9..4ead9acf7 100644 --- a/docs/providers/aws/events/sqs.md +++ b/docs/providers/aws/events/sqs.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/events/sqs) + # SQS Queues @@ -37,7 +39,7 @@ functions: - sqs: arn: Fn::Join: - - ":" + - ':' - - arn - aws - sqs diff --git a/docs/providers/aws/events/streams.md b/docs/providers/aws/events/streams.md index 2fc9e0b4c..4f5491718 100644 --- a/docs/providers/aws/events/streams.md +++ b/docs/providers/aws/events/streams.md @@ -7,14 +7,17 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/events/streams) + # DynamoDB / Kinesis Streams This setup specifies that the `compute` function should be triggered whenever: - 1. the corresponding DynamoDB table is modified (e.g. a new entry is added). - 2. the Lambda checkpoint has not reached the end of the Kinesis stream (e.g. a new record is added). + +1. the corresponding DynamoDB table is modified (e.g. a new entry is added). +2. the Lambda checkpoint has not reached the end of the Kinesis stream (e.g. a new record is added). The ARN for the stream can be specified as a string, the reference to the ARN of a resource by logical ID, or the import of an ARN that was exported by a different service or CloudFormation stack. diff --git a/docs/providers/aws/events/websocket.md b/docs/providers/aws/events/websocket.md index 320df6bc6..b448748d2 100644 --- a/docs/providers/aws/events/websocket.md +++ b/docs/providers/aws/events/websocket.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/events/websocket) + # Websocket @@ -46,10 +48,11 @@ functions: ## Routes The API-Gateway provides [4 types of routes](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-overview.html) which relate to the lifecycle of a ws-client: -* `$connect` called on connect of a ws-client -* `$disconnect` called on disconnect of a ws-client (may not be called in some situations) -* `$default` called if there is no handler to use for the event -* custom routes - called if the route name is specified for a handler + +- `$connect` called on connect of a ws-client +- `$disconnect` called on disconnect of a ws-client (may not be called in some situations) +- `$default` called if there is no handler to use for the event +- custom routes - called if the route name is specified for a handler ### Example serverless.yaml @@ -60,7 +63,7 @@ service: serverless-ws-test provider: name: aws - runtime: nodejs8.10 + runtime: nodejs10.x websocketsApiName: custom-websockets-api-name websocketsApiRouteSelectionExpression: $request.body.action # custom routes are selected by the value of the action property in the body @@ -84,6 +87,7 @@ functions: ``` ## Using Authorizers + You can enable an authorizer for your connect route by specifying the `authorizer` key in the websocket event definition. **Note:** AWS only supports authorizers for the `$connect` route. @@ -114,7 +118,6 @@ functions: By default, the `identitySource` property is set to `route.request.header.Auth`, meaning that your request must include the auth token in the `Auth` header of the request. You can overwrite this by specifying your own `identitySource` configuration: - ```yml functions: connectHandler: @@ -127,10 +130,11 @@ functions: identitySource: - 'route.request.header.Auth' - 'route.request.querystring.Auth' - + auth: handler: handler.auth ``` + With the above configuration, you can now must pass the auth token in both the `Auth` query string as well as the `Auth` header. You can also supply an ARN instead of the name when using the object syntax for the authorizer: @@ -147,12 +151,13 @@ functions: identitySource: - 'route.request.header.Auth' - 'route.request.querystring.Auth' - + auth: handler: handler.auth ``` ## Send a message to a ws-client + To send a message to a ws-client the [@connection](https://docs.amazonaws.cn/en_us/apigateway/latest/developerguide/apigateway-how-to-call-websocket-api-connections.html) command is used. It uses the URL of the websocket API and most importantly the `connectionId` of the ws-client's connection. If you want to send a message to a ws-client from another function, you need this `connectionId` to address the ws-client. @@ -160,29 +165,50 @@ It uses the URL of the websocket API and most importantly the `connectionId` of Example on how to respond with the complete `event` to the same ws-client: ```js -const sendMessageToClient = (url, connectionId, payload) => new Promise((resolve, reject) => { - const apigatewaymanagementapi = new AWS.ApiGatewayManagementApi({apiVersion: '2018-11-29', endpoint: url}); - apigatewaymanagementapi.postToConnection({ - ConnectionId: connectionId, // connectionId of the receiving ws-client - Data: JSON.stringify(payload), - }, (err, data) => { - if (err) { - console.log('err is', err); - reject(err); - } - resolve(data); +const sendMessageToClient = (url, connectionId, payload) => + new Promise((resolve, reject) => { + const apigatewaymanagementapi = new AWS.ApiGatewayManagementApi({ + apiVersion: '2018-11-29', + endpoint: url, + }); + apigatewaymanagementapi.postToConnection( + { + ConnectionId: connectionId, // connectionId of the receiving ws-client + Data: JSON.stringify(payload), + }, + (err, data) => { + if (err) { + console.log('err is', err); + reject(err); + } + resolve(data); + } + ); }); -}); module.exports.defaultHandler = async (event, context) => { const domain = event.requestContext.domainName; const stage = event.requestContext.stage; - const connectionId = event.requestContext.connectionId; + const connectionId = event.requestContext.connectionId; const callbackUrlForAWS = util.format(util.format('https://%s/%s', domain, stage)); //construct the needed url await sendMessageToClient(callbackUrlForAWS, connectionId, event); return { - statusCode: 200 + statusCode: 200, }; -} +}; ``` + +## Logs + +Use the following configuration to enable Websocket logs: + +```yml +# serverless.yml +provider: + name: aws + logs: + websocket: true +``` + +The log streams will be generated in a dedicated log group which follows the naming schema `/aws/websocket/{service}-{stage}`. diff --git a/docs/providers/aws/examples/README.md b/docs/providers/aws/examples/README.md index bc23fc706..9c58f8cbb 100644 --- a/docs/providers/aws/examples/README.md +++ b/docs/providers/aws/examples/README.md @@ -5,26 +5,28 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/examples/) + # Serverless AWS Lambda Examples Have an example? Submit a PR or [open an issue](https://github.com/serverless/examples/issues). ⚡️ -| Example | Runtime | -|:--------------------------- |:-----| -| [Aws Auth0 Api Gateway](https://serverless.com/examples/aws-node-auth0-custom-authorizers-api/)
    Demonstration of protecting API gateway endpoints with auth0 | nodeJS | -| [Env Variables Encrypted In A File](https://serverless.com/examples/aws-node-env-variables-encrypted-in-a-file/)
    Serverless example managing secrets in an encrypted file | nodeJS | -| [Serverless Node Env Variables](https://serverless.com/examples/aws-node-env-variables/)
    This example demonstrates how to use environment variables for AWS Lambdas. | nodeJS | -| [Fetch File And Store In S3](https://serverless.com/examples/aws-node-fetch-file-and-store-in-s3/)
    Fetch an image from remote source (URL) and then upload the image to a S3 bucket. | nodeJS | -| [Function Compiled With Babel](https://serverless.com/examples/aws-node-function-compiled-with-babel/)
    Demonstrating how to compile all your code with Babel | nodeJS | -| [Serverless Rest With Dynamodb](https://serverless.com/examples/aws-node-rest-api-with-dynamodb/)
    Serverless CRUD service exposing a REST HTTP interface | nodeJS | -| [Serverless Aws Cron Job Example](https://serverless.com/examples/aws-node-scheduled-cron/)
    Example of creating a function that runs as a cron job using the serverless `schedule` event | nodeJS | -| [Aws Node Serve Dynamic Html Via Http Endpoint](https://serverless.com/examples/aws-node-serve-dynamic-html-via-http-endpoint/)
    Hookup an AWS API Gateway endpoint to a Lambda function to render HTML on a `GET` request | nodeJS | -| [Aws Node Serve Dynamic Html Via Http Endpoint](https://serverless.com/examples/aws-node-simple-http-endpoint/)
    Example demonstrates how to setup a simple HTTP GET endpoint | nodeJS | -| [Single Page App Via Cloudfront](https://serverless.com/examples/aws-node-single-page-app-via-cloudfront/)
    Demonstrating how to deploy a Single Page Application with Serverless | nodeJS | -| [Serverless Data Pipeline](https://serverless.com/examples/aws-node-text-analysis-via-sns-post-processing/)
    Example demonstrates how to setup a simple data processing pipeline | nodeJS | -| [Aws Python Simple Http Endpoint](https://serverless.com/examples/aws-python-simple-http-endpoint/)
    Example demonstrates how to setup a simple HTTP GET endpoint with python | python | +| Example | Runtime | +| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :------ | +| [Aws Auth0 Api Gateway](https://serverless.com/examples/aws-node-auth0-custom-authorizers-api/)
    Demonstration of protecting API gateway endpoints with auth0 | nodeJS | +| [Env Variables Encrypted In A File](https://serverless.com/examples/aws-node-env-variables-encrypted-in-a-file/)
    Serverless example managing secrets in an encrypted file | nodeJS | +| [Serverless Node Env Variables](https://serverless.com/examples/aws-node-env-variables/)
    This example demonstrates how to use environment variables for AWS Lambdas. | nodeJS | +| [Fetch File And Store In S3](https://serverless.com/examples/aws-node-fetch-file-and-store-in-s3/)
    Fetch an image from remote source (URL) and then upload the image to a S3 bucket. | nodeJS | +| [Function Compiled With Babel](https://serverless.com/examples/aws-node-function-compiled-with-babel/)
    Demonstrating how to compile all your code with Babel | nodeJS | +| [Serverless Rest With Dynamodb](https://serverless.com/examples/aws-node-rest-api-with-dynamodb/)
    Serverless CRUD service exposing a REST HTTP interface | nodeJS | +| [Serverless Aws Cron Job Example](https://serverless.com/examples/aws-node-scheduled-cron/)
    Example of creating a function that runs as a cron job using the serverless `schedule` event | nodeJS | +| [Aws Node Serve Dynamic Html Via Http Endpoint](https://serverless.com/examples/aws-node-serve-dynamic-html-via-http-endpoint/)
    Hookup an AWS API Gateway endpoint to a Lambda function to render HTML on a `GET` request | nodeJS | +| [Aws Node Serve Dynamic Html Via Http Endpoint](https://serverless.com/examples/aws-node-simple-http-endpoint/)
    Example demonstrates how to setup a simple HTTP GET endpoint | nodeJS | +| [Single Page App Via Cloudfront](https://serverless.com/examples/aws-node-single-page-app-via-cloudfront/)
    Demonstrating how to deploy a Single Page Application with Serverless | nodeJS | +| [Serverless Data Pipeline](https://serverless.com/examples/aws-node-text-analysis-via-sns-post-processing/)
    Example demonstrates how to setup a simple data processing pipeline | nodeJS | +| [Aws Python Simple Http Endpoint](https://serverless.com/examples/aws-python-simple-http-endpoint/)
    Example demonstrates how to setup a simple HTTP GET endpoint with python | python | If you have questions, join the [chat in gitter](https://gitter.im/serverless/serverless) or [post over on the forums](https://forum.serverless.com/) diff --git a/docs/providers/aws/examples/hello-world/README.md b/docs/providers/aws/examples/hello-world/README.md index 378b05677..41e20235f 100644 --- a/docs/providers/aws/examples/hello-world/README.md +++ b/docs/providers/aws/examples/hello-world/README.md @@ -6,7 +6,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/examples/hello-world/) + # Hello World Serverless Example 🌍 @@ -15,10 +17,10 @@ Welcome to the Hello World example. Pick your language of choice: -* [JavaScript](./node) -* [Python](./python) -* [C#](./csharp) -* [F#](./fsharp) -* [Go](./go) +- [JavaScript](./node) +- [Python](./python) +- [C#](./csharp) +- [F#](./fsharp) +- [Go](./go) [View all examples](https://www.serverless.com/framework/docs/providers/aws/examples/) diff --git a/docs/providers/aws/examples/hello-world/csharp/README.md b/docs/providers/aws/examples/hello-world/csharp/README.md index 499d24224..397ae33c2 100644 --- a/docs/providers/aws/examples/hello-world/csharp/README.md +++ b/docs/providers/aws/examples/hello-world/csharp/README.md @@ -6,7 +6,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/examples/hello-world/csharp/) + # Hello World C# Example @@ -57,7 +59,6 @@ sls deploy This will deploy your function to AWS Lambda based on the settings in `serverless.yml`. - ## 4. Invoke deployed function ``` diff --git a/docs/providers/aws/examples/hello-world/fsharp/README.md b/docs/providers/aws/examples/hello-world/fsharp/README.md index f1e14c97d..b49d4ec2e 100644 --- a/docs/providers/aws/examples/hello-world/fsharp/README.md +++ b/docs/providers/aws/examples/hello-world/fsharp/README.md @@ -1,79 +1,81 @@ - - - -### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/examples/hello-world/fsharp/) - - -# Hello World F# Example - -Make sure `serverless` is installed. [See installation guide](../../../guide/installation.md). - -Once installed the Serverless CLI can be called with `serverless` or the shorthand `sls` command. - -``` -$ sls - -Commands -* You can run commands with "serverless" or the shortcut "sls" -* Pass "--verbose" to this command to get in-depth plugin info -* Pass "--no-color" to disable CLI colors -* Pass "--help" after any for contextual help -``` - -## 1. Create a service - -``` -sls create --template aws-fsharp --path myService -``` - -Using the `create` command we can specify one of the available [templates](https://serverless.com/framework/docs/providers/aws/cli-reference/create#available-templates). For this example use aws-fsharp with the `--template` or shorthand `-t` flag. - -The `--path` or shorthand `-p` is the location to be created with the template service files. Change directories into this new folder. - -## 2. Build using .NET Core 2.X CLI tools and create zip package - -``` -# Linux or Mac OS -./build.sh -``` - -``` -# Windows PowerShell -./build.cmd -``` - -## 3. Deploy - -``` -sls deploy -``` - -This will deploy your function to AWS Lambda based on the settings in `serverless.yml`. - -## 4. Invoke deployed function - -``` -sls invoke -f hello -``` - -Invoke deployed function with command `invoke` and `--function` or shorthand `-f`. - -In your terminal window you should see the response from AWS Lambda. - -```bash -{ - "Message": "Go Serverless v1.0! Your function executed successfully!", - "Request": { - "Key1": null, - "Key2": null, - "Key3": null - } -} -``` - -Congrats you have deployed and ran your Hello World function! + + + + +### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/examples/hello-world/fsharp/) + + + +# Hello World F# Example + +Make sure `serverless` is installed. [See installation guide](../../../guide/installation.md). + +Once installed the Serverless CLI can be called with `serverless` or the shorthand `sls` command. + +``` +$ sls + +Commands +* You can run commands with "serverless" or the shortcut "sls" +* Pass "--verbose" to this command to get in-depth plugin info +* Pass "--no-color" to disable CLI colors +* Pass "--help" after any for contextual help +``` + +## 1. Create a service + +``` +sls create --template aws-fsharp --path myService +``` + +Using the `create` command we can specify one of the available [templates](https://serverless.com/framework/docs/providers/aws/cli-reference/create#available-templates). For this example use aws-fsharp with the `--template` or shorthand `-t` flag. + +The `--path` or shorthand `-p` is the location to be created with the template service files. Change directories into this new folder. + +## 2. Build using .NET Core 2.X CLI tools and create zip package + +``` +# Linux or Mac OS +./build.sh +``` + +``` +# Windows PowerShell +./build.cmd +``` + +## 3. Deploy + +``` +sls deploy +``` + +This will deploy your function to AWS Lambda based on the settings in `serverless.yml`. + +## 4. Invoke deployed function + +``` +sls invoke -f hello +``` + +Invoke deployed function with command `invoke` and `--function` or shorthand `-f`. + +In your terminal window you should see the response from AWS Lambda. + +```bash +{ + "Message": "Go Serverless v1.0! Your function executed successfully!", + "Request": { + "Key1": null, + "Key2": null, + "Key3": null + } +} +``` + +Congrats you have deployed and ran your Hello World function! diff --git a/docs/providers/aws/examples/hello-world/go/README.md b/docs/providers/aws/examples/hello-world/go/README.md index 05a233a33..e01d6e533 100644 --- a/docs/providers/aws/examples/hello-world/go/README.md +++ b/docs/providers/aws/examples/hello-world/go/README.md @@ -6,7 +6,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/examples/hello-world/go/) + # Hello World Go Example @@ -63,7 +65,7 @@ Change directories into 'myService' folder and you can see this project has 2 ha │ └── main.go ``` -This because a `main()` function is required as entry point for each handler executable. +This because a `main()` function is required as entry point for each handler executable. ## 2. Build using go build to create static binaries diff --git a/docs/providers/aws/examples/hello-world/node/README.md b/docs/providers/aws/examples/hello-world/node/README.md index fb927951f..dbcc620a0 100644 --- a/docs/providers/aws/examples/hello-world/node/README.md +++ b/docs/providers/aws/examples/hello-world/node/README.md @@ -6,7 +6,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/examples/hello-world/node/) + # Hello World Node.js Example diff --git a/docs/providers/aws/examples/hello-world/node/handler.js b/docs/providers/aws/examples/hello-world/node/handler.js index 12a311f42..6c0df8241 100644 --- a/docs/providers/aws/examples/hello-world/node/handler.js +++ b/docs/providers/aws/examples/hello-world/node/handler.js @@ -1,7 +1,7 @@ 'use strict'; // Your function handler -module.exports.helloWorldHandler = function (event, context, callback) { +module.exports.helloWorldHandler = function(event, context, callback) { const message = { message: 'Hello World', event, diff --git a/docs/providers/aws/examples/hello-world/node/serverless.yml b/docs/providers/aws/examples/hello-world/node/serverless.yml index 97b220d9f..4d44fe7c3 100644 --- a/docs/providers/aws/examples/hello-world/node/serverless.yml +++ b/docs/providers/aws/examples/hello-world/node/serverless.yml @@ -3,7 +3,7 @@ service: hello-world # Service Name provider: name: aws - runtime: nodejs6.10 + runtime: nodejs10.x functions: helloWorld: diff --git a/docs/providers/aws/examples/hello-world/python/README.md b/docs/providers/aws/examples/hello-world/python/README.md index c6380de14..11f1c597a 100644 --- a/docs/providers/aws/examples/hello-world/python/README.md +++ b/docs/providers/aws/examples/hello-world/python/README.md @@ -6,7 +6,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/examples/hello-world/python/) + # Hello World Python Example diff --git a/docs/providers/aws/examples/hello-world/ruby/README.md b/docs/providers/aws/examples/hello-world/ruby/README.md index 720642ead..2f73539b0 100644 --- a/docs/providers/aws/examples/hello-world/ruby/README.md +++ b/docs/providers/aws/examples/hello-world/ruby/README.md @@ -6,7 +6,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/examples/hello-world/ruby/) + # Hello World Ruby Example @@ -55,8 +57,8 @@ In your terminal window you should see the response from AWS Lambda. ```json { - "statusCode": 200, - "body": "\"Go Serverless v1.0! Your function executed successfully!\"" + "statusCode": 200, + "body": "\"Go Serverless v1.0! Your function executed successfully!\"" } ``` diff --git a/docs/providers/aws/guide/README.md b/docs/providers/aws/guide/README.md index 12835d123..67bca12a0 100644 --- a/docs/providers/aws/guide/README.md +++ b/docs/providers/aws/guide/README.md @@ -5,7 +5,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/guide/) + # Serverless AWS Lambda Guide diff --git a/docs/providers/aws/guide/credentials.md b/docs/providers/aws/guide/credentials.md index 208c58e55..ca5f27b19 100644 --- a/docs/providers/aws/guide/credentials.md +++ b/docs/providers/aws/guide/credentials.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/guide/credentials) + # AWS - Credentials @@ -26,8 +28,7 @@ Here's how to set up the Serverless Framework with your Amazon Web Services acco If you're new to Amazon Web Services, make sure you put in a credit card. -All AWS users get access to the Free Tier for [AWS Lambda](https://aws.amazon.com/lambda/pricing/). AWS Lambda is part of the non-expiring [AWS Free Tier](https://aws.amazon.com/free/#AWS_FREE_TIER). For additional pricing information for AWS Lambda and Gateway [here](https://aws.amazon.com/lambda/pricing/). If you're using additional AWS services, they could incur additional costs. Please review pricing for you services on AWS [here](https://aws.amazon.com/pricing/). - +All AWS users get access to the Free Tier for [AWS Lambda](https://aws.amazon.com/lambda/pricing/). AWS Lambda is part of the non-expiring [AWS Free Tier](https://aws.amazon.com/free/#AWS_FREE_TIER). For additional pricing information for AWS Lambda and AWS API Gateway [here](https://aws.amazon.com/lambda/pricing/). If you're using additional AWS services, they could incur additional costs. Please review pricing for you services on AWS [here](https://aws.amazon.com/pricing/). If you don't have a credit card set up, you may not be able to deploy your resources and you may run into this error: @@ -35,7 +36,7 @@ If you don't have a credit card set up, you may not be able to deploy your resou AWS Access Key Id needs a subscription for the service ``` -While in the AWS Free Tier, you can build an entire application on AWS Lambda, AWS API Gateway, and more, without getting charged for 1 year... As long as you don't exceed the resources in the free tier, of course. +While in the AWS Free Tier, you can build an entire application on AWS Lambda, AWS API Gateway, and more, without getting charged for 1 year... As long as you don't exceed the resources in the free tier, of course. ### Creating AWS Access Keys @@ -45,17 +46,17 @@ To let the Serverless Framework access your AWS account, we're going to **create 2. Click on **Users** and then **Add user**. Enter a name in the first field to remind you this User is the Framework, like `serverless-agent`. Enable **Programmatic access** by clicking the checkbox. Click **Next** to go through to the Permissions page. Click on **Create policy**. Select the **JSON** tab, add the following JSON file you'll find in [this gist](https://gist.github.com/ServerlessBot/7618156b8671840a539f405dea2704c8). -When you are finished, select **Review policy**. You can assign this policy a **Name** and **Description**, then choose **Create Policy**. Check everything looks good and click **Create user**. Later, you can create different IAM Users for different apps and different stages of those apps. That is, if you don't use separate AWS accounts for stages/apps, which is most common. +When you are finished, select **Review policy**. You can assign this policy a **Name** and **Description**, then choose **Create Policy**. Check everything looks good and click **Create user**. Later, you can create different IAM Users for different apps and different stages of those apps. That is, if you don't use separate AWS accounts for stages/apps, which is most common. 3. View and copy the **API Key** & **Secret** to a temporary place. You'll need it in the next step. -As you add additional functions and services, your permission needs will change. Though not advised, you can **create an IAM User with Admin access**, which can configure the services in your AWS account. This IAM User will have its own set of AWS Access Keys. +As you add additional functions and services, your permission needs will change. Though not advised, you can **create an IAM User with Admin access**, which can configure the services in your AWS account. This IAM User will have its own set of AWS Access Keys. -**Note:** In a production environment, we recommend reducing the permissions to the IAM User which the Framework uses. Unfortunately, the Framework's functionality is growing so fast, we can't yet offer you a finite set of permissions it needs (we're working on this). Consider using a separate AWS account in the interim, if you cannot get permission to your organization's primary AWS accounts. +**Note:** In a production environment, we recommend reducing the permissions to the IAM User which the Framework uses. Unfortunately, the Framework's functionality is growing so fast, we can't yet offer you a finite set of permissions it needs (we're working on this). Consider using a separate AWS account in the interim, if you cannot get permission to your organization's primary AWS accounts. 1. Create or login to your Amazon Web Services Account and go to the Identity & Access Management (IAM) page. -2. Click on **Users** and then **Add user**. Enter a name in the first field to remind you this User is the Framework, like `serverless-admin`. Enable **Programmatic access** by clicking the checkbox. Click **Next** to go through to the Permissions page. Click on **Attach existing policies directly**. Search for and select **AdministratorAccess** then click **Next: Review**. Check everything looks good and click **Create user**. Later, you can create different IAM Users for different apps and different stages of those apps. That is, if you don't use separate AWS accounts for stages/apps, which is most common. +2. Click on **Users** and then **Add user**. Enter a name in the first field to remind you this User is the Framework, like `serverless-admin`. Enable **Programmatic access** by clicking the checkbox. Click **Next** to go through to the Permissions page. Click on **Attach existing policies directly**. Search for and select **AdministratorAccess** then click **Next: Review**. Check everything looks good and click **Create user**. Later, you can create different IAM Users for different apps and different stages of those apps. That is, if you don't use separate AWS accounts for stages/apps, which is most common. 3. View and copy the **API Key** & **Secret** to a temporary place. You'll need it in the next step. @@ -76,7 +77,8 @@ serverless deploy # 'export' command is valid only for unix shells. In Windows - use 'set' instead of 'export' ``` -**Please note:** *If you are using a self-signed certificate you'll need to do one of the following:* +**Please note:** _If you are using a self-signed certificate you'll need to do one of the following:_ + ```bash # String example: # if using the 'ca' variable, your certificate contents should replace the newline character with '\n' @@ -94,7 +96,6 @@ export cafile="/path/to/cafile1.pem,/path/to/cafile2.pem" # 'export' command is valid only for unix shells. In Windows - use 'set' instead of 'export' ``` - #### Using AWS Profiles For a more permanent solution you can also set up credentials through AWS profiles. Here are different methods you can use to do so. @@ -131,7 +132,7 @@ You can even set up different profiles for different accounts, which can be used service: new-service provider: name: aws - runtime: nodejs6.10 + runtime: nodejs10.x stage: dev profile: devProfile ``` @@ -176,7 +177,7 @@ This example `serverless.yml` snippet will load the profile depending upon the s service: new-service provider: name: aws - runtime: nodejs6.10 + runtime: nodejs10.x stage: ${opt:stage, self:custom.defaultStage} profile: ${self:custom.profiles.${self:provider.stage}} custom: @@ -191,4 +192,4 @@ custom: **Be aware!** Due to the way AWS IAM and the local environment works, if you invoke your lambda functions locally using the CLI command `serverless invoke local -f ...` the IAM role/profile could be (and probably is) different from the one set in the `serverless.yml` configuration file. Thus, most likely, a different set of permissions will be in place, altering the interaction between your lambda functions and other AWS resources. -*Please, refer to the [`invoke local`](https://serverless.com/framework/docs/providers/aws/cli-reference/invoke-local/#aws---invoke-local) CLI command documentation for more details.* +_Please, refer to the [`invoke local`](https://serverless.com/framework/docs/providers/aws/cli-reference/invoke-local/#aws---invoke-local) CLI command documentation for more details._ diff --git a/docs/providers/aws/guide/deploying.md b/docs/providers/aws/guide/deploying.md index 8fd9423cb..15a70b8ee 100644 --- a/docs/providers/aws/guide/deploying.md +++ b/docs/providers/aws/guide/deploying.md @@ -7,12 +7,14 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/guide/deploying) + # AWS - Deploying -The Serverless Framework was designed to provision your AWS Lambda Functions, Events and infrastructure Resources safely and quickly. It does this via a couple of methods designed for different types of deployments. +The Serverless Framework was designed to provision your AWS Lambda Functions, Events and infrastructure Resources safely and quickly. It does this via a couple of methods designed for different types of deployments. ## Deploy All @@ -24,32 +26,33 @@ serverless deploy Use this method when you have updated your Function, Event or Resource configuration in `serverless.yml` and you want to deploy that change (or multiple changes at the same time) to Amazon Web Services. -**Note:** You can always enforce a deployment using the `--force` option. +**Note:** You can always enforce a deployment using the `--force` option, or specify a different configuration file name with the the `--config` option. ### How It Works -The Serverless Framework translates all syntax in `serverless.yml` to a single AWS CloudFormation template. By depending on CloudFormation for deployments, users of the Serverless Framework get the safety and reliability of CloudFormation. +The Serverless Framework translates all syntax in `serverless.yml` to a single AWS CloudFormation template. By depending on CloudFormation for deployments, users of the Serverless Framework get the safety and reliability of CloudFormation. -* An AWS CloudFormation template is created from your `serverless.yml`. -* If a Stack has not yet been created, then it is created with no resources except for an S3 Bucket, which will store zip files of your Function code. -* The code of your Functions is then packaged into zip files. -* Serverless fetches the hashes for all files of the previous deployment (if any) and compares them against the hashes of the local files. -* Serverless terminates the deployment process if all file hashes are the same. -* Zip files of your Functions' code are uploaded to your Code S3 Bucket. -* Any IAM Roles, Functions, Events and Resources are added to the AWS CloudFormation template. -* The CloudFormation Stack is updated with the new CloudFormation template. -* Each deployment publishes a new version for each function in your service. +- An AWS CloudFormation template is created from your `serverless.yml`. +- If a Stack has not yet been created, then it is created with no resources except for an S3 Bucket, which will store zip files of your Function code. +- The code of your Functions is then packaged into zip files. +- Serverless fetches the hashes for all files of the previous deployment (if any) and compares them against the hashes of the local files. +- Serverless terminates the deployment process if all file hashes are the same. +- Zip files of your Functions' code are uploaded to your Code S3 Bucket. +- Any IAM Roles, Functions, Events and Resources are added to the AWS CloudFormation template. +- The CloudFormation Stack is updated with the new CloudFormation template. +- Each deployment publishes a new version for each function in your service. ### Tips -* Use this in your CI/CD systems, as it is the safest method of deployment. -* You can print the progress during the deployment if you use `verbose` mode, like this: +- Use this in your CI/CD systems, as it is the safest method of deployment. +- You can print the progress during the deployment if you use `verbose` mode, like this: ``` serverless deploy --verbose ``` -* This method uses the AWS CloudFormation Stack Update method. CloudFormation is slow, so this method is slower. If you want to develop more quickly, use the `serverless deploy function` command (described below) +- This method uses the AWS CloudFormation Stack Update method. CloudFormation is slow, so this method is slower. If you want to develop more quickly, use the `serverless deploy function` command (described below) + +- This method defaults to `dev` stage and `us-east-1` region. You can change the default stage and region in your `serverless.yml` file by setting the `stage` and `region` properties inside a `provider` object as the following example shows: -* This method defaults to `dev` stage and `us-east-1` region. You can change the default stage and region in your `serverless.yml` file by setting the `stage` and `region` properties inside a `provider` object as the following example shows: ```yml # serverless.yml @@ -60,47 +63,46 @@ The Serverless Framework translates all syntax in `serverless.yml` to a single A region: us-west-2 ``` -* You can also deploy to different stages and regions by passing in flags to the command: +- You can also deploy to different stages and regions by passing in flags to the command: + ``` serverless deploy --stage production --region eu-central-1 ``` -* You can specify your own S3 bucket which should be used to store all the deployment artifacts. +- You can specify your own S3 bucket which should be used to store all the deployment artifacts. The `deploymentBucket` config which is nested under `provider` lets you e.g. set the `name` or the `serverSideEncryption` method for this bucket. If you don't provide your own bucket, Serverless will create a bucket which uses default AES256 encryption. -* You can specify your own S3 prefix which should be used to store all the deployment artifacts. +- You can specify your own S3 prefix which should be used to store all the deployment artifacts. The `deploymentPrefix` config which is nested under `provider` lets you set the prefix under which the deployment artifacts will be stored. If not specified, defaults to `serverless`. -* You can make uploading to S3 faster by adding `--aws-s3-accelerate` +- You can make uploading to S3 faster by adding `--aws-s3-accelerate` Check out the [deploy command docs](../cli-reference/deploy.md) for all details and options. - -* For information on multi-region deployments, [checkout this article](https://serverless.com/blog/build-multiregion-multimaster-application-dynamodb-global-tables). +- For information on multi-region deployments, [checkout this article](https://serverless.com/blog/build-multiregion-multimaster-application-dynamodb-global-tables). ## Deploy Function -This deployment method does not touch your AWS CloudFormation Stack. Instead, it simply overwrites the zip file of the current function on AWS. This method is much faster, since it does not rely on CloudFormation. +This deployment method does not touch your AWS CloudFormation Stack. Instead, it simply overwrites the zip file of the current function on AWS. This method is much faster, since it does not rely on CloudFormation. ```bash serverless deploy function --function myFunction ``` --**Note:** You can always enforce a deployment using the `--force` option. --**Note:** You can use `--update-config` to change only Lambda configuration without deploying code. +-**Note:** You can always enforce a deployment using the `--force` option. -**Note:** You can use `--update-config` to change only Lambda configuration without deploying code. ### How It Works -* The Framework packages up the targeted AWS Lambda Function into a zip file. -* The Framework fetches the hash of the already uploaded function .zip file and compares it to the local .zip file hash. -* The Framework terminates if both hashes are the same. -* That zip file is uploaded to your S3 bucket using the same name as the previous function, which the CloudFormation stack is pointing to. +- The Framework packages up the targeted AWS Lambda Function into a zip file. +- The Framework fetches the hash of the already uploaded function .zip file and compares it to the local .zip file hash. +- The Framework terminates if both hashes are the same. +- That zip file is uploaded to your S3 bucket using the same name as the previous function, which the CloudFormation stack is pointing to. ### Tips -* Use this when you are developing and want to test on AWS because it's much faster. -* During development, people will often run this command several times, as opposed to `serverless deploy` which is only run when larger infrastructure provisioning is required. +- Use this when you are developing and want to test on AWS because it's much faster. +- During development, people will often run this command several times, as opposed to `serverless deploy` which is only run when larger infrastructure provisioning is required. Check out the [deploy command docs](../cli-reference/deploy.md) for all details and options. @@ -116,5 +118,3 @@ serverless deploy --package path-to-package - The argument to the `--package` flag is a directory that has been previously packaged by Serverless (with `serverless package`). - The deploy process bypasses the package step and uses the existing package to deploy and update CloudFormation stacks. - - diff --git a/docs/providers/aws/guide/events.md b/docs/providers/aws/guide/events.md index f98ddf222..701584b91 100644 --- a/docs/providers/aws/guide/events.md +++ b/docs/providers/aws/guide/events.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/guide/events) + # AWS - Events @@ -60,7 +62,7 @@ functions: ## Types -The Serverless Framework supports all of the AWS Lambda events and more. Instead of listing them here, we've put them in a separate section, since they have a lot of configurations and functionality. [Check out the events section for more information.](../events) +The Serverless Framework supports all of the AWS Lambda events and more. Instead of listing them here, we've put them in a separate section, since they have a lot of configurations and functionality. [Check out the events section for more information.](../events) ## Deploying diff --git a/docs/providers/aws/guide/functions.md b/docs/providers/aws/guide/functions.md index ecdfe09ed..dc0153ef0 100644 --- a/docs/providers/aws/guide/functions.md +++ b/docs/providers/aws/guide/functions.md @@ -7,12 +7,14 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/guide/functions) + # AWS - Functions -If you are using AWS as a provider, all *functions* inside the service are AWS Lambda functions. +If you are using AWS as a provider, all _functions_ inside the service are AWS Lambda functions. ## Configuration @@ -24,7 +26,7 @@ service: myService provider: name: aws - runtime: nodejs6.10 + runtime: nodejs10.x memorySize: 512 # optional, in MB, default is 1024 timeout: 10 # optional, in seconds, default is 6 versionFunctions: false # optional, default is true @@ -47,7 +49,7 @@ The `handler` property points to the file and module containing the code you wan ```javascript // handler.js -module.exports.functionOne = function(event, context, callback) {} +module.exports.functionOne = function(event, context, callback) {}; ``` You can add as many functions as you want within this property. @@ -59,7 +61,7 @@ service: myService provider: name: aws - runtime: nodejs6.10 + runtime: nodejs10.x functions: functionOne: @@ -79,7 +81,7 @@ service: myService provider: name: aws - runtime: nodejs6.10 + runtime: nodejs10.x memorySize: 512 # will be inherited by all functions functions: @@ -95,7 +97,7 @@ service: myService provider: name: aws - runtime: nodejs6.10 + runtime: nodejs10.x functions: functionOne: @@ -107,8 +109,7 @@ You can specify an array of functions, which is useful if you separate your func ```yml # serverless.yml -... - +--- functions: - ${file(./foo-functions.yml)} - ${file(./bar-functions.yml)} @@ -122,10 +123,9 @@ deleteFoo: handler: handler.foo ``` - ## Permissions -Every AWS Lambda function needs permission to interact with other AWS infrastructure resources within your account. These permissions are set via an AWS IAM Role. You can set permission policy statements within this role via the `provider.iamRoleStatements` property. +Every AWS Lambda function needs permission to interact with other AWS infrastructure resources within your account. These permissions are set via an AWS IAM Role. You can set permission policy statements within this role via the `provider.iamRoleStatements` property. ```yml # serverless.yml @@ -133,7 +133,7 @@ service: myService provider: name: aws - runtime: nodejs6.10 + runtime: nodejs10.x iamRoleStatements: # permissions for all of your functions can be set here - Effect: Allow Action: # Gives permission to DynamoDB tables in a specific region @@ -144,7 +144,7 @@ provider: - dynamodb:PutItem - dynamodb:UpdateItem - dynamodb:DeleteItem - Resource: "arn:aws:dynamodb:us-east-1:*:*" + Resource: 'arn:aws:dynamodb:us-east-1:*:*' functions: functionOne: @@ -160,21 +160,21 @@ service: myService provider: name: aws iamRoleStatements: - - Effect: "Allow" - Action: - - "s3:ListBucket" - # 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" - Action: - - "s3:PutObject" - Resource: - Fn::Join: - - "" - - - "arn:aws:s3:::" - - "Ref" : "ServerlessDeploymentBucket" - - "/*" + - Effect: 'Allow' + Action: + - 's3:ListBucket' + # 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' + Action: + - 's3:PutObject' + Resource: + Fn::Join: + - '' + - - 'arn:aws:s3:::' + - 'Ref': 'ServerlessDeploymentBucket' + - '/*' functions: functionOne: @@ -293,6 +293,7 @@ functions: environment: TABLE_NAME: tableName2 ``` + If you want your function's environment variables to have the same values from your machine's environment variables, please read the documentation about [Referencing Environment Variables](./variables.md). ## Tags @@ -359,7 +360,6 @@ To publish Lambda Layers, check out the [Layers](./layers.md) documentation. By default, the framework will create LogGroups for your Lambdas. This makes it easy to clean up your log groups in the case you remove your service, and make the lambda IAM permissions much more specific and secure. - ## Versioning Deployed Functions By default, the framework creates function versions for every deploy. This behavior is optional, and can be turned off in cases where you don't invoke past versions by their qualifier. If you would like to do this, you can invoke your functions as `arn:aws:lambda:....:function/myFunc:3` to invoke version 3 for example. @@ -390,7 +390,7 @@ service: service provider: name: aws - runtime: nodejs6.10 + runtime: nodejs10.x functions: hello: @@ -443,7 +443,7 @@ service: myService provider: name: aws - runtime: nodejs8.10 + runtime: nodejs10.x tracing: lambda: true ``` diff --git a/docs/providers/aws/guide/iam.md b/docs/providers/aws/guide/iam.md index ca982db00..f08eef33c 100644 --- a/docs/providers/aws/guide/iam.md +++ b/docs/providers/aws/guide/iam.md @@ -7,18 +7,20 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/guide/iam) + # IAM -Every AWS Lambda function needs permission to interact with other AWS infrastructure resources within your account. These permissions are set via an AWS IAM Role which the Serverless Framework automatically creates for each Serverless Service, and is shared by all of your Functions. The Framework allows you to modify this Role or create Function-specific Roles, easily. +Every AWS Lambda function needs permission to interact with other AWS infrastructure resources within your account. These permissions are set via an AWS IAM Role which the Serverless Framework automatically creates for each Serverless Service, and is shared by all of your Functions. The Framework allows you to modify this Role or create Function-specific Roles, easily. ## The Default IAM Role By default, one IAM Role is shared by all of the Lambda functions in your service. Also by default, your Lambda functions have permission to create and write to CloudWatch logs. When VPC configuration is provided the default AWS `AWSLambdaVPCAccessExecutionRole` will be associated in order to communicate with your VPC resources. -To add specific rights to this service-wide Role, define statements in `provider.iamRoleStatements` which will be merged into the generated policy. As those statements will be merged into the CloudFormation template, you can use `Join`, `Ref` or any other CloudFormation method or feature. +To add specific rights to this service-wide Role, define statements in `provider.iamRoleStatements` which will be merged into the generated policy. As those statements will be merged into the CloudFormation template, you can use `Join`, `Ref` or any other CloudFormation method or feature. ```yml service: new-service @@ -26,26 +28,27 @@ service: new-service provider: name: aws iamRoleStatements: - - Effect: "Allow" - Action: - - "s3:ListBucket" - Resource: - Fn::Join: - - "" - - - "arn:aws:s3:::" - - Ref: ServerlessDeploymentBucket - - Effect: "Allow" - Action: - - "s3:PutObject" - Resource: - Fn::Join: - - "" - - - "arn:aws:s3:::" - - Ref: ServerlessDeploymentBucket - - "/*" - + - Effect: 'Allow' + Action: + - 's3:ListBucket' + Resource: + Fn::Join: + - '' + - - 'arn:aws:s3:::' + - Ref: ServerlessDeploymentBucket + - Effect: 'Allow' + Action: + - 's3:PutObject' + Resource: + Fn::Join: + - '' + - - 'arn:aws:s3:::' + - Ref: ServerlessDeploymentBucket + - '/*' ``` + Alongside `provider.iamRoleStatements` managed policies can also be added to this service-wide Role, define managed policies in `provider.iamManagedPolicies`. These will also be merged into the generated IAM Role so you can use `Join`, `Ref` or any other CloudFormation method or feature here too. + ```yml service: new-service @@ -180,7 +183,7 @@ resources: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents - Resource: + Resource: - 'Fn::Join': - ':' - @@ -218,7 +221,7 @@ resources: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents - Resource: + Resource: - 'Fn::Join': - ':' - @@ -277,7 +280,7 @@ resources: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents - Resource: + Resource: - 'Fn::Join': - ':' - @@ -316,7 +319,7 @@ resources: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents - Resource: + Resource: - 'Fn::Join': - ':' - @@ -331,5 +334,5 @@ resources: - ec2:DetachNetworkInterface - ec2:DeleteNetworkInterface Resource: "*" - + ``` diff --git a/docs/providers/aws/guide/installation.md b/docs/providers/aws/guide/installation.md index 3b8806f6a..657d02a6c 100644 --- a/docs/providers/aws/guide/installation.md +++ b/docs/providers/aws/guide/installation.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/guide/installation) + # AWS - Installation diff --git a/docs/providers/aws/guide/intro.md b/docs/providers/aws/guide/intro.md index e4ea181a5..057f4217a 100644 --- a/docs/providers/aws/guide/intro.md +++ b/docs/providers/aws/guide/intro.md @@ -7,16 +7,19 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/guide/intro) + # AWS - Introduction -The Serverless Framework helps you develop and deploy your AWS Lambda functions, along with the AWS infrastructure resources they require. It's a CLI that offers structure, automation and best practices out-of-the-box, allowing you to focus on building sophisticated, event-driven, serverless architectures, comprised of [Functions](#functions) and [Events](#events). +The Serverless Framework helps you develop and deploy your AWS Lambda functions, along with the AWS infrastructure resources they require. It's a CLI that offers structure, automation and best practices out-of-the-box, allowing you to focus on building sophisticated, event-driven, serverless architectures, comprised of [Functions](#functions) and [Events](#events). The Serverless Framework is different from other application frameworks because: -* It manages your code as well as your infrastructure -* It supports multiple languages (Node.js, Python, Java, and more) + +- It manages your code as well as your infrastructure +- It supports multiple languages (Node.js, Python, Java, and more) ## Core Concepts @@ -24,24 +27,24 @@ Here are the Framework's main concepts and how they pertain to AWS and Lambda... ### Functions -A Function is an AWS Lambda function. 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 an AWS Lambda function. 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* -* *Performing a scheduled task* +- _Saving a user to the database_ +- _Processing a file in a database_ +- _Performing a scheduled task_ -You can perform multiple jobs in your code, but we don't recommend doing that without good reason. Separation of concerns is best and the Framework is designed to help you easily develop and deploy Functions, as well as manage lots of them. +You can perform multiple jobs in your code, but we don't recommend doing that without good reason. Separation of concerns is best and the Framework is designed to help you easily develop and deploy Functions, as well as manage lots of them. ### Events -Anything that triggers an AWS Lambda Function to execute is regarded by the Framework as an **Event**. Events are infrastructure events on AWS such as: +Anything that triggers an AWS Lambda Function to execute is regarded by the Framework as an **Event**. Events are infrastructure events on AWS such as: -* *An AWS API Gateway HTTP endpoint request (e.g., for a REST API)* -* *An AWS S3 bucket upload (e.g., for an image)* -* *A CloudWatch timer (e.g., run every 5 minutes)* -* *An AWS SNS topic (e.g., a message)* -* *A CloudWatch Alert (e.g., something happened)* -* *And more...* +- _An AWS API Gateway HTTP endpoint request (e.g., for a REST API)_ +- _An AWS S3 bucket upload (e.g., for an image)_ +- _A CloudWatch timer (e.g., run every 5 minutes)_ +- _An AWS SNS topic (e.g., a message)_ +- _A CloudWatch Alert (e.g., something happened)_ +- _And more..._ When you define an event for your AWS Lambda functions in the Serverless Framework, the Framework will automatically create any infrastructure necessary for that event (e.g., an API Gateway endpoint) and configure your AWS Lambda Functions to listen to it. @@ -49,16 +52,16 @@ When you define an event for your AWS Lambda functions in the Serverless Framewo **Resources** are AWS infrastructure components which your Functions use such as: -* *An AWS DynamoDB Table (e.g., for saving Users/Posts/Comments data)* -* *An AWS S3 Bucket (e.g., for saving images or files)* -* *An AWS SNS Topic (e.g., for sending messages asynchronously)* -* *Anything that can be defined in CloudFormation is supported by the Serverless Framework* +- _An AWS DynamoDB Table (e.g., for saving Users/Posts/Comments data)_ +- _An AWS S3 Bucket (e.g., for saving images or files)_ +- _An AWS SNS Topic (e.g., for sending messages asynchronously)_ +- _Anything that can be defined in CloudFormation is supported by the Serverless Framework_ The Serverless Framework not only deploys your Functions and the Events that trigger them, but it also deploys the AWS infrastructure components your Functions depend upon. ### Services -A **Service** is the Framework's unit of organization. You can think of it as a project file, though you can have multiple services for a single application. It's where you define your Functions, the Events that trigger them, and the Resources your Functions use, all in one file entitled `serverless.yml` (or `serverless.json` or `serverless.js`). It looks like this: +A **Service** is the Framework's unit of organization. You can think of it as a project file, though you can have multiple services for a single application. It's where you define your Functions, the Events that trigger them, and the Resources your Functions use, all in one file entitled `serverless.yml` (or `serverless.json` or `serverless.js`). It looks like this: ```yml # serverless.yml @@ -75,11 +78,12 @@ functions: # Your "Functions" resources: # The "Resources" your "Functions" use. Raw AWS CloudFormation goes in here. ``` + When you deploy with the Framework by running `serverless deploy`, everything in `serverless.yml` is deployed at once. ### Plugins -You can overwrite or extend the functionality of the Framework using **Plugins**. Every `serverless.yml` can contain a `plugins:` property, which features multiple plugins. +You can overwrite or extend the functionality of the Framework using **Plugins**. Every `serverless.yml` can contain a `plugins:` property, which features multiple plugins. ```yml # serverless.yml diff --git a/docs/providers/aws/guide/layers.md b/docs/providers/aws/guide/layers.md index fa895be1c..5c9cb1491 100644 --- a/docs/providers/aws/guide/layers.md +++ b/docs/providers/aws/guide/layers.md @@ -7,12 +7,14 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/guide/layers) + # AWS - Layers -If you are using AWS as a provider, all *layers* inside the service are [AWS Lambda +If you are using AWS as a provider, all _layers_ inside the service are [AWS Lambda layers](https://aws.amazon.com/blogs/aws/new-for-aws-lambda-use-any-programming-language-and-share-common-components/). ## Configuration @@ -109,7 +111,6 @@ layers: artifact: layerSource.zip ``` - ## Permissions You can make your layers usable by other accounts by setting the `allowedAccounts` property: @@ -170,5 +171,5 @@ functions: hello: handler: handler.hello layers: - - {Ref: TestLambdaLayer} + - { Ref: TestLambdaLayer } ``` diff --git a/docs/providers/aws/guide/packaging.md b/docs/providers/aws/guide/packaging.md index 2caec7ee1..2655f067d 100644 --- a/docs/providers/aws/guide/packaging.md +++ b/docs/providers/aws/guide/packaging.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/guide/packaging) + # AWS - Packaging @@ -45,12 +47,12 @@ previously excluded files and directories. By default, serverless will exclude the following patterns: -- .git/** +- .git/\*\* - .gitignore - .DS_Store - npm-debug.log -- .serverless/** -- .serverless_plugins/** +- .serverless/\*\* +- .serverless_plugins/\*\* and the serverless configuration file being used (i.e. `serverless.yml`) @@ -58,7 +60,7 @@ and the serverless configuration file being used (i.e. `serverless.yml`) Exclude all node_modules but then re-include a specific modules (in this case node-fetch) using `exclude` exclusively -``` yml +```yml package: exclude: - node_modules/** @@ -67,7 +69,7 @@ package: Exclude all files but `handler.js` using `exclude` and `include` -``` yml +```yml package: exclude: - src/** @@ -90,7 +92,7 @@ Serverless won't zip your service if this is configured and therefore `exclude` The artifact option is especially useful in case your development environment allows you to generate a deployable artifact like Maven does for Java. -### Example +#### Service package ```yml service: my-service @@ -98,9 +100,10 @@ package: artifact: path/to/my-artifact.zip ``` -You can also use this to package functions individually. +#### Individual function packages + +You can also use this to package functions individually: -### Example ```yml service: my-service @@ -118,6 +121,34 @@ functions: method: get ``` +#### Artifacts hosted on S3 + +Artifacts can also be fetched from a remote S3 bucket. In this case you just need to provide the S3 object URL as the artifact value. This applies to both, service-wide and function-level artifact setups. + +##### Service package + +```yml +service: my-service + +package: + artifact: https://s3.amazonaws.com/some-bucket/service-artifact.zip +``` + +##### Individual function packages + +```yml +service: my-service + +package: + individually: true + +functions: + hello: + handler: com.serverless.Handler + package: + artifact: https://s3.amazonaws.com/some-bucket/function-artifact.zip +``` + ### Packaging functions separately If you want even more controls over your functions for deployment you can configure them to be packaged independently. This allows you more control for optimizing your deployment. To enable individual packaging set `individually` to true in the service or function wide packaging settings. diff --git a/docs/providers/aws/guide/plugins.md b/docs/providers/aws/guide/plugins.md index fc52aafb2..0e2646c09 100644 --- a/docs/providers/aws/guide/plugins.md +++ b/docs/providers/aws/guide/plugins.md @@ -7,19 +7,21 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/guide/plugins) + # Plugins -A Plugin is custom Javascript code that creates new or extends existing commands within the Serverless Framework. The Serverless Framework is merely a group of Plugins that are provided in the core. If you or your organization have a specific workflow, install a pre-written Plugin or write a plugin to customize the Framework to your needs. External Plugins are written exactly the same way as the core Plugins. +A Plugin is custom Javascript code that creates new or extends existing commands within the Serverless Framework. The Serverless Framework is merely a group of Plugins that are provided in the core. If you or your organization have a specific workflow, install a pre-written Plugin or write a plugin to customize the Framework to your needs. External Plugins are written exactly the same way as the core Plugins. - [How to create serverless plugins - Part 1](https://serverless.com/blog/writing-serverless-plugins/) - [How to create serverless plugins - Part 2](https://serverless.com/blog/writing-serverless-plugins-2/) ## Installing Plugins -External Plugins are added on a per service basis and are not applied globally. Make sure you are in your Service's root directory, then install the corresponding Plugin with the help of NPM: +External Plugins are added on a per service basis and are not applied globally. Make sure you are in your Service's root directory, then install the corresponding Plugin with the help of NPM: ``` npm install --save custom-serverless-plugin @@ -33,9 +35,11 @@ We need to tell Serverless that we want to use the plugin inside our service. We plugins: - custom-serverless-plugin ``` + The `plugins` section supports two formats: Array object: + ```yml plugins: - plugin1 @@ -43,6 +47,7 @@ plugins: ``` Enhanced plugins object: + ```yml plugins: localPath: './custom_serverless_plugins' @@ -63,25 +68,36 @@ custom: ## Service local plugin -If you are working on a plugin or have a plugin that is just designed for one project they can be loaded from the local folder. Local plugins can be added in the `plugins` array in `serverless.yml`. +If you are working on a plugin or have a plugin that is just designed for one project, it can be loaded from the local `.serverless_plugins` folder at the root of your service. Local plugins can be added in the `plugins` array in `serverless.yml`. -By default local plugins can be added to the `.serverless_plugins` directory at the root of your service, and in the `plugins` array in `serverless.yml`. ```yml plugins: - custom-serverless-plugin ``` Local plugins folder can be changed by enhancing `plugins` object: + ```yml plugins: localPath: './custom_serverless_plugins' modules: - custom-serverless-plugin ``` -The `custom-serverless-plugin` will be loaded from the `custom_serverless_plugins` directory at the root of your service. If the `localPath` is not provided or empty `.serverless_plugins` directory will be taken as the `localPath`. + +The `custom-serverless-plugin` will be loaded from the `custom_serverless_plugins` directory at the root of your service. If the `localPath` is not provided or empty, the `.serverless_plugins` directory will be used. The plugin will be loaded based on being named `custom-serverless-plugin.js` or `custom-serverless-plugin\index.js` in the root of `localPath` folder (`.serverless_plugins` by default). +If you want to load a plugin from a specific directory without affecting other plugins, you can also specify a path relative to the root of your service: + +```yaml +plugins: + # This plugin will be loaded from the `.serverless_plugins/` or `node_modules/` directories + - custom-serverless-plugin + # This plugin will be loaded from the `sub/directory/` directory + - ./sub/directory/another-custom-plugin +``` + ### Load Order Keep in mind that the order you define your plugins matters. When Serverless loads all the core plugins and then the custom plugins in the order you've defined them. @@ -102,15 +118,15 @@ In this case `plugin1` is loaded before `plugin2`. #### Plugin -Code which defines *Commands*, any *Events* within a *Command*, and any *Hooks* assigned to a *Lifecycle Event*. +Code which defines _Commands_, any _Events_ within a _Command_, and any _Hooks_ assigned to a _Lifecycle Event_. -* Command // CLI configuration, commands, subcommands, options - * LifecycleEvent(s) // Events that happen sequentially when the command is run - * Hook(s) // Code that runs when a Lifecycle Event happens during a Command +- Command // CLI configuration, commands, subcommands, options + - LifecycleEvent(s) // Events that happen sequentially when the command is run + - Hook(s) // Code that runs when a Lifecycle Event happens during a Command #### Command -A CLI *Command* that can be called by a user, e.g. `serverless deploy`. A Command has no logic, but simply defines the CLI configuration (e.g. command, subcommands, parameters) and the *Lifecycle Events* for the command. Every command defines its own lifecycle events. +A CLI _Command_ that can be called by a user, e.g. `serverless deploy`. A Command has no logic, but simply defines the CLI configuration (e.g. command, subcommands, parameters) and the _Lifecycle Events_ for the command. Every command defines its own lifecycle events. ```javascript 'use strict'; @@ -119,10 +135,7 @@ class MyPlugin { constructor() { this.commands = { deploy: { - lifecycleEvents: [ - 'resources', - 'functions' - ] + lifecycleEvents: ['resources', 'functions'], }, }; } @@ -133,7 +146,7 @@ module.exports = MyPlugin; #### Lifecycle Events -Events that fire sequentially during a Command. The above example lists two Events. However, for each Event, an additional `before` and `after` event is created. Therefore, six Events exist in the above example: +Events that fire sequentially during a Command. The above example lists two Events. However, for each Event, an additional `before` and `after` event is created. Therefore, six Events exist in the above example: - `before:deploy:resources` - `deploy:resources` @@ -155,17 +168,14 @@ class Deploy { constructor() { this.commands = { deploy: { - lifecycleEvents: [ - 'resources', - 'functions' - ] + lifecycleEvents: ['resources', 'functions'], }, }; this.hooks = { 'before:deploy:resources': this.beforeDeployResources, 'deploy:resources': this.deployResources, - 'after:deploy:functions': this.afterDeployFunctions + 'after:deploy:functions': this.afterDeployFunctions, }; } @@ -196,20 +206,14 @@ class MyPlugin { constructor() { this.commands = { deploy: { - lifecycleEvents: [ - 'resources', - 'functions' - ], + lifecycleEvents: ['resources', 'functions'], commands: { function: { - lifecycleEvents: [ - 'package', - 'deploy' - ], + lifecycleEvents: ['package', 'deploy'], }, }, }, - } + }; } } @@ -226,7 +230,7 @@ Option Shortcuts are passed in with a single dash (`-`) like this: `serverless f The `options` object will be passed in as the second parameter to the constructor of your plugin. -In it, you can optionally add a `shortcut` property, as well as a `required` property. The Framework will return an error if a `required` Option is not included. You can also set a `default` property if your option is not required. +In it, you can optionally add a `shortcut` property, as well as a `required` property. The Framework will return an error if a `required` Option is not included. You can also set a `default` property if your option is not required. **Note:** At this time, the Serverless Framework does not use parameters. @@ -240,27 +244,25 @@ class Deploy { this.commands = { deploy: { - lifecycleEvents: [ - 'functions' - ], + lifecycleEvents: ['functions'], options: { function: { usage: 'Specify the function you want to deploy (e.g. "--function myFunction")', shortcut: 'f', - required: true + required: true, }, stage: { usage: 'Specify the stage you want to deploy to. (e.g. "--stage prod")', shortcut: 's', - default: 'dev' - } - } + default: 'dev', + }, + }, }, }; this.hooks = { - 'deploy:functions': this.deployFunction.bind(this) - } + 'deploy:functions': this.deployFunction.bind(this), + }; } deployFunction() { @@ -292,21 +294,19 @@ class ProviderDeploy { this.commands = { deploy: { - lifecycleEvents: [ - 'functions' - ], + lifecycleEvents: ['functions'], options: { function: { usage: 'Specify the function you want to deploy (e.g. "--function myFunction")', - required: true - } - } + required: true, + }, + }, }, }; this.hooks = { - 'deploy:functions': this.deployFunction.bind(this) - } + 'deploy:functions': this.deployFunction.bind(this), + }; } deployFunction() { @@ -333,15 +333,13 @@ class MyPlugin { this.commands = { log: { - lifecycleEvents: [ - 'serverless' - ], + lifecycleEvents: ['serverless'], }, }; this.hooks = { - 'log:serverless': this.logServerless.bind(this) - } + 'log:serverless': this.logServerless.bind(this), + }; } logServerless() { diff --git a/docs/providers/aws/guide/quick-start.md b/docs/providers/aws/guide/quick-start.md index 3c1144832..0831aef70 100644 --- a/docs/providers/aws/guide/quick-start.md +++ b/docs/providers/aws/guide/quick-start.md @@ -11,8 +11,8 @@ layout: Doc ## Pre-requisites 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. +2. Serverless CLI `v1.9.0` or later. You can run + `npm install -g serverless` to install it. 3. An AWS account. If you don't already have one, you can sign up for a [free trial](https://aws.amazon.com/s/dm/optimization/server-side-test/free-tier/free_np/) that includes 1 million free Lambda requests per month. 4. **Set-up your [Provider Credentials](./credentials.md)** -> [Watch the video on setting up credentials](https://www.youtube.com/watch?v=KngM5bfpttA) @@ -40,34 +40,35 @@ $ cd my-service 1. **Deploy the Service** - Use this when you have made changes to your Functions, Events or Resources in `serverless.yml` or you simply want to deploy all changes within your Service at the same time. - - ```bash - serverless deploy -v - ``` +Use this when you have made changes to your Functions, Events or Resources in `serverless.yml` or you simply want to deploy all changes within your Service at the same time. + +```bash +serverless deploy -v +``` 2. **Deploy the Function** - Use this to quickly upload and overwrite your function code, allowing you to develop faster. - - ```bash - serverless deploy function -f hello - ``` +Use this to quickly upload and overwrite your function code, allowing you to develop faster. + +```bash +serverless deploy function -f hello +``` 3. **Invoke the Function** - Invokes a Function and returns logs. - - ```bash - serverless invoke -f hello -l - ``` +Invokes a Function and returns logs. + +```bash +serverless invoke -f hello -l +``` 4. **Fetch the Function Logs** - Open up a separate tab in your console, set your [Provider Credentials](./credentials.md) and stream all logs for a specific Function using this command. - ```bash - serverless logs -f hello -t - ``` +Open up a separate tab in your console, set your [Provider Credentials](./credentials.md) and stream all logs for a specific Function using this command. + +```bash +serverless logs -f hello -t +``` ## Cleanup diff --git a/docs/providers/aws/guide/resources.md b/docs/providers/aws/guide/resources.md index ca3b0aff6..11e2a7331 100644 --- a/docs/providers/aws/guide/resources.md +++ b/docs/providers/aws/guide/resources.md @@ -7,20 +7,22 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/guide/resources) + # AWS - Resources -If you are using AWS as a provider for your Service, all [*Resources*](./intro.md#resources) are other AWS infrastructure resources which the AWS Lambda functions in your [*Service*](./intro.md#services) depend on, like AWS DynamoDB or AWS S3. +If you are using AWS as a provider for your Service, all [_Resources_](./intro.md#resources) are other AWS infrastructure resources which the AWS Lambda functions in your [_Service_](./intro.md#services) depend on, like AWS DynamoDB or AWS S3. Using the Serverless Framework, you can define the infrastructure resources you need in `serverless.yml`, and easily deploy them. ## Configuration -Every stage you deploy to with `serverless.yml` using the `aws` provider is a single AWS CloudFormation stack. This is where your AWS Lambda functions and their event configurations are defined and it's how they are deployed. When you add `resources` those resources are added into your CloudFormation stack upon `serverless deploy`. +Every stage you deploy to with `serverless.yml` using the `aws` provider is a single AWS CloudFormation stack. This is where your AWS Lambda functions and their event configurations are defined and it's how they are deployed. When you add `resources` those resources are added into your CloudFormation stack upon `serverless deploy`. -Define your AWS resources in a property titled `resources`. What goes in this property is raw CloudFormation template syntax, in YAML, like this: +Define your AWS resources in a property titled `resources`. What goes in this property is raw CloudFormation template syntax, in YAML, like this: ```yml # serverless.yml @@ -29,7 +31,7 @@ service: usersCrud provider: aws functions: -resources: # CloudFormation template syntax +resources: # CloudFormation template syntax Resources: usersTable: Type: AWS::DynamoDB::Table @@ -46,7 +48,7 @@ resources: # CloudFormation template syntax WriteCapacityUnits: 1 ``` -You can overwrite/attach any kind of resource to your CloudFormation stack. You can add `Resources`, `Outputs` or even overwrite the `Description`. You can also use [Serverless Variables](./variables.md) for sensitive data or reusable configuration in your resources templates. Please be cautious as overwriting existing parts of your CloudFormation stack might introduce unexpected behavior. +You can overwrite/attach any kind of resource to your CloudFormation stack. You can add `Resources`, `Outputs` or even overwrite the `Description`. You can also use [Serverless Variables](./variables.md) for sensitive data or reusable configuration in your resources templates. Please be cautious as overwriting existing parts of your CloudFormation stack might introduce unexpected behavior. ## AWS CloudFormation Resource Reference @@ -54,10 +56,10 @@ To have consistent naming in the CloudFormation Templates that get deployed we u `{Function Name}{Cloud Formation Resource Type}{Resource Name}{SequentialID, instanceId or Random String}` -* `Function Name` - This is optional for Resources that should be recreated when the function name gets changed. Those resources are also called *function bound* -* `Cloud Formation Resource Type` - E.g., S3Bucket -* `Resource Name` - An identifier for the specific resource, e.g. for an S3 Bucket the configured bucket name. -* `SequentialID, instanceId or Random String` - For a few resources we need to add an optional sequential id, the Serverless instanceId (accessible via `${sls:instanceId}`) or a random string to identify them +- `Function Name` - This is optional for Resources that should be recreated when the function name gets changed. Those resources are also called _function bound_ +- `Cloud Formation Resource Type` - E.g., S3Bucket +- `Resource Name` - An identifier for the specific resource, e.g. for an S3 Bucket the configured bucket name. +- `SequentialID, instanceId or Random String` - For a few resources we need to add an optional sequential id, the Serverless instanceId (accessible via `${sls:instanceId}`) or a random string to identify them All resource names that are deployed by Serverless have to follow this naming scheme. The only exception (for backwards compatibility reasons) is the S3 Bucket that is used to upload artifacts so they can be deployed to your function. @@ -66,30 +68,30 @@ We're also using the term `normalizedName` or similar terms in this guide. This _Tip:_ If you are unsure how a resource is named, that you want to reference from your custom resources, you can issue a `serverless package`. This will create the CloudFormation template for your service in the `.serverless` folder (it is named `cloudformation-template-update-stack.json`). Just open the file and check for the generated resource name. -| AWS Resource | Name Template | Example | -|--- |--- | --- | -| S3::Bucket | S3Bucket{normalizedBucketName} | S3BucketMybucket | -|IAM::Role | IamRoleLambdaExecution | IamRoleLambdaExecution | -|Lambda::Function | {normalizedFunctionName}LambdaFunction | HelloLambdaFunction | -|Lambda::Version | {normalizedFunctionName}LambdaVersion{sha256} | HelloLambdaVersionr3pgoTvv1xT4E4NiCL6JG02fl6vIyi7OS1aW0FwAI | -|Logs::LogGroup | {normalizedFunctionName}LogGroup | HelloLogGroup | -|Lambda::Permission |
    • **Schedule**: {normalizedFunctionName}LambdaPermissionEventsRuleSchedule{index}
    • **CloudWatch Event**: {normalizedFunctionName}LambdaPermissionEventsRuleCloudWatchEvent{index}
    • **CloudWatch Log**: {normalizedFunctionName}LambdaPermissionLogsSubscriptionFilterCloudWatchLog{index}
    • **IoT**: {normalizedFunctionName}LambdaPermissionIotTopicRule{index}
    • **S3**: {normalizedFunctionName}LambdaPermission{normalizedBucketName}S3
    • **APIG**: {normalizedFunctionName}LambdaPermissionApiGateway
    • **SNS**: {normalizedFunctionName}LambdaPermission{normalizedTopicName}SNS
    • **Alexa Skill**: {normalizedFunctionName}LambdaPermissionAlexaSkill
    • **Alexa Smart Home**: {normalizedFunctionName}LambdaPermissionAlexaSmartHome{index}
    • **Cognito User Pool Trigger Source**: {normalizedFunctionName}LambdaPermissionCognitoUserPool{normalizedPoolId}TriggerSource{triggerSource}
    |
    • **Schedule**: HelloLambdaPermissionEventsRuleSchedule1
    • **CloudWatch Event**: HelloLambdaPermissionEventsRuleCloudWatchEvent1
    • **CloudWatch Log**: HelloLambdaPermissionLogsSubscriptionFilterCloudWatchLog1
    • **IoT**: HelloLambdaPermissionIotTopicRule1
    • **S3**: HelloLambdaPermissionBucketS3
    • **APIG**: HelloLambdaPermissionApiGateway
    • **SNS**: HelloLambdaPermissionTopicSNS
    • **Alexa Skill**: HelloLambdaPermissionAlexaSkill
    • **Alexa Smart Home**: HelloLambdaPermissionAlexaSmartHome1
    • **Cognito User Pool Trigger Source**: HelloLambdaPermissionCognitoUserPoolMyPoolTriggerSourceCustomMessage
    | -|Events::Rule |
    • **Schedule**: {normalizedFunctionName}EventsRuleSchedule{SequentialID}
    • **CloudWatch Event**: {normalizedFunctionName}EventsRuleCloudWatchEvent{SequentialID}
    |
    • **Schedule**: HelloEventsRuleSchedule1
    • **CloudWatch Event**: HelloEventsRuleCloudWatchEvent1
    | -|AWS::Logs::SubscriptionFilter | {normalizedFunctionName}LogsSubscriptionFilterCloudWatchLog{SequentialID} | HelloLogsSubscriptionFilterCloudWatchLog1 | -|AWS::IoT::TopicRule | {normalizedFunctionName}IotTopicRule{SequentialID} | HelloIotTopicRule1 | -|ApiGateway::RestApi | ApiGatewayRestApi | ApiGatewayRestApi | -|ApiGateway::Resource | ApiGatewayResource{normalizedPath} | ApiGatewayResourceUsers | -|ApiGateway::Method | ApiGatewayMethod{normalizedPath}{normalizedMethod} | ApiGatewayMethodUsersGet | -|ApiGateway::Authorizer | {normalizedFunctionName}ApiGatewayAuthorizer | HelloApiGatewayAuthorizer | -|ApiGateway::Deployment | ApiGatewayDeployment{instanceId} | ApiGatewayDeployment12356789 | -|ApiGateway::ApiKey | ApiGatewayApiKey{OptionalNormalizedName}{SequentialID} | ApiGatewayApiKeyFree1 | -|ApiGateway::UsagePlan | ApiGatewayUsagePlan{OptionalNormalizedName} | ApiGatewayUsagePlanFree | -|ApiGateway::UsagePlanKey | ApiGatewayUsagePlanKey{OptionalNormalizedName}{SequentialID} | ApiGatewayUsagePlanKeyFree1 | -|ApiGateway::Stage | ApiGatewayStage | ApiGatewayStage | -|SNS::Topic | SNSTopic{normalizedTopicName} | SNSTopicSometopic | -|SNS::Subscription | {normalizedFunctionName}SnsSubscription{normalizedTopicName} | HelloSnsSubscriptionSomeTopic | -|AWS::Lambda::EventSourceMapping |
    • **DynamoDB:** {normalizedFunctionName}EventSourceMappingDynamodb{tableName}
    • **Kinesis:** {normalizedFunctionName}EventSourceMappingKinesis{streamName}
    |
    • **DynamoDB:** HelloLambdaEventSourceMappingDynamodbUsers
    • **Kinesis:** HelloLambdaEventSourceMappingKinesisMystream
    | -|Cognito::UserPool | CognitoUserPool{normalizedPoolId} | CognitoUserPoolPoolId | +| AWS Resource | Name Template | Example | +| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| S3::Bucket | S3Bucket{normalizedBucketName} | S3BucketMybucket | +| IAM::Role | IamRoleLambdaExecution | IamRoleLambdaExecution | +| Lambda::Function | {normalizedFunctionName}LambdaFunction | HelloLambdaFunction | +| Lambda::Version | {normalizedFunctionName}LambdaVersion{sha256} | HelloLambdaVersionr3pgoTvv1xT4E4NiCL6JG02fl6vIyi7OS1aW0FwAI | +| Logs::LogGroup | {normalizedFunctionName}LogGroup | HelloLogGroup | +| Lambda::Permission |
    • **Schedule**: {normalizedFunctionName}LambdaPermissionEventsRuleSchedule{index}
    • **CloudWatch Event**: {normalizedFunctionName}LambdaPermissionEventsRuleCloudWatchEvent{index}
    • **CloudWatch Log**: {normalizedFunctionName}LambdaPermissionLogsSubscriptionFilterCloudWatchLog{index}
    • **IoT**: {normalizedFunctionName}LambdaPermissionIotTopicRule{index}
    • **S3**: {normalizedFunctionName}LambdaPermission{normalizedBucketName}S3
    • **APIG**: {normalizedFunctionName}LambdaPermissionApiGateway
    • **SNS**: {normalizedFunctionName}LambdaPermission{normalizedTopicName}SNS
    • **Alexa Skill**: {normalizedFunctionName}LambdaPermissionAlexaSkill
    • **Alexa Smart Home**: {normalizedFunctionName}LambdaPermissionAlexaSmartHome{index}
    • **Cognito User Pool Trigger Source**: {normalizedFunctionName}LambdaPermissionCognitoUserPool{normalizedPoolId}TriggerSource{triggerSource}
    |
    • **Schedule**: HelloLambdaPermissionEventsRuleSchedule1
    • **CloudWatch Event**: HelloLambdaPermissionEventsRuleCloudWatchEvent1
    • **CloudWatch Log**: HelloLambdaPermissionLogsSubscriptionFilterCloudWatchLog1
    • **IoT**: HelloLambdaPermissionIotTopicRule1
    • **S3**: HelloLambdaPermissionBucketS3
    • **APIG**: HelloLambdaPermissionApiGateway
    • **SNS**: HelloLambdaPermissionTopicSNS
    • **Alexa Skill**: HelloLambdaPermissionAlexaSkill
    • **Alexa Smart Home**: HelloLambdaPermissionAlexaSmartHome1
    • **Cognito User Pool Trigger Source**: HelloLambdaPermissionCognitoUserPoolMyPoolTriggerSourceCustomMessage
    | +| Events::Rule |
    • **Schedule**: {normalizedFunctionName}EventsRuleSchedule{SequentialID}
    • **CloudWatch Event**: {normalizedFunctionName}EventsRuleCloudWatchEvent{SequentialID}
    |
    • **Schedule**: HelloEventsRuleSchedule1
    • **CloudWatch Event**: HelloEventsRuleCloudWatchEvent1
    | +| AWS::Logs::SubscriptionFilter | {normalizedFunctionName}LogsSubscriptionFilterCloudWatchLog{SequentialID} | HelloLogsSubscriptionFilterCloudWatchLog1 | +| AWS::IoT::TopicRule | {normalizedFunctionName}IotTopicRule{SequentialID} | HelloIotTopicRule1 | +| ApiGateway::RestApi | ApiGatewayRestApi | ApiGatewayRestApi | +| ApiGateway::Resource | ApiGatewayResource{normalizedPath} | ApiGatewayResourceUsers | +| ApiGateway::Method | ApiGatewayMethod{normalizedPath}{normalizedMethod} | ApiGatewayMethodUsersGet | +| ApiGateway::Authorizer | {normalizedFunctionName}ApiGatewayAuthorizer | HelloApiGatewayAuthorizer | +| ApiGateway::Deployment | ApiGatewayDeployment{instanceId} | ApiGatewayDeployment12356789 | +| ApiGateway::ApiKey | ApiGatewayApiKey{OptionalNormalizedName}{SequentialID} | ApiGatewayApiKeyFree1 | +| ApiGateway::UsagePlan | ApiGatewayUsagePlan{OptionalNormalizedName} | ApiGatewayUsagePlanFree | +| ApiGateway::UsagePlanKey | ApiGatewayUsagePlanKey{OptionalNormalizedName}{SequentialID} | ApiGatewayUsagePlanKeyFree1 | +| ApiGateway::Stage | ApiGatewayStage | ApiGatewayStage | +| SNS::Topic | SNSTopic{normalizedTopicName} | SNSTopicSometopic | +| SNS::Subscription | {normalizedFunctionName}SnsSubscription{normalizedTopicName} | HelloSnsSubscriptionSomeTopic | +| AWS::Lambda::EventSourceMapping |
    • **DynamoDB:** {normalizedFunctionName}EventSourceMappingDynamodb{tableName}
    • **Kinesis:** {normalizedFunctionName}EventSourceMappingKinesis{streamName}
    |
    • **DynamoDB:** HelloLambdaEventSourceMappingDynamodbUsers
    • **Kinesis:** HelloLambdaEventSourceMappingKinesisMystream
    | +| Cognito::UserPool | CognitoUserPool{normalizedPoolId} | CognitoUserPoolPoolId | ## Override AWS CloudFormation Resource @@ -117,5 +119,5 @@ resources: WriteDashPostLogGroup: Type: AWS::Logs::LogGroup Properties: - RetentionInDays: "30" + RetentionInDays: '30' ``` diff --git a/docs/providers/aws/guide/serverless.yml.md b/docs/providers/aws/guide/serverless.yml.md index 1ccfe878b..c9e6d573f 100644 --- a/docs/providers/aws/guide/serverless.yml.md +++ b/docs/providers/aws/guide/serverless.yml.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/guide/serverless.yml) + # Serverless.yml Reference @@ -21,11 +23,11 @@ service: name: myService awsKmsKeyArn: arn:aws:kms:us-east-1:XXXXXX:key/some-hash # Optional KMS key arn which will be used for encryption for all functions -frameworkVersion: ">=1.0.0 <2.0.0" +frameworkVersion: '>=1.0.0 <2.0.0' provider: name: aws - runtime: nodejs8.10 + runtime: nodejs10.x stage: ${opt:stage, 'dev'} # Set the default stage used. Default is dev region: ${opt:region, 'us-east-1'} # Overwrite the default region used. Default is us-east-1 stackName: custom-stack-name # Use a custom name for the CloudFormation stack @@ -60,6 +62,7 @@ provider: restApiResources: # List of existing resources that were created in the REST API. This is required or the stack will be conflicted '/users': xxxxxxxxxx '/users/create': xxxxxxxxxx + websocketApiId: # Websocket API resource ID. Default is generated by the framewok apiKeySourceType: HEADER # Source of API key for usage plan. HEADER or AUTHORIZER. minimumCompressionSize: 1024 # Compress response when larger than specified size in bytes (must be between 0 and 10485760) description: Some Description # Optional description for the API Gateway stage deployment @@ -88,12 +91,12 @@ provider: - 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: "*" - Action: "Update:*" - Resource: "*" + Principal: '*' + Action: 'Update:*' + Resource: '*' - Effect: Deny - Principal: "*" - Resource: "*" + Principal: '*' + Resource: '*' Action: - Update:Replace - Update:Delete @@ -112,14 +115,14 @@ provider: - 'arn:aws:sns:us-east-1:XXXXXX:mytopic' resourcePolicy: - Effect: Allow - Principal: "*" + Principal: '*' Action: execute-api:Invoke Resource: - execute-api:/*/*/* Condition: IpAddress: aws:SourceIp: - - "123.123.123.123" + - '123.123.123.123' tags: # Optional service wide function tags foo: bar baz: qux @@ -128,6 +131,7 @@ provider: lambda: true # Optional, can be true (true equals 'Active'), 'Active' or 'PassThrough' logs: restApi: true # Optional configuration which specifies if API Gateway logs are used + websocket: true # Optional configuration which specifies if Websockets logs are used package: # Optional deployment packaging configuration include: # Specify the directories and files which should be included in the deployment package @@ -140,7 +144,6 @@ package: # Optional deployment packaging configuration artifact: path/to/my-artifact.zip # Own package that should be used. You must provide this file. individually: true # Enables individual packaging for each function. If true you must provide package for each function. Defaults to false - functions: usersCreate: # A Function handler: users.create # The file and module for this specific function. @@ -148,7 +151,7 @@ functions: description: My function # The description of your function. memorySize: 512 # memorySize for this specific function. reservedConcurrency: 5 # optional, reserved concurrency limit for this function. By default, AWS uses account concurrency limit - runtime: nodejs6.10 # Runtime for this specific function. Overrides the default which is set on the provider level + runtime: nodejs10.x # Runtime for this specific function. Overrides the default which is set on the provider level timeout: 10 # Timeout for this specific function. Overrides the default set above. role: arn:aws:iam::XXXXXX:role/role # IAM role which will be used for this function onError: arn:aws:sns:us-east-1:XXXXXX:sns-topic # Optional SNS topic / SQS arn (Ref, Fn::GetAtt and Fn::ImportValue are supported as well) which will be used for the DeadLetterConfig @@ -184,7 +187,7 @@ functions: private: true # Requires clients to add API keys values in the `x-api-key` header of their request authorizer: # An AWS API Gateway custom authorizer function name: authorizerFunc # The name of the authorizer function (must be in this service) - arn: xxx:xxx:Lambda-Name # Can be used instead of name to reference a function outside of service + arn: xxx:xxx:Lambda-Name # Can be used instead of name to reference a function outside of service resultTtlInSeconds: 0 identitySource: method.request.header.Authorization identityValidationExpression: someRegex @@ -245,9 +248,9 @@ functions: - cloudwatchEvent: event: source: - - "aws.ec2" + - 'aws.ec2' detail-type: - - "EC2 Instance State-change Notification" + - 'EC2 Instance State-change Notification' detail: state: - pending @@ -268,6 +271,12 @@ functions: - cognitoUserPool: pool: MyUserPool trigger: PreSignUp + - alb: + listenerArn: arn:aws:elasticloadbalancing:us-east-1:12345:listener/app/my-load-balancer/50dc6c495c0c9188/ + priority: 1 + conditions: + host: example.com + path: /hello layers: hello: # A Lambda layer @@ -302,7 +311,7 @@ resources: UsersTableArn: Description: The ARN for the User's Table Value: - "Fn::GetAtt": [ usersTable, Arn ] + 'Fn::GetAtt': [usersTable, Arn] Export: Name: ${self:service}:${opt:stage}:UsersTableArn # see Fn::ImportValue to use in other services and http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html for documentation on use. ``` diff --git a/docs/providers/aws/guide/services.md b/docs/providers/aws/guide/services.md index e69c24278..53d053a0e 100644 --- a/docs/providers/aws/guide/services.md +++ b/docs/providers/aws/guide/services.md @@ -7,25 +7,27 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/guide/services) + # Services -A `service` is like a project. It's where you define your AWS Lambda Functions, the `events` that trigger them and any AWS infrastructure `resources` they require, all in a file called `serverless.yml`. +A `service` is like a project. It's where you define your AWS Lambda Functions, the `events` that trigger them and any AWS infrastructure `resources` they require, all in a file called `serverless.yml`. To get started building your first Serverless Framework project, create a `service`. ## Organization -In the beginning of an application, many people use a single Service to define all of the Functions, Events and Resources for that project. This is what we recommend in the beginning. +In the beginning of an application, many people use a single Service to define all of the Functions, Events and Resources for that project. This is what we recommend in the beginning. ```bash myService/ serverless.yml # Contains all functions and infrastructure resources ``` -However, as your application grows, you can break it out into multiple services. A lot of people organize their services by workflows or data models, and group the functions related to those workflows and data models together in the service. +However, as your application grows, you can break it out into multiple services. A lot of people organize their services by workflows or data models, and group the functions related to those workflows and data models together in the service. ```bash users/ @@ -35,13 +37,12 @@ posts/ comments/ serverless.yml # Contains 4 functions that do Comments CRUD operations and the Comments database ``` -This makes sense since related functions usually use common infrastructure resources, and you want to keep those functions and resources together as a single unit of deployment, for better organization and separation of concerns. -**Note:** Currently, every service will create a separate REST API on AWS API Gateway. Due to a limitation with AWS API Gateway, you can only have a custom domain per one REST API. If you plan on making a large REST API, please make note of this limitation. Also, [a fix is in the works](https://github.com/serverless/serverless/issues/3078) and is a top priority. +This makes sense since related functions usually use common infrastructure resources, and you want to keep those functions and resources together as a single unit of deployment, for better organization and separation of concerns. ## Creation -To create a service, use the `create` command. You must also pass in a runtime (e.g., node.js, python etc.) you would like to write the service in. You can also pass in a path to create a directory and auto-name your service: +To create a service, use the `create` command. You must also pass in a runtime (e.g., node.js, python etc.) you would like to write the service in. You can also pass in a path to create a directory and auto-name your service: ```bash # Create service with nodeJS template in the folder ./myService @@ -50,32 +51,33 @@ serverless create --template aws-nodejs --path myService Here are the available templates for AWS Lambda: -* aws-clojurescript-gradle -* aws-clojure-gradle -* aws-nodejs -* aws-nodejs-typescript -* aws-alexa-typescript -* aws-nodejs-ecma-script -* aws-python -* aws-python3 -* aws-ruby -* aws-provided -* aws-kotlin-jvm-maven -* aws-kotlin-nodejs-gradle -* aws-groovy-gradle -* aws-java-gradle -* aws-java-maven -* aws-scala-sbt -* aws-csharp -* aws-fsharp -* aws-go -* aws-go-dep +- aws-clojurescript-gradle +- aws-clojure-gradle +- aws-nodejs +- aws-nodejs-typescript +- aws-alexa-typescript +- aws-nodejs-ecma-script +- aws-python +- aws-python3 +- aws-ruby +- aws-provided +- aws-kotlin-jvm-maven +- aws-kotlin-nodejs-gradle +- aws-groovy-gradle +- aws-java-gradle +- aws-java-maven +- aws-scala-sbt +- aws-csharp +- aws-fsharp +- aws-go +- aws-go-dep Check out the [create command docs](../cli-reference/create) for all the details and options. ## Contents You'll see the following files in your working directory: + - `serverless.yml` - `handler.js` @@ -83,14 +85,14 @@ You'll see the following files in your working directory: Each `service` configuration is managed in the `serverless.yml` file. The main responsibilities of this file are: - - Declare a Serverless service - - Define one or more functions in the service - - Define the provider the service will be deployed to (and the runtime if provided) - - Define any custom plugins to be used - - Define events that trigger each function to execute (e.g. HTTP requests) - - Define a set of resources (e.g. 1 DynamoDB table) required by the functions in this service - - Allow events listed in the `events` section to automatically create the resources required for the event upon deployment - - Allow flexible configuration using Serverless Variables +- Declare a Serverless service +- Define one or more functions in the service +- Define the provider the service will be deployed to (and the runtime if provided) +- Define any custom plugins to be used +- Define events that trigger each function to execute (e.g. HTTP requests) +- Define a set of resources (e.g. 1 DynamoDB table) required by the functions in this service +- Allow events listed in the `events` section to automatically create the resources required for the event upon deployment +- Allow flexible configuration using Serverless Variables You can see the name of the service, the provider configuration and the first function inside the `functions` definition which points to the `handler.js` file. Any further service configuration will be done in this file. @@ -101,7 +103,7 @@ service: users provider: name: aws - runtime: nodejs6.10 + runtime: nodejs10.x stage: dev # Set the default stage used. Default is dev region: us-east-1 # Overwrite the default region used. Default is us-east-1 stackName: my-custom-stack-name-${self:provider.stage} # Overwrite default CloudFormation stack name. Default is ${self:service}-${self:provider.stage} @@ -117,18 +119,18 @@ provider: deploymentPrefix: serverless # Overwrite the default S3 prefix under which deployed artifacts should be stored. Default is serverless versionFunctions: false # Optional function versioning stackTags: # Optional CF stack tags - key: value + key: value stackPolicy: # Optional CF stack policy. The example below allows updates to all resources except deleting/replacing EC2 instances (use with caution!) - Effect: Allow - Principal: "*" - Action: "Update:*" - Resource: "*" + Principal: '*' + Action: 'Update:*' + Resource: '*' - Effect: Deny - Principal: "*" + Principal: '*' Action: - Update:Replace - Update:Delete - Resource: "*" + Resource: '*' Condition: StringEquals: ResourceType: @@ -141,7 +143,7 @@ functions: - http: post users/create usersDelete: # A Function handler: users.delete - events: # The Events that trigger this Function + events: # The Events that trigger this Function - http: delete users/delete # The "Resources" your "Functions" use. Raw AWS CloudFormation goes in here. @@ -196,7 +198,7 @@ Deployment defaults to `dev` stage and `us-east-1` region on AWS, unless you spe serverless deploy --stage prod --region us-east-1 ``` -Check out the [deployment guide](https://serverless.com/framework/docs/providers/aws/guide/deploying/) to learn more about deployments and how they work. Or, check out the [deploy command docs](../cli-reference/deploy) for all the details and options. +Check out the [deployment guide](https://serverless.com/framework/docs/providers/aws/guide/deploying/) to learn more about deployments and how they work. Or, check out the [deploy command docs](../cli-reference/deploy) for all the details and options. ## Removal @@ -231,7 +233,7 @@ service: users provider: name: aws - runtime: nodejs6.10 + runtime: nodejs10.x memorySize: 512 … @@ -248,7 +250,7 @@ service: users provider: name: aws - runtime: nodejs6.10 + runtime: nodejs10.x memorySize: 512 … @@ -268,6 +270,7 @@ npm install serverless --save-dev To execute the locally installed Serverless executable you have to reference the binary out of the node modules directory. Example: + ``` node ./node_modules/serverless/bin/serverless deploy ``` diff --git a/docs/providers/aws/guide/testing.md b/docs/providers/aws/guide/testing.md index b09aadf3d..35c79e54c 100644 --- a/docs/providers/aws/guide/testing.md +++ b/docs/providers/aws/guide/testing.md @@ -7,22 +7,24 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/guide/testing) + # Testing -While the Serverless Architecture introduces a lot of simplicity when it comes to serving business logic, some of its characteristics present challenges for testing. They are: +While the Serverless Architecture introduces a lot of simplicity when it comes to serving business logic, some of its characteristics present challenges for testing. They are: -* The Serverless Architecture is an integration of separate, distributed services, which must be tested both independently, and together. -* The Serverless Architecture is dependent on internet/cloud services, which are hard to emulate locally. -* The Serverless Architecture can feature event-driven, asynchronous workflows, which are hard to emulate entirely. +- The Serverless Architecture is an integration of separate, distributed services, which must be tested both independently, and together. +- The Serverless Architecture is dependent on internet/cloud services, which are hard to emulate locally. +- The Serverless Architecture can feature event-driven, asynchronous workflows, which are hard to emulate entirely. To get through these challenges, and to keep the [test pyramid](http://martinfowler.com/bliki/TestPyramid.html) in mind, keep the following principles in mind: -* Write your business logic so that it is separate from your FaaS provider (e.g., AWS Lambda), to keep it provider-independent, reusable and more easily testable. -* When your business logic is written separately from the FaaS provider, you can write traditional Unit Tests to ensure it is working properly. -* Write Integration Tests to verify integrations with other services are working correctly. +- Write your business logic so that it is separate from your FaaS provider (e.g., AWS Lambda), to keep it provider-independent, reusable and more easily testable. +- When your business logic is written separately from the FaaS provider, you can write traditional Unit Tests to ensure it is working properly. +- Write Integration Tests to verify integrations with other services are working correctly. ## A Poor Example @@ -35,10 +37,10 @@ const mailer = require('mailer'); module.exports.saveUser = (event, context, callback) => { const user = { email: event.email, - created_at: Date.now() - } + created_at: Date.now(), + }; - db.saveUser(user, function (err) { + db.saveUser(user, function(err) { if (err) { callback(err); } else { @@ -51,8 +53,8 @@ module.exports.saveUser = (event, context, callback) => { There are two main problems with this function: -* The business logic is not separate from the FaaS Provider. It's bounded to how AWS Lambda passes incoming data (Lambda's `event` object). -* Testing this function will rely on separate services. Specifically, running a database instance and a mail server. +- The business logic is not separate from the FaaS Provider. It's bounded to how AWS Lambda passes incoming data (Lambda's `event` object). +- Testing this function will rely on separate services. Specifically, running a database instance and a mail server. ## Writing Testable AWS Lambda Functions @@ -68,17 +70,17 @@ class Users { save(email, callback) { const user = { email: email, - created_at: Date.now() - } + created_at: Date.now(), + }; - this.db.saveUser(user, function (err) { + this.db.saveUser(user, function(err) { if (err) { callback(err); } else { this.mailer.sendWelcomeEmail(email); callback(); } - }); + }); } } @@ -97,14 +99,15 @@ module.exports.saveUser = (event, context, callback) => { }; ``` -Now, the above class keeps business logic separate. Further, the code responsible for setting up dependencies, injecting them, calling business logic functions and interacting with AWS Lambda is in its own file, which will be changed less often. This way, the business logic is not provider dependent, easier to re-use, and easier to test. +Now, the above class keeps business logic separate. Further, the code responsible for setting up dependencies, injecting them, calling business logic functions and interacting with AWS Lambda is in its own file, which will be changed less often. This way, the business logic is not provider dependent, easier to re-use, and easier to test. -Further, this code doesn't require running any external services. Instead of a real `db` and `mailer` services, we can pass mocks and assert if `saveUser` and `sendWelcomeEmail` has been called with proper arguments. +Further, this code doesn't require running any external services. Instead of a real `db` and `mailer` services, we can pass mocks and assert if `saveUser` and `sendWelcomeEmail` has been called with proper arguments. -Unit Tests can easily be written to cover the above class. An integration test can be added by invoking the function (`serverless invoke`) with fixture email address, check if user is actually saved to DB and check if email was received to see if everything is working together. +Unit Tests can easily be written to cover the above class. An integration test can be added by invoking the function (`serverless invoke`) with fixture email address, check if user is actually saved to DB and check if email was received to see if everything is working together. ## Other Here are a few links to services which can help you test locally: -* [dynamodb-local](http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.html) -* [kinesalite](https://github.com/mhart/kinesalite) + +- [dynamodb-local](http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.html) +- [kinesalite](https://github.com/mhart/kinesalite) diff --git a/docs/providers/aws/guide/v0_to_v1.md b/docs/providers/aws/guide/v0_to_v1.md index b250ed7ce..3d52a3b71 100644 --- a/docs/providers/aws/guide/v0_to_v1.md +++ b/docs/providers/aws/guide/v0_to_v1.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/guide/v0_to_v1) + # Comparison Of Serverless Framework V.1 & V.0 diff --git a/docs/providers/aws/guide/variables.md b/docs/providers/aws/guide/variables.md index 804820b22..9aa6cf0b1 100644 --- a/docs/providers/aws/guide/variables.md +++ b/docs/providers/aws/guide/variables.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/guide/variables) + # Variables @@ -73,6 +75,7 @@ Likewise, if `sls deploy --stage prod` is run the `config.prod.json` file would 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. ```yml @@ -87,13 +90,13 @@ custom: functions: hello: - handler: handler.hello - events: - - schedule: ${self:custom.globalSchedule} + handler: handler.hello + events: + - schedule: ${self:custom.globalSchedule} world: - handler: handler.world - events: - - schedule: ${self:custom.globalSchedule} + handler: handler.world + events: + - schedule: ${self:custom.globalSchedule} resources: Outputs: NewServiceExport: @@ -105,6 +108,7 @@ resources: In the above example you're setting a global schedule for all functions by referencing the `globalSchedule` property in the same `serverless.yml` file. This way, you can easily change the schedule for all functions whenever you like. ## Referencing Serverless Core Variables + Serverless initializes core variables which are used internally by the Framework itself. Those values are exposed via the Serverless Variables system and can be re-used with the `{sls:}` variable prefix. The following variables are available: @@ -126,7 +130,8 @@ functions: ``` ## Referencing Environment Variables -To reference environment variables, use the `${env:SOME_VAR}` syntax in your `serverless.yml` configuration file. It is valid to use the empty string in place of `SOME_VAR`. This looks like "`${env:}`" and the result of declaring this in your `serverless.yml` is to embed the complete `process.env` object (i.e. all the variables defined in your environment). + +To reference environment variables, use the `${env:SOME_VAR}` syntax in your `serverless.yml` configuration file. It is valid to use the empty string in place of `SOME_VAR`. This looks like "`${env:}`" and the result of declaring this in your `serverless.yml` is to embed the complete `process.env` object (i.e. all the variables defined in your environment). **Note:** @@ -147,43 +152,48 @@ functions: In the above example you're dynamically adding a prefix to the function names by referencing the `FUNC_PREFIX` env var. So you can easily change that prefix for all functions by changing the `FUNC_PREFIX` env var. ## Referencing CLI Options -To reference CLI options that you passed, use the `${opt:some_option}` syntax in your `serverless.yml` configuration file. It is valid to use the empty string in place of `some_option`. This looks like "`${opt:}`" and the result of declaring this in your `serverless.yml` is to embed the complete `options` object (i.e. all the command line options from your `serverless` command). + +To reference CLI options that you passed, use the `${opt:some_option}` syntax in your `serverless.yml` configuration file. It is valid to use the empty string in place of `some_option`. This looks like "`${opt:}`" and the result of declaring this in your `serverless.yml` is to embed the complete `options` object (i.e. all the command line options from your `serverless` command). ```yml service: new-service provider: aws functions: hello: - name: ${opt:stage}-hello - handler: handler.hello + name: ${opt:stage}-hello + handler: handler.hello world: - name: ${opt:stage}-world - handler: handler.world + name: ${opt:stage}-world + handler: handler.world ``` In the above example, you're dynamically adding a prefix to the function names by referencing the `stage` option that you pass in the CLI when you run `serverless deploy --stage dev`. So when you deploy, the function name will always include the stage you're deploying to. ## Reference CloudFormation Outputs + You can reference CloudFormation stack output values as the source of your variables to use in your service with the `cf:stackName.outputKey` syntax. For example: + ```yml service: new-service provider: aws functions: hello: - name: ${cf:another-service-dev.functionPrefix}-hello - handler: handler.hello + name: ${cf:another-service-dev.functionPrefix}-hello + handler: handler.hello world: - name: ${cf:another-stack.functionPrefix}-world - handler: handler.world + name: ${cf:another-stack.functionPrefix}-world + handler: handler.world ``` + In that case, the framework will fetch the values of those `functionPrefix` outputs from the provided stack names and populate your variables. There are many use cases for this functionality and it allows your service to communicate with other services/stacks. You can add such custom output to CloudFormation stack. For example: + ```yml service: another-service provider: name: aws - runtime: nodejs8.10 + runtime: nodejs10.x region: ap-northeast-1 memorySize: 512 functions: @@ -191,7 +201,7 @@ functions: name: ${self:custom.functionPrefix}hello handler: handler.hello custom: - functionPrefix: "my-prefix-" + functionPrefix: 'my-prefix-' resources: Outputs: functionPrefix: @@ -205,16 +215,17 @@ resources: ``` You can also reference CloudFormation stack in another regions with the `cf.REGION:stackName.outputKey` syntax. For example: + ```yml service: new-service provider: aws functions: hello: - name: ${cf.us-west-2:another-service-dev.functionPrefix}-hello - handler: handler.hello + name: ${cf.us-west-2:another-service-dev.functionPrefix}-hello + handler: handler.hello world: - name: ${cf.ap-northeast-1:another-stack.functionPrefix}-world - handler: handler.world + name: ${cf.ap-northeast-1:another-stack.functionPrefix}-world + handler: handler.world ``` You can reference [CloudFormation stack outputs export values](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html) as well. For example: @@ -238,18 +249,22 @@ provider: ``` ## Referencing S3 Objects + You can reference S3 values as the source of your variables to use in your service with the `s3:bucketName/key` syntax. For example: + ```yml service: new-service provider: aws functions: hello: - name: ${s3:myBucket/myKey}-hello - handler: handler.hello + name: ${s3:myBucket/myKey}-hello + handler: handler.hello ``` + In the above example, the value for `myKey` in the `myBucket` S3 bucket will be looked up and used to populate the variable. ## Reference Variables using the SSM Parameter Store + You can reference SSM Parameters as the source of your variables with the `ssm:/path/to/param` syntax. For example: ```yml @@ -280,8 +295,8 @@ custom: In this example, the serverless variable will contain the decrypted value of the SecureString. ## Reference Variables using AWS Secrets Manager -Variables in [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/) can be referenced [using SSM](https://docs.aws.amazon.com/systems-manager/latest/userguide/integration-ps-secretsmanager.html). Use the `ssm:/aws/reference/secretsmanager/secret_ID_in_Secrets_Manager~true` syntax(note `~true` as secrets are always encrypted). For example: +Variables in [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/) can be referenced [using SSM](https://docs.aws.amazon.com/systems-manager/latest/userguide/integration-ps-secretsmanager.html). Use the `ssm:/aws/reference/secretsmanager/secret_ID_in_Secrets_Manager~true` syntax(note `~true` as secrets are always encrypted). For example: ```yml service: new-service @@ -326,9 +341,9 @@ custom: - false ``` - ## Reference Variables in Other Files -You can reference variables in other YAML or JSON files. To reference variables in other YAML files use the `${file(./myFile.yml):someProperty}` syntax in your `serverless.yml` configuration file. To reference variables in other JSON files use the `${file(./myFile.json):someProperty}` syntax. It is important that the file you are referencing has the correct suffix, or file extension, for its file type (`.yml` for YAML or `.json` for JSON) in order for it to be interpreted correctly. Here's an example: + +You can reference variables in other YAML or JSON files. To reference variables in other YAML files use the `${file(./myFile.yml):someProperty}` syntax in your `serverless.yml` configuration file. To reference variables in other JSON files use the `${file(./myFile.json):someProperty}` syntax. It is important that the file you are referencing has the correct suffix, or file extension, for its file type (`.yml` for YAML or `.json` for JSON) in order for it to be interpreted correctly. Here's an example: ```yml # myCustomFile.yml @@ -342,32 +357,34 @@ provider: aws custom: ${file(./myCustomFile.yml)} # You can reference the entire file functions: hello: - handler: handler.hello - events: - - schedule: ${file(./myCustomFile.yml):globalSchedule} # Or you can reference a specific property + handler: handler.hello + events: + - schedule: ${file(./myCustomFile.yml):globalSchedule} # Or you can reference a specific property world: - handler: handler.world - events: - - schedule: ${self:custom.globalSchedule} # This would also work in this case + handler: handler.world + events: + - schedule: ${self:custom.globalSchedule} # This would also work in this case ``` -In the above example, you're referencing the entire `myCustomFile.yml` file in the `custom` property. You need to pass the path relative to your service directory. You can also request specific properties in that file as shown in the `schedule` property. It's completely recursive and you can go as deep as you want. Additionally you can request properties that contain arrays from either YAML or JSON reference files. Here's a YAML example for an events array: +In the above example, you're referencing the entire `myCustomFile.yml` file in the `custom` property. You need to pass the path relative to your service directory. You can also request specific properties in that file as shown in the `schedule` property. It's completely recursive and you can go as deep as you want. Additionally you can request properties that contain arrays from either YAML or JSON reference files. Here's a YAML example for an events array: ```yml myevents: - schedule: - rate: rate(1 minute) + rate: rate(1 minute) ``` and for JSON: ```json { - "myevents": [{ - "schedule" : { - "rate" : "rate(1 minute)" + "myevents": [ + { + "schedule": { + "rate": "rate(1 minute)" + } } - }] + ] } ``` @@ -402,21 +419,21 @@ Here are other examples: ```js // scheduleConfig.js module.exports.rate = () => { - // Code that generates dynamic data - return 'rate (10 minutes)'; -} + // Code that generates dynamic data + return 'rate (10 minutes)'; +}; ``` ```js // config.js -module.exports = (serverless) => { +module.exports = serverless => { serverless.cli.consoleLog('You can access Serverless config and methods as well!'); return { property1: 'some value', - property2: 'some other value' - } -} + property2: 'some other value', + }; +}; ``` ```yml @@ -428,12 +445,12 @@ custom: ${file(./config.js)} functions: hello: - handler: handler.hello - events: - - schedule: ${file(./scheduleConfig.js):rate} # Reference a specific module + handler: handler.hello + events: + - schedule: ${file(./scheduleConfig.js):rate} # Reference a specific module ``` -You can also return an object and reference a specific property. Just make sure you are returning a valid object and referencing a valid property: +You can also return an object and reference a specific property. Just make sure you are returning a valid object and referencing a valid property: ```yml # serverless.yml @@ -441,22 +458,23 @@ service: new-service provider: aws functions: scheduledFunction: - handler: handler.scheduledFunction - events: - - schedule: ${file(./myCustomFile.js):schedule.ten} + handler: handler.scheduledFunction + events: + - schedule: ${file(./myCustomFile.js):schedule.ten} ``` ```js // myCustomFile.js module.exports.schedule = () => { - // Code that generates dynamic data - return { - ten: 'rate(10 minutes)', - twenty: 'rate(20 minutes)', - thirty: 'rate(30 minutes)' - }; -} + // Code that generates dynamic data + return { + ten: 'rate(10 minutes)', + twenty: 'rate(20 minutes)', + thirty: 'rate(30 minutes)', + }; +}; ``` + If your use case requires handling dynamic/async data sources (ie. DynamoDB, API calls...etc), you can also return a Promise that would be resolved as the value of the variable: ```yml @@ -465,17 +483,17 @@ service: new-service provider: aws functions: scheduledFunction: - handler: handler.scheduledFunction - events: - - schedule: ${file(./myCustomFile.js):promised} + handler: handler.scheduledFunction + events: + - schedule: ${file(./myCustomFile.js):promised} ``` ```js // myCustomFile.js module.exports.promised = () => { - // Async code that fetches the rate config... - return Promise.resolve('rate(10 minutes)'); -} + // Async code that fetches the rate config... + return Promise.resolve('rate(10 minutes)'); +}; ``` ## Multiple Configuration Files @@ -516,6 +534,7 @@ Resources: ``` ## Nesting Variable References + The Serverless variable system allows you to nest variable references within each other for ultimate flexibility. So you can reference certain variables based on other variables. Here's an example: ```yml @@ -526,12 +545,13 @@ custom: functions: hello: - handler: handler.hello + handler: handler.hello ``` In the above example, if you pass `dev` as a stage option, the framework will look for the `dev_arn` environment variable. If you pass `production`, the framework will look for `production_arn`, and so on. This allows you to creatively use multiple variables by using a certain naming pattern without having to update the values of these variables constantly. You can go as deep as you want in your nesting, and can reference variables at any level of nesting from any source (env, opt, self or file). ## Overwriting Variables + The Serverless framework gives you an intuitive way to reference multiple variables as a fallback strategy in case one of the variables is missing. This way you'll be able to use a default value from a certain source, if the variable from another source is missing. For example, if you want to reference the stage you're deploying to, but you don't want to keep on providing the `stage` option in the CLI. What you can do in `serverless.yml` is: @@ -547,7 +567,7 @@ custom: functions: hello: - handler: handler.hello + handler: handler.hello ``` What this says is to use the `stage` CLI option if it exists, if not, use the default stage (which lives in `provider.stage`). So during development you can safely deploy with `serverless deploy`, but during production you can do `serverless deploy --stage production` and the stage will be picked up for you without having to make any changes to `serverless.yml`. @@ -555,6 +575,7 @@ What this says is to use the `stage` CLI option if it exists, if not, use the de You can have as many variable references as you want, from any source you want, and each of them can be of different type and different name. ## Using Custom Variable Syntax + In some cases, the `${xxx}` variable syntax conflicts with some CloudFormation functionality. In that case you can provide a custom syntax to overwrite our default `${xxx}` syntax by setting the `provider.variableSyntax` property to the desired regex: ```yml @@ -562,16 +583,18 @@ service: new-service provider: name: aws - runtime: nodejs6.10 + runtime: nodejs10.x variableSyntax: "\\${{([ ~:a-zA-Z0-9._@\\'\",\\-\\/\\(\\)]+?)}}" # notice the double quotes for yaml to ignore the escape characters! # variableSyntax: "\\${((?!AWS)[ ~:a-zA-Z0-9._@'\",\\-\\/\\(\\)]+?)}" # Use this for allowing CloudFormation Pseudo-Parameters in your serverless.yml -- e.g. ${AWS::Region}. All other Serverless variables work as usual. custom: myStage: ${{opt:stage}} ``` + In this example, we're overwriting the default regex for our variable syntax. So whenever you define variables, you now need to use `${{}}` instead of `${}` (double curly brackets). ## Migrating serverless.env.yml + Previously we used the `serverless.env.yml` file to track Serverless Variables. It was a completely different system with different concepts. To migrate your variables from `serverless.env.yml`, you'll need to decide where you want to store your variables. **Using a config file:** You can still use `serverless.env.yml`, but the difference now is that you can structure the file however you want, and you'll need to reference each variable/property correctly in `serverless.yml`. For more info, you can check the file reference section above. @@ -591,12 +614,11 @@ You can reference [AWS Pseudo Parameters](http://docs.aws.amazon.com/AWSCloudFor Here's an example: ```yml - Resources: - - 'Fn::Join': - - ':' - - - - 'arn:aws:logs' - - Ref: 'AWS::Region' - - Ref: 'AWS::AccountId' - - 'log-group:/aws/lambda/*:*:*' +Resources: + - 'Fn::Join': + - ':' + - - 'arn:aws:logs' + - Ref: 'AWS::Region' + - Ref: 'AWS::AccountId' + - 'log-group:/aws/lambda/*:*:*' ``` diff --git a/docs/providers/aws/guide/workflow.md b/docs/providers/aws/guide/workflow.md index 297cbdf28..58a6cf75e 100644 --- a/docs/providers/aws/guide/workflow.md +++ b/docs/providers/aws/guide/workflow.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/guide/workflow) + # Workflow @@ -20,54 +22,69 @@ Quick recommendations and tips for various processes. 2. Use `serverless deploy` only when you've made changes to `serverless.yml` and in CI/CD systems. For more information on setting up CI/CD for your Serverless app, read [this article](https://serverless.com/blog/ci-cd-workflow-serverless-apps-with-circleci). 3. Use `serverless deploy function -f myFunction` to rapidly deploy changes when you are working on a specific AWS Lambda Function. 4. Use `serverless invoke -f myFunction -l` to test your AWS Lambda Functions on AWS. -5. Open up a separate tab in your console and stream logs in there via `serverless logs -f myFunction -t`. +5. Open up a separate tab in your console and stream logs in there via `serverless logs -f myFunction -t`. 6. Write tests to run locally. ### Using stages -* At the very least, use a `dev` and `production` stage. -* Use different AWS accounts for stages. -* In larger teams, each member should use a separate AWS account and their own stage for development. + +- At the very least, use a `dev` and `production` stage. +- Use different AWS accounts for stages. +- In larger teams, each member should use a separate AWS account and their own stage for development. ### Larger Projects -* Break your application/project into multiple Serverless Services. -* Model your Serverless Services around Data Models or Workflows. -* Keep the Functions and Resources in your Serverless Services to a minimum. + +- Break your application/project into multiple Serverless Services. +- Model your Serverless Services around Data Models or Workflows. +- Keep the Functions and Resources in your Serverless Services to a minimum. ## Cheat Sheet + A handy list of commands to use when developing with the Serverless Framework. ##### Create A Service: + Creates a new Service + ``` serverless create -p [SERVICE NAME] -t aws-nodejs ``` ##### Install A Service + This is a convenience method to install a pre-made Serverless Service locally by downloading the Github repo and unzipping it. + ``` serverless install -u [GITHUB URL OF SERVICE] ``` ##### Deploy All + Use this when you have made changes to your Functions, Events or Resources in `serverless.yml` or you simply want to deploy all changes within your Service at the same time. + ``` serverless deploy -s [STAGE NAME] -r [REGION NAME] -v ``` ##### Deploy Function + Use this to quickly overwrite your AWS Lambda code on AWS, allowing you to develop faster. + ``` serverless deploy function -f [FUNCTION NAME] -s [STAGE NAME] -r [REGION NAME] ``` ##### Invoke Function + Invokes an AWS Lambda Function on AWS and returns logs. + ``` serverless invoke -f [FUNCTION NAME] -s [STAGE NAME] -r [REGION NAME] -l ``` ##### Streaming Logs + Open up a separate tab in your console and stream all logs for a specific Function using this command. + ``` serverless logs -f [FUNCTION NAME] -s [STAGE NAME] -r [REGION NAME] ``` diff --git a/docs/providers/azure/README.md b/docs/providers/azure/README.md index 0bd1b70be..6f513e280 100644 --- a/docs/providers/azure/README.md +++ b/docs/providers/azure/README.md @@ -5,7 +5,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/) + # Azure Functions Provider Documentation diff --git a/docs/providers/azure/cli-reference/README.md b/docs/providers/azure/cli-reference/README.md index 716163f19..bb3eea48c 100644 --- a/docs/providers/azure/cli-reference/README.md +++ b/docs/providers/azure/cli-reference/README.md @@ -5,12 +5,14 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/azure/cli-reference/) + # Serverless Azure Functions CLI Reference -Welcome to the Serverless Azure Functions CLI Reference! Please select a section +Welcome to the Serverless Azure Functions CLI Reference! Please select a section on the left to get started. If you have questions, join the [chat in gitter](https://gitter.im/serverless/serverless) or [post over on the forums](http://forum.serverless.com/). diff --git a/docs/providers/azure/cli-reference/create.md b/docs/providers/azure/cli-reference/create.md index f9bddeeaa..b51a3aef0 100644 --- a/docs/providers/azure/cli-reference/create.md +++ b/docs/providers/azure/cli-reference/create.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/azure/cli-reference/create) + # Azure - Create @@ -28,6 +30,7 @@ serverless create --template azure-nodejs --path myService ``` ## Options + - `--template` or `-t` The name of one of the available templates. **Required if --template-url and --template-path are not present**. - `--template-url` or `-u` The name of one of the available templates. **Required if --template and --template-path are not present**. - `--template-path` The local path of your template. **Required if --template and --template-url are not present**. @@ -35,6 +38,7 @@ serverless create --template azure-nodejs --path myService - `--name` or `-n` the name of the service in `serverless.yml`. ## Provided lifecycle events + - `create:create` ## Available Templates @@ -64,8 +68,7 @@ serverless create --template azure-nodejs --path my-new-service ``` This example will generate scaffolding for a service with `Azure` 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 +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 diff --git a/docs/providers/azure/cli-reference/deploy-function.md b/docs/providers/azure/cli-reference/deploy-function.md index 8fc4d465c..ad3cf3818 100644 --- a/docs/providers/azure/cli-reference/deploy-function.md +++ b/docs/providers/azure/cli-reference/deploy-function.md @@ -7,12 +7,14 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/azure/cli-reference/deploy-function) + # Azure - Deploy Function -The `serverless deploy function` command deploys an individual function. This +The `serverless deploy function` command deploys an individual function. This command simply compiles a deployment package with a single function handler. This is a much faster way of deploying changes in code. @@ -24,4 +26,5 @@ serverless deploy function -f functionName properties such as environment variables and events will **not** be deployed. ## Options + - `--function` or `-f` The name of the function which should be deployed diff --git a/docs/providers/azure/cli-reference/deploy.md b/docs/providers/azure/cli-reference/deploy.md index bf53ce438..b544e8172 100644 --- a/docs/providers/azure/cli-reference/deploy.md +++ b/docs/providers/azure/cli-reference/deploy.md @@ -7,14 +7,16 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/azure/cli-reference/deploy) + # Azure - Deploy The `serverless deploy` command deploys your entire service via the Azure Resource Manager API. Run this command when you have made service changes (i.e., -you edited `serverless.yml`). Use `serverless deploy function -f myFunction` +you edited `serverless.yml`). Use `serverless deploy function -f myFunction` when you have made code changes and you want to quickly upload your updated code to Azure Functions. @@ -23,6 +25,8 @@ serverless deploy ``` ## Options + +- `--config` or `-c` Path to your conifguration file, if other than `serverless.yml|.yaml|.js|.json`. - `--noDeploy` or `-n` Skips the deployment steps and leaves artifacts in the `.serverless` directory - `--verbose` or `-v` Shows all stack events during deployment, and display any Stack Output. - `--function` or `-f` Invoke `deploy function` (see above). Convenience shortcut. diff --git a/docs/providers/azure/cli-reference/install.md b/docs/providers/azure/cli-reference/install.md index 916115f8d..a3c39473f 100644 --- a/docs/providers/azure/cli-reference/install.md +++ b/docs/providers/azure/cli-reference/install.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/azure/cli-reference/install) + # Azure - Install @@ -19,12 +21,21 @@ serverless install --url https://github.com/some/service ``` ## Options + - `--url` or `-u` The services GitHub URL. **Required**. - `--name` or `-n` Name for the service. ## Provided lifecycle events + - `install:install` +## Supported Code Hosting Platforms + +- GitHub +- GitHub Enterprise +- GitLab +- BitBucket + ## Examples ### Installing a service from a GitHub URL diff --git a/docs/providers/azure/cli-reference/invoke.md b/docs/providers/azure/cli-reference/invoke.md index f7479d215..cf7d3c400 100644 --- a/docs/providers/azure/cli-reference/invoke.md +++ b/docs/providers/azure/cli-reference/invoke.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/azure/cli-reference/invoke) + # Azure - Invoke @@ -20,10 +22,12 @@ serverless invoke --function functionName ``` ## Options + - `--function` or `-f` The name of the function in your service that you want to invoke. **Required**. - `--path` or `-p` The path to a json file with input data to be passed to the invoked function. This path is relative to the root directory of the service. ## Provided lifecycle events + - `invoke:invoke` ## Examples diff --git a/docs/providers/azure/cli-reference/login.md b/docs/providers/azure/cli-reference/login.md index 15becc6c3..9a1368ca5 100644 --- a/docs/providers/azure/cli-reference/login.md +++ b/docs/providers/azure/cli-reference/login.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/azure/cli-reference/login) + # Login diff --git a/docs/providers/azure/cli-reference/logs.md b/docs/providers/azure/cli-reference/logs.md index b3965137c..01cbf6eda 100644 --- a/docs/providers/azure/cli-reference/logs.md +++ b/docs/providers/azure/cli-reference/logs.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/azure/cli-reference/logs) + # Azure - Logs @@ -29,4 +31,5 @@ serverless logs -f hello ```bash serverless logs -f hello ``` + This will stream all future logs for a given Function. diff --git a/docs/providers/azure/cli-reference/plugin-install.md b/docs/providers/azure/cli-reference/plugin-install.md index 354aa62b4..43e346c4b 100644 --- a/docs/providers/azure/cli-reference/plugin-install.md +++ b/docs/providers/azure/cli-reference/plugin-install.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/azure/cli-reference/plugin-install) + # Plugin Install @@ -22,9 +24,11 @@ serverless plugin install --name pluginName ``` ## Options + - `--name` or `-n` The plugins name. **Required**. ## Provided lifecycle events + - `plugin:install:install` ## Examples diff --git a/docs/providers/azure/cli-reference/plugin-list.md b/docs/providers/azure/cli-reference/plugin-list.md index b54edae88..b82201b1a 100644 --- a/docs/providers/azure/cli-reference/plugin-list.md +++ b/docs/providers/azure/cli-reference/plugin-list.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/azure/cli-reference/plugin-list) + # Plugin List @@ -19,7 +21,9 @@ serverless plugin list ``` ## Options -- *None* + +- _None_ ## Provided lifecycle events + - `plugin:list:list` diff --git a/docs/providers/azure/cli-reference/plugin-search.md b/docs/providers/azure/cli-reference/plugin-search.md index ff3044b0d..b0097164b 100644 --- a/docs/providers/azure/cli-reference/plugin-search.md +++ b/docs/providers/azure/cli-reference/plugin-search.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/azure/cli-reference/plugin-search) + # Plugin Search @@ -19,9 +21,11 @@ serverless plugin search --query query ``` ## Options + - `--query` or `-q` The query you want to use for your search. **Required**. ## Provided lifecycle events + - `plugin:search:search` ## Examples diff --git a/docs/providers/azure/cli-reference/plugin-uninstall.md b/docs/providers/azure/cli-reference/plugin-uninstall.md index 92f5af2db..77bf0f599 100644 --- a/docs/providers/azure/cli-reference/plugin-uninstall.md +++ b/docs/providers/azure/cli-reference/plugin-uninstall.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/azure/cli-reference/plugin-uninstall) + # Plugin Uninstall @@ -19,9 +21,11 @@ serverless plugin uninstall --name pluginName ``` ## Options + - `--name` or `-n` The plugins name. **Required**. ## Provided lifecycle events + - `plugin:uninstall:uninstall` ## Examples diff --git a/docs/providers/azure/cli-reference/print.md b/docs/providers/azure/cli-reference/print.md index e402e8e7f..8e1d5ece5 100644 --- a/docs/providers/azure/cli-reference/print.md +++ b/docs/providers/azure/cli-reference/print.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/azure/cli-reference/print) + # Print diff --git a/docs/providers/azure/cli-reference/remove.md b/docs/providers/azure/cli-reference/remove.md index aa769f3cf..4b5856a20 100644 --- a/docs/providers/azure/cli-reference/remove.md +++ b/docs/providers/azure/cli-reference/remove.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/azure/cli-reference/remove) + # Azure - Remove @@ -20,6 +22,7 @@ serverless remove ``` ## Provided lifecycle events + - `remove:remove` ## Examples diff --git a/docs/providers/azure/events/README.md b/docs/providers/azure/events/README.md index b3ac196a0..1d6a50046 100644 --- a/docs/providers/azure/events/README.md +++ b/docs/providers/azure/events/README.md @@ -1,11 +1,13 @@ + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/azure/events/) + # Serverless Azure Functions Events diff --git a/docs/providers/azure/events/blobstorage.md b/docs/providers/azure/events/blobstorage.md index 60475b39b..f8a9eea49 100644 --- a/docs/providers/azure/events/blobstorage.md +++ b/docs/providers/azure/events/blobstorage.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/azure/events/blobstorage) + # Blob Storage Trigger @@ -35,9 +37,9 @@ functions: events: - blob: x-azure-settings: - name: item #, default - "myBlob", specifies which name it's available on `context.bindings` - path: hello/{name} - connection: AzureWebJobsStorage #, default - "AzureWebJobsStorage", App Setting/environment variable which contains Storage Account Connection String + name: item #, default - "myBlob", specifies which name it's available on `context.bindings` + path: hello/{name} + connection: AzureWebJobsStorage #, default - "AzureWebJobsStorage", App Setting/environment variable which contains Storage Account Connection String ``` ```javascript @@ -46,7 +48,7 @@ functions: 'use strict'; module.exports.hello = function(context, item) { - context.log("Received item: ${item}"); + context.log('Received item: ${item}'); context.done(); }; ``` diff --git a/docs/providers/azure/events/eventhubs.md b/docs/providers/azure/events/eventhubs.md index 8c738d146..3cee537a1 100644 --- a/docs/providers/azure/events/eventhubs.md +++ b/docs/providers/azure/events/eventhubs.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/azure/events/eventhubs) + # Event Hubs Trigger @@ -34,10 +36,10 @@ functions: events: - eventHub: x-azure-settings: - name: item #, default - "myEventHubMessage", specifies which name it's available on `context.bindings` - path: hello #, specifies the Name of the Event Hub - consumerGroup: $Default #, default - "$Default", specifies the consumerGroup to listen with - connection: EventHubsConnection #, App Setting/environment variable which contains Event Hubs Namespace Connection String + name: item #, default - "myEventHubMessage", specifies which name it's available on `context.bindings` + path: hello #, specifies the Name of the Event Hub + consumerGroup: $Default #, default - "$Default", specifies the consumerGroup to listen with + connection: EventHubsConnection #, App Setting/environment variable which contains Event Hubs Namespace Connection String ``` ```javascript @@ -46,7 +48,7 @@ functions: 'use strict'; module.exports.hello = function(context, item) { - context.log("Received item: ${item}"); + context.log('Received item: ${item}'); context.done(); }; ``` diff --git a/docs/providers/azure/events/http.md b/docs/providers/azure/events/http.md index 7717009a2..951be22b7 100644 --- a/docs/providers/azure/events/http.md +++ b/docs/providers/azure/events/http.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/azure/events/http) + # HTTP Trigger @@ -40,11 +42,11 @@ functions: events: - http: true x-azure-settings: - name: req #, default - "req", specifies which name it's available on `context.bindings` - methods: # [GET, POST, PUT, DELETE], default - all - - get - route: example/hello #, default - - authLevel: anonymous # + name: req #, default - "req", specifies which name it's available on `context.bindings` + methods: # [GET, POST, PUT, DELETE], default - all + - get + route: example/hello #, default - + authLevel: anonymous # ``` URL paths for the serverless functions are prefixed with "api" by default, e.g. @@ -58,8 +60,8 @@ URL paths for the serverless functions are prefixed with "api" by default, e.g. module.exports.hello = function(context, req) { context.res = { - body: "Hello world!" - } + body: 'Hello world!', + }; context.done(); }; ``` @@ -87,13 +89,13 @@ module.exports.hello = function(context, req) { const rawBody = req.rawBody; // unparsed body context.res = { - headers:{ - "content-type":"application/json" - }, - body: { - "hello":"world" - } - } + headers: { + 'content-type': 'application/json', + }, + body: { + hello: 'world', + }, + }; context.done(); }; ``` diff --git a/docs/providers/azure/events/other.md b/docs/providers/azure/events/other.md index b7b7167a2..e39673181 100644 --- a/docs/providers/azure/events/other.md +++ b/docs/providers/azure/events/other.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/azure/events/other) + ## Other Bindings @@ -33,16 +35,16 @@ functions: events: - queue: hello x-azure-settings: - name: item #, default - "myQueueItem", specifies which name it's available on `context.bindings` - connection: AzureWebJobsStorage #, default - "AzureWebJobsStorage", environment variable which contains Storage Account Connection String + name: item #, default - "myQueueItem", specifies which name it's available on `context.bindings` + connection: AzureWebJobsStorage #, default - "AzureWebJobsStorage", environment variable which contains Storage Account Connection String - documentDB: x-azure-settings: - name: record # Name of input parameter in function signature>", - databaseName: myDocs # "", - collectionName: todo # "", - createIfNotExists: true - connection: docDBAppSetting # "", - direction: out + name: record # Name of input parameter in function signature>", + databaseName: myDocs # "", + collectionName: todo # "", + createIfNotExists: true + connection: docDBAppSetting # "", + direction: out ``` ```javascript @@ -51,10 +53,10 @@ functions: 'use strict'; module.exports.hello = function(context, item) { - context.log("Received item: ${item}"); + context.log('Received item: ${item}'); context.bindings.record = { - hello: "world" - } + hello: 'world', + }; context.done(); }; ``` diff --git a/docs/providers/azure/events/queuestorage.md b/docs/providers/azure/events/queuestorage.md index 763911382..1dd0e8e7d 100644 --- a/docs/providers/azure/events/queuestorage.md +++ b/docs/providers/azure/events/queuestorage.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/azure/events/queuestorage) + # Queue Storage Trigger @@ -34,8 +36,8 @@ functions: events: - queue: hello x-azure-settings: - name: item #, default - "myQueueItem", specifies which name it's available on `context.bindings` - connection: AzureWebJobsStorage #, default - "AzureWebJobsStorage", environment variable which contains Storage Account Connection String + name: item #, default - "myQueueItem", specifies which name it's available on `context.bindings` + connection: AzureWebJobsStorage #, default - "AzureWebJobsStorage", environment variable which contains Storage Account Connection String ``` ```javascript @@ -44,7 +46,7 @@ functions: 'use strict'; module.exports.hello = function(context, item) { - context.log("Received item: ${item}"); + context.log('Received item: ${item}'); context.done(); }; ``` diff --git a/docs/providers/azure/events/servicebus.md b/docs/providers/azure/events/servicebus.md index 28a1dfbf8..bd539e5b0 100644 --- a/docs/providers/azure/events/servicebus.md +++ b/docs/providers/azure/events/servicebus.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/azure/events/servicebus) + # Service Bus Trigger @@ -32,10 +34,10 @@ functions: events: - serviceBus: x-azure-settings: - name: item #, default - "mySbMsg", specifies which name it's available on `context.bindings` - queueName: hello #, specifies the queue name to listen on - accessRights: manage #, specifies the permission to use when listening on the queue (manage will create queue if not exists) - connection: ServiceBusConnection #, environment variable which contains Service Bus Namespace Connection String + name: item #, default - "mySbMsg", specifies which name it's available on `context.bindings` + queueName: hello #, specifies the queue name to listen on + accessRights: manage #, specifies the permission to use when listening on the queue (manage will create queue if not exists) + connection: ServiceBusConnection #, environment variable which contains Service Bus Namespace Connection String ``` ```javascript @@ -44,7 +46,7 @@ functions: 'use strict'; module.exports.hello = function(context, item) { - context.log("Received item: ${item}"); + context.log('Received item: ${item}'); context.done(); }; ``` @@ -65,10 +67,10 @@ functions: events: - serviceBus: x-azure-settings: - name: item #, default - "mySbMsg", specifies which name it's available on `context.bindings` - topicName: "hello" #, topic to listen on - subscriptionName: "hello" #, subscription to listen on - connection: ServiceBusConnection #, environment variable which contains Service Bus Namespace Connection String + name: item #, default - "mySbMsg", specifies which name it's available on `context.bindings` + topicName: 'hello' #, topic to listen on + subscriptionName: 'hello' #, subscription to listen on + connection: ServiceBusConnection #, environment variable which contains Service Bus Namespace Connection String ``` ```javascript @@ -77,7 +79,7 @@ functions: 'use strict'; module.exports.hello = function(context, item) { - context.log("Received item: ${item}"); + context.log('Received item: ${item}'); context.done(); }; ``` diff --git a/docs/providers/azure/events/timer.md b/docs/providers/azure/events/timer.md index 929d0bc11..d06972570 100644 --- a/docs/providers/azure/events/timer.md +++ b/docs/providers/azure/events/timer.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/azure/events/timer) + # Timer Trigger @@ -33,8 +35,8 @@ functions: events: - timer: x-azure-settings: - name: timerObj #, default - "myTimer", specifies which name it's available on `context.bindings` - schedule: 0 */5 * * * * #, cron expression to run on + name: timerObj #, default - "myTimer", specifies which name it's available on `context.bindings` + schedule: 0 */5 * * * * #, cron expression to run on ``` ```javascript @@ -43,7 +45,7 @@ functions: 'use strict'; module.exports.hello = function(context, timerObj) { - context.log("Timer ran"); + context.log('Timer ran'); context.done(); }; ``` diff --git a/docs/providers/azure/examples/README.md b/docs/providers/azure/examples/README.md index 1ca7cecb4..1ee444cd5 100644 --- a/docs/providers/azure/examples/README.md +++ b/docs/providers/azure/examples/README.md @@ -5,15 +5,17 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/azure/examples/) + # Serverless Azure Functions Examples Have an example? Submit a PR or [open an issue](https://github.com/serverless/examples/issues). ⚡️ -| Example | Runtime | -|:--------------------------- |:-----| -| [azure Node Simple](https://serverless.com/examples/azure-node-simple-http-endpoint/)
    Boilerplate project repository for Azure provider with Serverless Framework. | nodeJS | +| Example | Runtime | +| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------ | +| [azure Node Simple](https://serverless.com/examples/azure-node-simple-http-endpoint/)
    Boilerplate project repository for Azure provider with Serverless Framework. | nodeJS | If you have questions, join the [chat in gitter](https://gitter.im/serverless/serverless) or [post over on the forums](https://forum.serverless.com/) diff --git a/docs/providers/azure/examples/hello-world/README.md b/docs/providers/azure/examples/hello-world/README.md index 309d3f7c8..557e5f536 100644 --- a/docs/providers/azure/examples/hello-world/README.md +++ b/docs/providers/azure/examples/hello-world/README.md @@ -6,7 +6,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/azure/examples/hello-world/) + # Hello World Serverless Example 🌍 @@ -15,6 +17,6 @@ Welcome to the Hello World example. Pick your language of choice: -* [JavaScript](./node) +- [JavaScript](./node) [View all examples](https://www.serverless.com/framework/docs/providers/azure/examples/) diff --git a/docs/providers/azure/examples/hello-world/node/README.md b/docs/providers/azure/examples/hello-world/node/README.md index 2a33bfb5a..707c22dea 100644 --- a/docs/providers/azure/examples/hello-world/node/README.md +++ b/docs/providers/azure/examples/hello-world/node/README.md @@ -6,7 +6,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/azure/examples/hello-world/node/) + # Hello World Node.js Example diff --git a/docs/providers/azure/examples/hello-world/node/serverless.yml b/docs/providers/azure/examples/hello-world/node/serverless.yml index 67ea99864..da261f3bb 100644 --- a/docs/providers/azure/examples/hello-world/node/serverless.yml +++ b/docs/providers/azure/examples/hello-world/node/serverless.yml @@ -9,11 +9,9 @@ plugins: - serverless-azure-functions functions: - hello: - handler: templates/handler.hello - events: - - http: true - x-azure-settings: - authLevel : anonymous - - \ No newline at end of file + hello: + handler: templates/handler.hello + events: + - http: true + x-azure-settings: + authLevel: anonymous diff --git a/docs/providers/azure/guide/README.md b/docs/providers/azure/guide/README.md index ee493844e..3a6892ef5 100644 --- a/docs/providers/azure/guide/README.md +++ b/docs/providers/azure/guide/README.md @@ -5,7 +5,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/azure/guide/) + # Serverless Azure Functions Guide diff --git a/docs/providers/azure/guide/credentials.md b/docs/providers/azure/guide/credentials.md index c110a64a6..7efb99cbf 100644 --- a/docs/providers/azure/guide/credentials.md +++ b/docs/providers/azure/guide/credentials.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/azure/guide/credentials) + # Azure - Credentials @@ -27,7 +29,7 @@ Here's how to get started… - Sign up for a free account @ [azure.com](https://azure.microsoft.com/en-us/services/functions/) Azure comes with a [free trial](https://azure.microsoft.com/en-us/free/) that -includes $200 of free credit. +includes \$200 of free credit. ### Interactive Login @@ -35,8 +37,8 @@ Upon running `$ serverless deploy`, you will automatically be prompted to login via your browser. Simply follow the instructions. > Note: Once you've authenticated, a new Azure "service principal" will be -created, and used for subsequent deployments. This prevents you from needing to -manually login again. +> created, and used for subsequent deployments. This prevents you from needing to +> manually login again. ### Azure Account Credentials @@ -51,74 +53,74 @@ you set your Azure endpoint before logging in. 1. Get the Azure CLI - Follow the guide on [docs.microsoft.com](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli) - or use the [Azure Cloud Shell](https://docs.microsoft.com/en-us/azure/cloud-shell/overview). + Follow the guide on [docs.microsoft.com](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli) + or use the [Azure Cloud Shell](https://docs.microsoft.com/en-us/azure/cloud-shell/overview). 2. Login to Azure - ```sh - $ az login - ``` + ```sh + $ az login + ``` - This will give you a code and prompt you to visit - [aka.ms/devicelogin](https://aka.ms/devicelogin). Provide the code and then - login with your Azure identity (this may happen automatically if you're - already logged in). You'll then be able to access your account via the CLI. + This will give you a code and prompt you to visit + [aka.ms/devicelogin](https://aka.ms/devicelogin). Provide the code and then + login with your Azure identity (this may happen automatically if you're + already logged in). You'll then be able to access your account via the CLI. 3. Get your subscription and tenant id - ```sh - $ az account list - { - "cloudName": "AzureCloud", - "id": "c6e5c9a2-a4dd-4c05-81b4-6bed04f913ea", - "isDefault": true, - "name": "My Azure Subscription", - "registeredProviders": [], - "state": "Enabled", - "tenantId": "5bc10873-159c-4cbe-a7c9-bce05cb065c1", - "user": { - "name": "hello@example.com", - "type": "user" - } - } - ``` + ```sh + $ az account list + { + "cloudName": "AzureCloud", + "id": "c6e5c9a2-a4dd-4c05-81b4-6bed04f913ea", + "isDefault": true, + "name": "My Azure Subscription", + "registeredProviders": [], + "state": "Enabled", + "tenantId": "5bc10873-159c-4cbe-a7c9-bce05cb065c1", + "user": { + "name": "hello@example.com", + "type": "user" + } + } + ``` - Save the ID of the subscription for step 5. + Save the ID of the subscription for step 5. 4. Create a service principal - ```sh - $ az ad sp create-for-rbac - { - "appId": "19f7b7c1-fc4e-4c92-8aaf-21fffc93b4c9", - "displayName": "azure-cli-1970-01-01-00-00-00", - "name": "http://azure-cli-1970-01-01-00-00-00", - "password": "48d82644-00f2-4e64-80c5-65192f9bb2d0", - "tenant": "16f63fe8-17db-476f-b2b3-ba3752a03a33" - } - ``` + ```sh + $ az ad sp create-for-rbac + { + "appId": "19f7b7c1-fc4e-4c92-8aaf-21fffc93b4c9", + "displayName": "azure-cli-1970-01-01-00-00-00", + "name": "http://azure-cli-1970-01-01-00-00-00", + "password": "48d82644-00f2-4e64-80c5-65192f9bb2d0", + "tenant": "16f63fe8-17db-476f-b2b3-ba3752a03a33" + } + ``` - This will return an JSON object containing the other pieces that you need to - authenticate with Azure. + This will return an JSON object containing the other pieces that you need to + authenticate with Azure. 5. Set up environment variables - Finally, create environment variables for subscription ID (from step 3), - tenant, name, and password. + Finally, create environment variables for subscription ID (from step 3), + tenant, name, and password. - ```sh - # bash - export azureSubId='' # From step 3 - export azureServicePrincipalTenantId='' - export azureServicePrincipalClientId='' - export azureServicePrincipalPassword='' - ``` + ```sh + # bash + export azureSubId='' # From step 3 + export azureServicePrincipalTenantId='' + export azureServicePrincipalClientId='' + export azureServicePrincipalPassword='' + ``` - ```powershell - # PowerShell - $env:azureSubId='' # From step 3 - $env:azureServicePrincipalTenantId='' - $env:azureServicePrincipalClientId='' - $env:azureServicePrincipalPassword='' - ``` + ```powershell + # PowerShell + $env:azureSubId='' # From step 3 + $env:azureServicePrincipalTenantId='' + $env:azureServicePrincipalClientId='' + $env:azureServicePrincipalPassword='' + ``` diff --git a/docs/providers/azure/guide/deploying.md b/docs/providers/azure/guide/deploying.md index a2a979054..c7a89b9f9 100644 --- a/docs/providers/azure/guide/deploying.md +++ b/docs/providers/azure/guide/deploying.md @@ -7,13 +7,15 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/azure/guide/deploying) + # Azure - Deploying The Serverless Framework was designed to provision your Azure Functions -Functions, Triggers and Rules safely and quickly. It does this via a couple of +Functions, Triggers and Rules safely and quickly. It does this via a couple of methods designed for different types of deployments. ## Deploy All @@ -28,18 +30,20 @@ Use this method when you have updated your Function, Event or Resource configuration in `serverless.yml` and you want to deploy that change (or multiple changes at the same time) to Azure Functions. +**Note:** You can specify a different configuration file name with the the `--config` option. + ### How It Works The Serverless Framework translates all syntax in `serverless.yml` to an Azure Resource Manager Template and Azure Function project. -* Provider plugin parses `serverless.yml` configuration and translates to Azure resources. -* The code of your Functions is then packaged into a directory and zipped. -* Resources are deployed in the following order: *ARM template, Functions* +- Provider plugin parses `serverless.yml` configuration and translates to Azure resources. +- The code of your Functions is then packaged into a directory and zipped. +- Resources are deployed in the following order: _ARM template, Functions_ ### Tips -* Use this in your CI/CD systems, as it is the safest method of deployment. +- Use this in your CI/CD systems, as it is the safest method of deployment. Check out the [deploy command docs](../cli-reference/deploy.md) for all details and options. @@ -55,13 +59,13 @@ serverless deploy function --function myFunction ### How It Works -* The Framework packages up the targeted Azure Function into a zip file. -* That zip file is deployed to the Function App using the kudu zip API. +- The Framework packages up the targeted Azure Function into a zip file. +- That zip file is deployed to the Function App using the kudu zip API. ### Tips -* Use this when you are developing and want to test on Azure Functions because it's much faster. -* During development, people will often run this command several times, as opposed to `serverless deploy` which is only run when larger infrastructure provisioning is required. +- Use this when you are developing and want to test on Azure Functions because it's much faster. +- During development, people will often run this command several times, as opposed to `serverless deploy` which is only run when larger infrastructure provisioning is required. Check out the [deploy command docs](../cli-reference/deploy.md) for all details and options. diff --git a/docs/providers/azure/guide/events.md b/docs/providers/azure/guide/events.md index fe07c4e26..8b0ce0561 100644 --- a/docs/providers/azure/guide/events.md +++ b/docs/providers/azure/guide/events.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/azure/guide/events) + # Azure - Events @@ -48,7 +50,7 @@ queuejs: events: - queue: YourQueueName x-azure-settings: - connection : StorageAppSettingName + connection: StorageAppSettingName - blob: x-azure-settings: name: bindingName diff --git a/docs/providers/azure/guide/functions.md b/docs/providers/azure/guide/functions.md index 699eb6fd9..2b049a1e1 100644 --- a/docs/providers/azure/guide/functions.md +++ b/docs/providers/azure/guide/functions.md @@ -7,12 +7,14 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/azure/guide/functions) + # Azure - Functions -If you are using Azure Functions as a provider, all *functions* inside the service are Azure Functions. +If you are using Azure Functions as a provider, all _functions_ inside the service are Azure Functions. ## Configuration @@ -32,11 +34,11 @@ plugins: functions: hello: - handler: templates/handler.hello - events: - - http: true - x-azure-settings: - authLevel : anonymous + handler: templates/handler.hello + events: + - http: true + x-azure-settings: + authLevel: anonymous ``` The `handler` property points to the file (default filename: handler.js) and @@ -44,15 +46,14 @@ module containing the code you want to run in your function. ```javascript // handler.js -exports.handler = function(params) {} +exports.handler = function(params) {}; ``` You can add as many functions as you want within this property. ```yml # serverless.yml -... - +--- functions: functionOne: handler: handler.functionOne @@ -67,8 +68,7 @@ You can specify an array of functions, which is useful if you separate your func ```yml # serverless.yml -... - +--- functions: - ${file(./foo-functions.yml)} - ${file(./bar-functions.yml)} diff --git a/docs/providers/azure/guide/installation.md b/docs/providers/azure/guide/installation.md index 1a9b6d48f..b816bec9c 100644 --- a/docs/providers/azure/guide/installation.md +++ b/docs/providers/azure/guide/installation.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/azure/guide/installation) + # Azure - Installation diff --git a/docs/providers/azure/guide/intro.md b/docs/providers/azure/guide/intro.md index f33a846c7..750a664d5 100644 --- a/docs/providers/azure/guide/intro.md +++ b/docs/providers/azure/guide/intro.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/azure/guide/intro) + # Azure - Introduction @@ -19,8 +21,9 @@ driven, serverless architectures, comprised of [Functions](#functions) and [Events](#events). The Serverless Framework is different than other application frameworks because: -* It manages your code as well as your infrastructure -* It supports multiple languages (Node.js, Python, Java, and more) + +- It manages your code as well as your infrastructure +- It supports multiple languages (Node.js, Python, Java, and more) ## Core Concepts @@ -33,9 +36,9 @@ 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* -* *Performing a scheduled task* +- _Saving a user to the database_ +- _Processing a file in a database_ +- _Performing a scheduled task_ You can perform multiple jobs in your code, but we don't recommend doing that without good reason. Separation of concerns is best and the Framework is designed @@ -46,12 +49,12 @@ to help you easily develop and deploy Functions, as well as manage lots of them. Anything that triggers an Azure Function to execute is regarded by the Framework as an **Event**. Events are platform events on Azure Functions such as: -* *An HTTP Trigger (e.g., for a REST API)* -* *A scheduled timer (e.g., run every 5 minutes)* -* *A Service Bus Queue trigger (e.g. a workitem from another Function)* -* *An IoT/Event Hub message (e.g., a message from a device or service)* -* *A Webhook fires (e.g., Github project update)* -* *And more...* +- _An HTTP Trigger (e.g., for a REST API)_ +- _A scheduled timer (e.g., run every 5 minutes)_ +- _A Service Bus Queue trigger (e.g. a workitem from another Function)_ +- _An IoT/Event Hub message (e.g., a message from a device or service)_ +- _A Webhook fires (e.g., Github project update)_ +- _And more..._ When you define an event for your Azure Function in the Serverless Framework, the Framework will automatically translate this into @@ -63,7 +66,7 @@ needed for that event and configure your functions to listen to it. A **Service** is the Framework's unit of organization. You can think of it as a project file, though you can have multiple services for a single application. It's where you define your Functions, the Events that trigger them, and the -Resources your Functions use, all in one file entitled `serverless.yml` (or +Resources your Functions use, all in one file by default entitled `serverless.yml` (or `serverless.json` or `serverless.js`). It looks like this: ```yml @@ -78,7 +81,7 @@ functions: # Your "Functions" x-azure-settings: name: req methods: - - post + - post route: /users/create usersDelete: events: @@ -86,12 +89,12 @@ functions: # Your "Functions" x-azure-settings: name: req methods: - - delete + - delete route: /users/delete ``` When you deploy with the Framework by running `serverless deploy`, everything in -`serverless.yml` is deployed at once. +`serverless.yml` (or the file specified with the `--config` option) is deployed at once. ### Plugins diff --git a/docs/providers/azure/guide/packaging.md b/docs/providers/azure/guide/packaging.md index e6ffd6b87..3a3147836 100644 --- a/docs/providers/azure/guide/packaging.md +++ b/docs/providers/azure/guide/packaging.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/azure/guide/packaging) + # Azure - Packaging @@ -57,7 +59,7 @@ files and directories. Exclude all node_modules but then re-include a specific modules (in this case node-fetch) using `exclude` exclusively -``` yml +```yml package: exclude: - node_modules/** @@ -66,7 +68,7 @@ package: Exclude all files but `handler.js` using `exclude` and `include` -``` yml +```yml package: exclude: - src/** diff --git a/docs/providers/azure/guide/plugins.md b/docs/providers/azure/guide/plugins.md index 5263fa3b9..e1a7e9ad1 100644 --- a/docs/providers/azure/guide/plugins.md +++ b/docs/providers/azure/guide/plugins.md @@ -7,16 +7,18 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/azure/guide/plugins) + # Azure - Plugins A Plugin is custom JavaScript code that creates new or extends existing commands -within the Serverless Framework. The Serverless Framework is merely a group of -Plugins that are provided in the core. If you or your organization have a +within the Serverless Framework. The Serverless Framework is merely a group of +Plugins that are provided in the core. If you or your organization have a specific workflow, install a pre-written Plugin or write a plugin to customize -the Framework to your needs. External Plugins are written exactly the same way +the Framework to your needs. External Plugins are written exactly the same way as the core Plugins. - [How to create serverless plugins - Part 1](https://serverless.com/blog/writing-serverless-plugins/) @@ -42,9 +44,11 @@ do this by adding the name of the Plugin to the `plugins` section in the plugins: - custom-serverless-plugin ``` + The `plugins` section supports two formats: Array object: + ```yml plugins: - plugin1 @@ -52,6 +56,7 @@ plugins: ``` Enhanced plugins object: + ```yml plugins: localPath: './custom_serverless_plugins' @@ -75,18 +80,21 @@ custom: If you are working on a plugin or have a plugin that is just designed for one project they can be loaded from the local folder. Local plugins can be added in the `plugins` array in `serverless.yml`. By default local plugins can be added to the `.serverless_plugins` directory at the root of your service, and in the `plugins` array in `serverless.yml`. + ```yml plugins: - custom-serverless-plugin ``` Local plugins folder can be changed by enhancing `plugins` object: + ```yml plugins: localPath: './custom_serverless_plugins' modules: - custom-serverless-plugin ``` + The `custom-serverless-plugin` will be loaded from the `custom_serverless_plugins` directory at the root of your service. If the `localPath` is not provided or empty `.serverless_plugins` directory will be taken as the `localPath`. The plugin will be loaded based on being named `custom-serverless-plugin.js` or `custom-serverless-plugin\index.js` in the root of `localPath` folder (`.serverless_plugins` by default). @@ -111,18 +119,18 @@ In this case `plugin1` is loaded before `plugin2`. #### Plugin -Code which defines *Commands*, any *Events* within a *Command*, and any *Hooks* -assigned to an *Lifecycle Event*. +Code which defines _Commands_, any _Events_ within a _Command_, and any _Hooks_ +assigned to an _Lifecycle Event_. -* Command // CLI configuration, commands, subcommands, options - * LifecycleEvent(s) // Events that happen sequentially when the command is run - * Hook(s) // Code that runs when a Lifecycle Event happens during a Command +- Command // CLI configuration, commands, subcommands, options + - LifecycleEvent(s) // Events that happen sequentially when the command is run + - Hook(s) // Code that runs when a Lifecycle Event happens during a Command #### Command -A CLI *Command* that can be called by a user, e.g. `serverless deploy`. A +A CLI _Command_ that can be called by a user, e.g. `serverless deploy`. A Command has no logic, but simply defines the CLI configuration (e.g. command, -subcommands, parameters) and the *Lifecycle Events* for the command. Every +subcommands, parameters) and the _Lifecycle Events_ for the command. Every command defines its own lifecycle events. ```javascript @@ -132,10 +140,7 @@ class MyPlugin { constructor() { this.commands = { deploy: { - lifecycleEvents: [ - 'resources', - 'functions' - ] + lifecycleEvents: ['resources', 'functions'], }, }; } @@ -146,9 +151,9 @@ module.exports = MyPlugin; #### Lifecycle Events -Events that fire sequentially during a Command. The above example list two -Events. However, for each Event, and additional `before` and `after` event is -created. Therefore, six Events exist in the above example: +Events that fire sequentially during a Command. The above example list two +Events. However, for each Event, and additional `before` and `after` event is +created. Therefore, six Events exist in the above example: - `before:deploy:resources` - `deploy:resources` @@ -170,17 +175,14 @@ class Deploy { constructor() { this.commands = { deploy: { - lifecycleEvents: [ - 'resources', - 'functions' - ] + lifecycleEvents: ['resources', 'functions'], }, }; this.hooks = { 'before:deploy:resources': this.beforeDeployResources, 'deploy:resources': this.deployResources, - 'after:deploy:functions': this.afterDeployFunctions + 'after:deploy:functions': this.afterDeployFunctions, }; } @@ -202,8 +204,7 @@ module.exports = Deploy; ### Nesting Commands -You can also nest commands, e.g. if you want to provide a command `serverless -deploy single`. Those nested commands have their own lifecycle events and do not +You can also nest commands, e.g. if you want to provide a command `serverless deploy single`. Those nested commands have their own lifecycle events and do not inherit them from their parents. ```javascript @@ -213,20 +214,14 @@ class MyPlugin { constructor() { this.commands = { deploy: { - lifecycleEvents: [ - 'resources', - 'functions' - ], + lifecycleEvents: ['resources', 'functions'], commands: { function: { - lifecycleEvents: [ - 'package', - 'deploy' - ], + lifecycleEvents: ['package', 'deploy'], }, }, }, - } + }; } } @@ -245,7 +240,7 @@ The `options` object will be passed in as the second parameter to the constructo of your plugin. In it, you can optionally add a `shortcut` property, as well as a `required` -property. The Framework will return an error if a `required` Option is not +property. The Framework will return an error if a `required` Option is not included. **Note:** At this time, the Serverless Framework does not use parameters. @@ -260,22 +255,20 @@ class Deploy { this.commands = { deploy: { - lifecycleEvents: [ - 'functions' - ], + lifecycleEvents: ['functions'], options: { function: { usage: 'Specify the function you want to deploy (e.g. "--function myFunction")', shortcut: 'f', - required: true - } - } + required: true, + }, + }, }, }; this.hooks = { - 'deploy:functions': this.deployFunction.bind(this) - } + 'deploy:functions': this.deployFunction.bind(this), + }; } deployFunction() { @@ -308,21 +301,19 @@ class ProviderDeploy { this.commands = { deploy: { - lifecycleEvents: [ - 'functions' - ], + lifecycleEvents: ['functions'], options: { function: { usage: 'Specify the function you want to deploy (e.g. "--function myFunction")', - required: true - } - } + required: true, + }, + }, }, }; this.hooks = { - 'deploy:functions': this.deployFunction.bind(this) - } + 'deploy:functions': this.deployFunction.bind(this), + }; } deployFunction() { @@ -352,15 +343,13 @@ class MyPlugin { this.commands = { log: { - lifecycleEvents: [ - 'serverless' - ], + lifecycleEvents: ['serverless'], }, }; this.hooks = { - 'log:serverless': this.logServerless.bind(this) - } + 'log:serverless': this.logServerless.bind(this), + }; } logServerless() { diff --git a/docs/providers/azure/guide/quick-start.md b/docs/providers/azure/guide/quick-start.md index e9350a48a..f1143e1d7 100644 --- a/docs/providers/azure/guide/quick-start.md +++ b/docs/providers/azure/guide/quick-start.md @@ -10,11 +10,11 @@ layout: Doc ## Pre-requisites -1. Node.js `v6.5.0` or later. *(v6.5.0 is the minimum runtime version supported by Azure Functions)* +1. Node.js `v6.5.0` or later. _(v6.5.0 is the minimum runtime version supported by Azure Functions)_ 2. Serverless CLI `v1.9.0` or later. You can run -`npm install -g serverless` to install it. + `npm install -g serverless` to install it. 3. Azure plugin that allows you to work with Azure Functions `npm install -g serverless-azure-functions` -4. An Azure account. If you don't already have one, you can sign up for a [free trial](https://azure.microsoft.com/en-us/free/) that includes $200 of free credit. +4. An Azure account. If you don't already have one, you can sign up for a [free trial](https://azure.microsoft.com/en-us/free/) that includes \$200 of free credit. 5. **Set-up your [Provider Credentials](./credentials.md)**. ## Create a new service @@ -55,46 +55,46 @@ Note: The file `{function name}/function.json` is included in the template for t 1. **Deploy the Service:** - Deploy your new service to Azure! The first time you do this, you will be asked - to authenticate with your Azure account, so the `serverless` CLI can manage - Functions on your behalf. Simply follow the provided instructions, and the - deployment will continue as soon as the authentication process is completed. +Deploy your new service to Azure! The first time you do this, you will be asked +to authenticate with your Azure account, so the `serverless` CLI can manage +Functions on your behalf. Simply follow the provided instructions, and the +deployment will continue as soon as the authentication process is completed. - ```bash - serverless deploy - ``` +```bash +serverless deploy +``` - > Note: Once you've authenticated, a new Azure "service principal" will be - created, and used for subsequent deployments. This prevents you from needing to - manually login again. See [below](#advanced-authentication) if you'd prefer to - use a custom service principal instead. +> Note: Once you've authenticated, a new Azure "service principal" will be +> created, and used for subsequent deployments. This prevents you from needing to +> manually login again. See [below](#advanced-authentication) if you'd prefer to +> use a custom service principal instead. 2. **Deploy the Function** - Use this to quickly upload and overwrite your function code,allowing you to - develop faster. If you're working on a single function, you can simply deploy - the specified function instead of the entire service. +Use this to quickly upload and overwrite your function code,allowing you to +develop faster. If you're working on a single function, you can simply deploy +the specified function instead of the entire service. - ```bash - serverless deploy function -f hello - ``` +```bash +serverless deploy function -f hello +``` 3. **Invoke the Function** - Invoke a function, in order to test that it works: +Invoke a function, in order to test that it works: - ```bash - serverless invoke -f hello - ``` +```bash +serverless invoke -f hello +``` 4. **Fetch the Function Logs** - Open up a separate tab in your console and stream all logs for a specific - Function using this command. +Open up a separate tab in your console and stream all logs for a specific +Function using this command. - ```bash - serverless logs -f hello -t - ``` +```bash +serverless logs -f hello -t +``` ## Cleanup @@ -117,6 +117,7 @@ yourself, you can indicate that this plugin should use its credentials instead, by setting the following environment variables: **Bash** + ```bash export azureSubId='' export azureServicePrincipalTenantId='' @@ -125,6 +126,7 @@ export azureServicePrincipalPassword='' ``` **Powershell** + ```powershell $env:azureSubId='' $env:azureServicePrincipalTenantId='' diff --git a/docs/providers/azure/guide/services.md b/docs/providers/azure/guide/services.md index 1ab4bdbe6..40b9a2762 100644 --- a/docs/providers/azure/guide/services.md +++ b/docs/providers/azure/guide/services.md @@ -7,13 +7,15 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/azure/guide/services) + # Azure - Services -A `service` is like a project. It's where you define your Azure Functions, the -`events` that trigger them and any `resources` they require, all in a file +A `service` is like a project. It's where you define your Azure Functions, the +`events` that trigger them and any `resources` they require, all in a file called `serverless.yml`. To get started building your first Serverless Framework project, create a @@ -22,7 +24,7 @@ To get started building your first Serverless Framework project, create a ## Organization In the beginning of an application, many people use a single Service to define -all of the Functions, Events and Resources for that project. This is what we +all of the Functions, Events and Resources for that project. This is what we recommend in the beginning. ```bash @@ -60,6 +62,7 @@ serverless create -t azure-nodejs --path ## Contents You'll see the following files in your working directory: + - `serverless.yml` - `handler.js` @@ -69,11 +72,11 @@ Each `service` configuration is managed in the `serverless.yml` file. The main r - Declare a Serverless service - Define one or more functions in the service - - Define the provider the service will be deployed to (and the runtime if provided) - - Define any custom plugins to be used - - Define events that trigger each function to execute (e.g. HTTP requests) - - Allow events listed in the `events` section to automatically create the resources required for the event upon deployment - - Allow flexible configuration using Serverless Variables + - Define the provider the service will be deployed to (and the runtime if provided) + - Define any custom plugins to be used + - Define events that trigger each function to execute (e.g. HTTP requests) + - Allow events listed in the `events` section to automatically create the resources required for the event upon deployment + - Allow flexible configuration using Serverless Variables You can see the name of the service, the provider configuration and the first function inside the `functions` definition which points to the `handler.js` file. Any further service configuration will be done in this file. @@ -91,11 +94,11 @@ plugins: functions: hello: - handler: handler.hello - events: - - http: true - x-azure-settings: - authLevel : anonymous + handler: handler.hello + events: + - http: true + x-azure-settings: + authLevel: anonymous ``` ### handler.js @@ -128,7 +131,7 @@ serverless deploy ``` Check out the [deployment guide](https://serverless.com/framework/docs/providers/azure/guide/deploying/) -to learn more about deployments and how they work. Or, check out the +to learn more about deployments and how they work. Or, check out the [deploy command docs](../cli-reference/deploy) for all the details and options. ## Removal @@ -150,8 +153,7 @@ on. ## Version Pinning -The Serverless framework is usually installed globally via `npm install -g -serverless`. This way you have the Serverless CLI available for all your +The Serverless framework is usually installed globally via `npm install -g serverless`. This way you have the Serverless CLI available for all your services. Installing tools globally has the downside that the version can't be pinned @@ -192,7 +194,6 @@ frameworkVersion: ">=1.0.0 <2.0.0" … ``` - ## Installing Serverless in an existing service If you already have a Serverless service, and would prefer to lock down the @@ -210,6 +211,7 @@ To execute the locally installed Serverless executable you have to reference the binary out of the node modules directory. Example: + ``` node ./node_modules/serverless/bin/serverless deploy ``` diff --git a/docs/providers/azure/guide/testing.md b/docs/providers/azure/guide/testing.md index dae305f0c..2a0be29fb 100644 --- a/docs/providers/azure/guide/testing.md +++ b/docs/providers/azure/guide/testing.md @@ -7,26 +7,28 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/azure/guide/testing) + # Azure - Testing While the Serverless Architecture introduces a lot of simplicity when it comes to serving business logic, some of its characteristics present challenges for -testing. They are: +testing. They are: -* The Serverless Architecture is an integration of separate, distributed services, which must be tested both independently, and together. -* The Serverless Architecture is dependent on internet/cloud services, which are hard to emulate locally. -* The Serverless Architecture can feature event-driven, asynchronous workflows, which are hard to emulate entirely. +- The Serverless Architecture is an integration of separate, distributed services, which must be tested both independently, and together. +- The Serverless Architecture is dependent on internet/cloud services, which are hard to emulate locally. +- The Serverless Architecture can feature event-driven, asynchronous workflows, which are hard to emulate entirely. To get through these challenges, and to keep the [test pyramid](http://martinfowler.com/bliki/TestPyramid.html) in mind, keep the following principles in mind: -* Write your business logic so that it is separate from your FaaS provider (e.g., Azure Functions), to keep it provider-independent, reusable and more easily testable. -* When your business logic is written separately from the FaaS provider, you can write traditional Unit Tests to ensure it is working properly. -* Write Integration Tests to verify integrations with other services are working correctly. +- Write your business logic so that it is separate from your FaaS provider (e.g., Azure Functions), to keep it provider-independent, reusable and more easily testable. +- When your business logic is written separately from the FaaS provider, you can write traditional Unit Tests to ensure it is working properly. +- Write Integration Tests to verify integrations with other services are working correctly. ## A Poor Example @@ -42,10 +44,10 @@ module.exports.saveUser = (context, req) => { return Promise((resolve, reject) => { const user = { email: req.params.email, - created_at: Date.now() - } + created_at: Date.now(), + }; - db.saveUser(user, function (err) { + db.saveUser(user, function(err) { if (err) { reject(err); } else { @@ -53,14 +55,14 @@ module.exports.saveUser = (context, req) => { resolve(); } }); - }) + }); }; ``` There are two main problems with this function: -* The business logic is not separate from the FaaS Provider. It's bounded to how Azure Functions passes incoming data (`params` object). -* Testing this function will rely on separate services. Specifically, running a database instance and a mail server. +- The business logic is not separate from the FaaS Provider. It's bounded to how Azure Functions passes incoming data (`params` object). +- Testing this function will rely on separate services. Specifically, running a database instance and a mail server. ## Writing Testable Azure Functions Functions @@ -78,10 +80,10 @@ class Users { return new Promise((resolve, reject) => { const user = { email: email, - created_at: Date.now() - } + created_at: Date.now(), + }; - this.db.saveUser(user, function (err) { + this.db.saveUser(user, function(err) { if (err) { reject(err); } else { @@ -89,7 +91,7 @@ class Users { resolve(); } }); - }) + }); } } @@ -108,17 +110,17 @@ module.exports.saveUser = (context, req) => { }; ``` -Now, the above class keeps business logic separate. Further, the code +Now, the above class keeps business logic separate. Further, the code responsible for setting up dependencies, injecting them, calling business logic functions and interacting with Azure Functions is in its own file, which will be -changed less often. This way, the business logic is not provider dependent, +changed less often. This way, the business logic is not provider dependent, easier to re-use, and easier to test. -Further, this code doesn't require running any external services. Instead of a +Further, this code doesn't require running any external services. Instead of a real `db` and `mailer` services, we can pass mocks and assert if `saveUser` and `sendWelcomeEmail` has been called with proper arguments. -Unit Tests can easily be written to cover the above class. An integration test +Unit Tests can easily be written to cover the above class. An integration test can be added by invoking the function (`serverless invoke`) with fixture email address, check if user is actually saved to DB and check if email was received to see if everything is working together. diff --git a/docs/providers/azure/guide/variables.md b/docs/providers/azure/guide/variables.md index 57648171b..12220124c 100644 --- a/docs/providers/azure/guide/variables.md +++ b/docs/providers/azure/guide/variables.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/azure/guide/variables) + # Azure - Variables @@ -55,7 +57,8 @@ referencing the `globalSchedule` property in the same `serverless.yml` file. Thi way, you can easily change the schedule for all functions whenever you like. ## Reference Variables in other Files -You can reference variables in other YAML or JSON files. To reference variables in other YAML files use the `${file(./myFile.yml):someProperty}` syntax in your `serverless.yml` configuration file. To reference variables in other JSON files use the `${file(./myFile.json):someProperty}` syntax. It is important that the file you are referencing has the correct suffix, or file extension, for its file type (`.yml` for YAML or `.json` for JSON) in order for it to be interpreted correctly. Here's an example: + +You can reference variables in other YAML or JSON files. To reference variables in other YAML files use the `${file(./myFile.yml):someProperty}` syntax in your `serverless.yml` configuration file. To reference variables in other JSON files use the `${file(./myFile.json):someProperty}` syntax. It is important that the file you are referencing has the correct suffix, or file extension, for its file type (`.yml` for YAML or `.json` for JSON) in order for it to be interpreted correctly. Here's an example: ```yml # myCustomFile.yml @@ -80,8 +83,7 @@ functions: - timer: ${self:custom.cron} # This would also work in this case ``` - -In the above example, you're referencing the entire `myCustomFile.yml` file in the `custom` property. You need to pass the path relative to your service directory. You can also request specific properties in that file as shown in the `cron` property. It's completely recursive and you can go as deep as you want. Additionally you can request properties that contain arrays from either YAML or JSON reference files. Here's a YAML example for an events array: +In the above example, you're referencing the entire `myCustomFile.yml` file in the `custom` property. You need to pass the path relative to your service directory. You can also request specific properties in that file as shown in the `cron` property. It's completely recursive and you can go as deep as you want. Additionally you can request properties that contain arrays from either YAML or JSON reference files. Here's a YAML example for an events array: ```yml myevents: @@ -89,15 +91,19 @@ myevents: ``` and for JSON: + ```json { - "myevents": [{ - "timer" : "cron(0 * * * *)" - }] + "myevents": [ + { + "timer": "cron(0 * * * *)" + } + ] } ``` In your serverless.yml, depending on the type of your source file, either have the following syntax for YAML + ```yml functions: hello: @@ -106,6 +112,7 @@ functions: ``` or for a JSON reference file use this sytax: + ```yml functions: hello: @@ -124,9 +131,9 @@ References can be either named or unnamed exports. To use the exported `someModu ```js // scheduleConfig.js module.exports.cron = () => { - // Code that generates dynamic data - return 'cron(0 * * * *)'; -} + // Code that generates dynamic data + return 'cron(0 * * * *)'; +}; ``` ```js @@ -134,9 +141,9 @@ module.exports.cron = () => { module.exports = () => { return { property1: 'some value', - property2: 'some other value' - } -} + property2: 'some other value', + }; +}; ``` ```yml @@ -148,12 +155,12 @@ custom: ${file(./config.js)} functions: hello: - handler: handler.hello - events: - - timer: ${file(./scheduleConfig.js):cron} # Reference a specific module + handler: handler.hello + events: + - timer: ${file(./scheduleConfig.js):cron} # Reference a specific module ``` -You can also return an object and reference a specific property. Just make sure +You can also return an object and reference a specific property. Just make sure you are returning a valid object and referencing a valid property: ```yml @@ -162,19 +169,19 @@ service: new-service provider: azure functions: scheduledFunction: - handler: handler.scheduledFunction - events: - - timer: ${file(./myCustomFile.js):schedule.hour} + handler: handler.scheduledFunction + events: + - timer: ${file(./myCustomFile.js):schedule.hour} ``` ```js // myCustomFile.js module.exports.schedule = () => { - // Code that generates dynamic data - return { - hour: 'cron(0 * * * *)' - }; -} + // Code that generates dynamic data + return { + hour: 'cron(0 * * * *)', + }; +}; ``` ## Multiple Configuration Files diff --git a/docs/providers/azure/guide/workflow.md b/docs/providers/azure/guide/workflow.md index 3bd43a0cd..e59439bbf 100644 --- a/docs/providers/azure/guide/workflow.md +++ b/docs/providers/azure/guide/workflow.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/azure/guide/workflow) + # Azure - Workflow @@ -19,16 +21,18 @@ Intro. Quick recommendations and tips for various processes. 1. Write your functions 2. Use `serverless deploy` only when you've made changes to `serverless.yml` and in CI/CD systems. 3. Use `serverless deploy function -f myFunction` to rapidly deploy changes when you are working on a specific Azure Functions Function. -4. Use `serverless invoke -f myFunction ` to test your Azure Functions. +4. Use `serverless invoke -f myFunction` to test your Azure Functions. 5. Open up a separate tab in your console and stream logs in there via `serverless logs -f myFunction`. 6. Write tests to run locally. ### Larger Projects -* Break your application/project into multiple Serverless Services. -* Model your Serverless Services around Data Models or Workflows. -* Keep the Functions and Resources in your Serverless Services to a minimum. + +- Break your application/project into multiple Serverless Services. +- Model your Serverless Services around Data Models or Workflows. +- Keep the Functions and Resources in your Serverless Services to a minimum. ## Cheat Sheet + A handy list of commands to use when developing with the Serverless Framework. ##### Create A Service: diff --git a/docs/providers/cloudflare/README.md b/docs/providers/cloudflare/README.md index 5cdc83273..98c39c31f 100644 --- a/docs/providers/cloudflare/README.md +++ b/docs/providers/cloudflare/README.md @@ -5,7 +5,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/) + # Cloudflare Provider Documentation diff --git a/docs/providers/cloudflare/cli-reference/README.md b/docs/providers/cloudflare/cli-reference/README.md index 5e37d8038..18961ec88 100644 --- a/docs/providers/cloudflare/cli-reference/README.md +++ b/docs/providers/cloudflare/cli-reference/README.md @@ -5,11 +5,13 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/cloudflare/cli-reference/) + # Serverless Cloudflare Workers CLI Reference -Welcome to the Serverless Cloudflare Workers CLI Reference! Please select a section on the left to get started. +Welcome to the Serverless Cloudflare Workers CLI Reference! Please select a section on the left to get started. If you have questions, join the [chat in gitter](https://gitter.im/serverless/serverless) or [post over on the forums](http://forum.serverless.com/). diff --git a/docs/providers/cloudflare/cli-reference/create.md b/docs/providers/cloudflare/cli-reference/create.md index 645dc4726..7db43ea39 100644 --- a/docs/providers/cloudflare/cli-reference/create.md +++ b/docs/providers/cloudflare/cli-reference/create.md @@ -7,71 +7,83 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/cloudflare/cli-reference/create) + - # Cloudflare Workers - Create + Creates a new Serverless service in the current working directory based on the provided template. - + **Create service in current working directory:** - -```bash + +```bash serverless create --template cloudflare-workers ``` Or for Enterprise Cloudflare accounts: -```bash +```bash serverless create --template cloudflare-workers-enterprise ``` **Create service in new folder:** - + ```bash serverless create --template cloudflare-workers --path my-service ``` Or for Enterprise Cloudflare accounts: -```bash +```bash serverless create --template cloudflare-workers-enterprise --path my-service ``` ## Options + - `--template` or `-t` The name of one of the available templates. Required if --template-url and --template-path are not present. - `--template-url` or `-u` The name of one of the available templates. Required if --template and --template-path are not present. - `--template-path` The local path of your template. Required if --template and --template-url are not present. - `--path` or `-p` The path where the service should be created. - `--name` or `-n` the name of the service in `serverless.yml`. + ## Provided lifecycle events + - `create:create` + ## Available Templates for Cloudflare Workers + To see a list of available templates run `serverless create --help` These are the current available templates for Cloudflare Workers: - + - cloudflare-workers - cloudflare-workers-enterprise - cloudflare-workers-rust ## Examples + ### Creating a new service + ```bash serverless create --template cloudflare-workers --name my-special-service ``` This example will generate scaffolding for a service with `Cloudflare` as a provider. The scaffolding will be generated in the current working directory. - + ### Creating a named service in a (new) directory + ```bash serverless create --template cloudflare-workers --path my-new-service ``` This example will generate scaffolding for a service with `Cloudflare` as a provider. 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`. - + ### Creating a new service using a local template + ```bash serverless create --template-path path/to/my/template/folder --path path/to/my/service --name my-new-service ``` + This will copy the `path/to/my/template/folder` folder into `path/to/my/service` and rename the service to `my-new-service`. diff --git a/docs/providers/cloudflare/cli-reference/deploy.md b/docs/providers/cloudflare/cli-reference/deploy.md index 6e9c3e5fb..0fb6fc741 100644 --- a/docs/providers/cloudflare/cli-reference/deploy.md +++ b/docs/providers/cloudflare/cli-reference/deploy.md @@ -7,11 +7,13 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/cloudflare/cli-reference/deploy) + - # Cloudflare Workers - Deploy + In order to be able to deploy any Cloudflare Workers, You will need to set your Global API key from Cloudflare as an environmental variable named `CLOUDFLARE_AUTH_KEY`, and your Cloudflare account email as an environmental variable named `CLOUDFLARE_AUTH_EMAIL`. You can get your Global API key from your [Cloudflare profile](https://dash.cloudflare.com/profile) page. You will also need to set `accountId` and `zoneId` in `serverless.yml` under `service.config`. The first part of the path when you open [Cloudflare dashboard](https://dash.cloudflare.com/) as a logged in user is your `accountId`, e.g. `dash.cloudflare.com/{accountId}`. And the `zoneId` can be found from the overview tab after selecting the desired zone from the [Cloudflare dashboard](https://dash.cloudflare.com/). Environmental variables are variables that live inside your terminal. @@ -34,17 +36,20 @@ You’ll need to redefine your environmental variables after each time you close The `serverless deploy` command deploys your entire service via the Cloudflare Workers API. Run this command when you have made service changes (i.e., you edited `serverless.yml`). Use `serverless deploy -f my-function` when you have made code changes and you want to quickly upload your updated code to Cloudflare. - + ```bash serverless deploy ``` This is the simplest deployment usage possible. With this command, Serverless will deploy your service to Cloudflare. - + ## Options + +- `--config` or `-c` Path to your conifguration file, if other than `serverless.yml|.yaml|.js|.json`. - `--verbose` or `-v`: Shows all stack events during deployment, and display any Stack Output. - `--function` or `-f`: Invokes `deploy function` (see above). Convenience shortcut ## Provided lifecycle events + - `deploy:deploy` - `deploy:function:deploy` diff --git a/docs/providers/cloudflare/cli-reference/invoke.md b/docs/providers/cloudflare/cli-reference/invoke.md index fc029c205..170302dbc 100644 --- a/docs/providers/cloudflare/cli-reference/invoke.md +++ b/docs/providers/cloudflare/cli-reference/invoke.md @@ -7,13 +7,15 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/cloudflare-workers/cli-reference/invoke) + - # Cloudflare Workers - Invoke + Invokes a deployed function. It allows you to send an event to a deployed function, which can be useful for testing. Cloudflare Workers only support `GET` requests for now. The optional `headers` field allows you to specify headers that will be sent to your Worker along with your request. - + ```bash serverless invoke --function functionName ``` @@ -22,7 +24,7 @@ In the following example, you could run: ```bash serverless invoke --function helloWorld -``` +``` ```yml # serverless.yml @@ -43,23 +45,27 @@ functions: ``` ## Options -* `--function` or `-f` The name of the function in your service that you want to invoke. Required. -* `--data` or `-d` String data to be passed as an event to your function. By default data is read from standard input. -* `--path` or `-p` The path to a json file with input data to be passed to the invoked function. This path is relative to the root directory of the service. + +- `--function` or `-f` The name of the function in your service that you want to invoke. Required. +- `--data` or `-d` String data to be passed as an event to your function. By default data is read from standard input. +- `--path` or `-p` The path to a json file with input data to be passed to the invoked function. This path is relative to the root directory of the service. ## Provided lifecycle events + - `invoke:invoke` ## Examples ### Cloudflare Workers + ```bash serverless invoke --function functionName ``` This example will invoke your deployed function on the configured Cloudflare Workers API URL endpoint. This will output the result of the request in your terminal. - + #### Function invocation with data + ```bash serverless invoke --function functionName -``` \ No newline at end of file +``` diff --git a/docs/providers/cloudflare/cli-reference/plugin-install.md b/docs/providers/cloudflare/cli-reference/plugin-install.md index 0bb19f137..95fbcfe48 100644 --- a/docs/providers/cloudflare/cli-reference/plugin-install.md +++ b/docs/providers/cloudflare/cli-reference/plugin-install.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/cloudflare/cli-reference/plugin-install) + # Plugin Install @@ -22,9 +24,11 @@ serverless plugin install --name pluginName ``` ## Options + - `--name` or `-n` The plugins name. **Required**. ## Provided lifecycle events + - `plugin:install:install` ## Examples diff --git a/docs/providers/cloudflare/cli-reference/plugin-list.md b/docs/providers/cloudflare/cli-reference/plugin-list.md index c7b74d804..09536b034 100644 --- a/docs/providers/cloudflare/cli-reference/plugin-list.md +++ b/docs/providers/cloudflare/cli-reference/plugin-list.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/cloudflare/cli-reference/plugin-list) + # Plugin List @@ -19,7 +21,9 @@ serverless plugin list ``` ## Options -- *None* + +- _None_ ## Provided lifecycle events + - `plugin:list:list` diff --git a/docs/providers/cloudflare/cli-reference/plugin-search.md b/docs/providers/cloudflare/cli-reference/plugin-search.md index 4f793f035..ff49eadc8 100644 --- a/docs/providers/cloudflare/cli-reference/plugin-search.md +++ b/docs/providers/cloudflare/cli-reference/plugin-search.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/cloudflare/cli-reference/plugin-search) + # Plugin Search @@ -19,9 +21,11 @@ serverless plugin search --query query ``` ## Options + - `--query` or `-q` The query you want to use for your search. **Required**. ## Provided lifecycle events + - `plugin:search:search` ## Examples diff --git a/docs/providers/cloudflare/cli-reference/plugin-uninstall.md b/docs/providers/cloudflare/cli-reference/plugin-uninstall.md index 74852fb6b..a1288d470 100644 --- a/docs/providers/cloudflare/cli-reference/plugin-uninstall.md +++ b/docs/providers/cloudflare/cli-reference/plugin-uninstall.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/cloudflare/cli-reference/plugin-uninstall) + # Plugin Uninstall @@ -19,9 +21,11 @@ serverless plugin uninstall --name pluginName ``` ## Options + - `--name` or `-n` The plugins name. **Required**. ## Provided lifecycle events + - `plugin:uninstall:uninstall` ## Examples diff --git a/docs/providers/cloudflare/cli-reference/remove.md b/docs/providers/cloudflare/cli-reference/remove.md index 505bc0df1..15d566627 100644 --- a/docs/providers/cloudflare/cli-reference/remove.md +++ b/docs/providers/cloudflare/cli-reference/remove.md @@ -7,15 +7,21 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/cloudflare/cli-reference/remove) + # Cloudflare Workers - Remove + The `serverless remove` command will remove the deployed service, defined in your current working directory, from the provider. - + ```bash serverless remove ``` + It will remove the Cloudflare Worker functions from the Cloudflare. + ## Provided lifecycle events + - `remove:remove` diff --git a/docs/providers/cloudflare/events/README.md b/docs/providers/cloudflare/events/README.md index 09b25c7a8..9d103379a 100644 --- a/docs/providers/cloudflare/events/README.md +++ b/docs/providers/cloudflare/events/README.md @@ -5,7 +5,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/cloudflare/events/) + # Serverless Cloudflare Workers Events @@ -15,4 +17,3 @@ Welcome to the Serverless Cloudflare Workers Events Glossary! Please select a section on the left to get started. If you have questions, join the [chat in gitter](https://gitter.im/serverless/serverless) or [post over on the forums](http://forum.serverless.com/) - diff --git a/docs/providers/cloudflare/events/http.md b/docs/providers/cloudflare/events/http.md index e618a0f4e..ecff7e8ab 100644 --- a/docs/providers/cloudflare/events/http.md +++ b/docs/providers/cloudflare/events/http.md @@ -5,20 +5,22 @@ menuOrder: 1 description: HTTP Events in Cloudflare Workers layout: Doc --> - + + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/cloudflare/events/http) + - # Cloudflare Workers - HTTP Events - + ## Serverless Yml -When creating a service your serverless yml will define which endpoint is used for your function and when you run the [`serverless invoke`](../cli-reference/invoke.md) command. - + +When creating a service your serverless yml will define which endpoint is used for your function and when you run the [`serverless invoke`](../cli-reference/invoke.md) command. + ```yml # serverless.yml -... +--- functions: helloWorld: # What the script will be called on Cloudflare (this property value must match the function name one line above) @@ -33,4 +35,4 @@ functions: greeting: hi ``` -The events section in the yml above makes it so that the Function helloWorld will be used for request to the `example.com/hello/user` endpoint. This configuration would send a GET request with a header called `greeting` that has a value of `hi` to the `example.com/hello/user` endpoint when you run `serverless invoke -f helloWorld`. \ No newline at end of file +The events section in the yml above makes it so that the Function helloWorld will be used for request to the `example.com/hello/user` endpoint. This configuration would send a GET request with a header called `greeting` that has a value of `hi` to the `example.com/hello/user` endpoint when you run `serverless invoke -f helloWorld`. diff --git a/docs/providers/cloudflare/guide/README.md b/docs/providers/cloudflare/guide/README.md index bf10653c8..de54e8a50 100644 --- a/docs/providers/cloudflare/guide/README.md +++ b/docs/providers/cloudflare/guide/README.md @@ -5,7 +5,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/cloudflare/guide/) + # Serverless Cloudflare Workers Guide diff --git a/docs/providers/cloudflare/guide/debugging.md b/docs/providers/cloudflare/guide/debugging.md index 99d917538..5c4406434 100644 --- a/docs/providers/cloudflare/guide/debugging.md +++ b/docs/providers/cloudflare/guide/debugging.md @@ -7,29 +7,32 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/cloudflare/guide/debugging) + # Cloudflare Workers - Debugging + How can we debug errors in our Cloudflare Workers functions? - + Let's imagine that we have deployed the following code as a Cloudflare Worker function using Serverless: - + ```javascript addEventListener('fetch', event => { - event.respondWith(handleRequest(event.request)) - }) - async function handleRequest(request) { - const answer = request.headers.get("greeting") || "hello" - return new Response(answer + " world") - } - + event.respondWith(handleRequest(event.request)); +}); +async function handleRequest(request) { + const answer = request.headers.get('greeting') || 'hello'; + return new Response(answer + ' world'); +} ``` + And its corresponding Serverless yml file: - + ```yml # serverless.yml -... +--- functions: helloWorld: # What the script will be called on Cloudflare (this property value must match the function name one line above) @@ -46,7 +49,7 @@ functions: ``` Let's invoke correctly that function - + ```bash serverless invoke --function helloWorld @@ -54,9 +57,8 @@ serverless invoke --function helloWorld hi world ``` - If we were to call the above function without any headers, you would get `hello world` back instead of `hi world`, so we know that our worker is properly reading the greeting header. - + ## Cloudflare Workers Playground - + Cloudflare Workers also have a [Playground](https://cloudflareworkers.com/#) you can use to modify a Cloudflare Worker and see the results live on the same screen. The Cloudflare Workers Playground is another great way to debug your worker. diff --git a/docs/providers/cloudflare/guide/deploying.md b/docs/providers/cloudflare/guide/deploying.md index 20c229ee0..b2864272c 100644 --- a/docs/providers/cloudflare/guide/deploying.md +++ b/docs/providers/cloudflare/guide/deploying.md @@ -5,18 +5,21 @@ menuOrder: 7 description: How to deploy your Cloudflare Workers functions and their required infrastructure layout: Doc --> - + + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/cloudflare/guide/deploying) + # Cloudflare Workers - Deploying + The Serverless Framework was designed to provision your Cloudflare Workers Functions and Events. It does this via a couple of methods designed for different types of deployments. - + ## prerequisites - + In order to deploy your Cloudflare Worker, you need to set your Cloudflare email as an environmental variable called `CLOUDFLARE_AUTH_EMAIL`, and your Cloudflare Global API Key as an environmental variable called `CLOUDFLARE_AUTH_KEY`. You will also need to set `accountId` and `zoneId` in `serverless.yml` under `service.config`. The first part of the path when you open [Cloudflare dashboard](https://dash.cloudflare.com/) as a logged in user is your `accountId`, e.g. `dash.cloudflare.com/{accountId}`. And the `zoneId` can be found from the overview tab after selecting the desired zone from the [Cloudflare dashboard](https://dash.cloudflare.com/). - + Environmental variables are variables that live inside your terminal. For Mac and Linux users, you can set environmental variables like this: @@ -34,33 +37,35 @@ set CLOUDFLARE_AUTH_EMAIL=YOUR_CLOUDFLARE_EMAIL ``` You’ll need to redefine your environmental variables after each time you close your terminal. - - ## Deploy All + This is the main method for doing deployments with the Serverless Framework: - + ```bash serverless deploy ``` Use this method when you have updated your Function, Event or Resource configuration in `serverless.yml` and you want to deploy that change (or multiple changes at the same time) to your Cloudflare Worker. If you've made changes to any of your routes since last deploying, the Serverless Framework will update them on the server for you. - + +**Note:** You can specify a different configuration file name with the the `--config` option. + ### How It Works + The Serverless Framework reads in `serverless.yml` and uses it to provision your Functions. - -For each defined Function in your `serverless.yml` file, the Framework will create a Cloudflare Workers script. This means that only enterprise customers can declare more than one Function, but anyone can use [webpack](https://developers.cloudflare.com/workers/writing-workers/using-npm-modules/) to package their application into a single script. - + +For each defined Function in your `serverless.yml` file, the Framework will create a Cloudflare Workers script. This means that only enterprise customers can declare more than one Function, but anyone can use [webpack](https://developers.cloudflare.com/workers/writing-workers/using-npm-modules/) to package their application into a single script. + For example, let's take the following example `serverless.yml` file: - + ```yml # serverless.yml service: - name: hello-world - config: - accountId: CLOUDFLARE_ACCOUNT_ID - zoneId: CLOUDFLARE_ZONE_ID + name: hello-world + config: + accountId: CLOUDFLARE_ACCOUNT_ID + zoneId: CLOUDFLARE_ZONE_ID provider: name: cloudflare @@ -82,7 +87,6 @@ functions: headers: greeting: hi - # Only Enterprise accounts would be allowed to add this second function foo: name: foo @@ -94,10 +98,11 @@ functions: ``` After deploying that file, you’ll be able to hit the specified top-level routes of your zone, `example.com/hello/*` and `example.com/foo/*`, and any endpoints that resolve to these routes, like `example.com/hello/user` and `example.com/foo/bar`. - + ## Deploy Function + 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 re-deploying your whole service each time. - + ```bash serverless deploy --function myFunction ``` @@ -105,4 +110,5 @@ serverless deploy --function myFunction If you've made changes to the routes corresponding to this Function since last deploying, the Serverless Framework will update them on the server for you. ### Tips + Check out the [deploy command docs](../cli-reference/deploy.md) for all details and options. diff --git a/docs/providers/cloudflare/guide/events.md b/docs/providers/cloudflare/guide/events.md index e10ccfa64..a6bed643a 100644 --- a/docs/providers/cloudflare/guide/events.md +++ b/docs/providers/cloudflare/guide/events.md @@ -7,19 +7,21 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/cloudflare/guide/events) + - # Cloudflare Workers - Events + Simply put, events are the things that trigger your functions to run. - + If you are using Cloudflare Workers as your provider, all `events` in the service are HTTP Events, because that is the only event that Cloudflare Workers currently support. - + ```yml # serverless.yml … - + functions: helloWorld: # What the script will be called on Cloudflare (this property value must match the function name one line above) @@ -51,12 +53,12 @@ addEventListener('fetch', event => { }); import hello from './includeMe'; - + async function handleRequest(request) { return new Response(hello.hello()) } ``` If your handler script looks like the above, the includeMe script will be packed into the final script on deployment. - + [View the Cloudflare Workers events section for more information on HTTP events](../events). diff --git a/docs/providers/cloudflare/guide/functions.md b/docs/providers/cloudflare/guide/functions.md index 9f9a8f6ca..b9fc1eea3 100644 --- a/docs/providers/cloudflare/guide/functions.md +++ b/docs/providers/cloudflare/guide/functions.md @@ -5,30 +5,32 @@ menuOrder: 5 description: How to configure Cloudflare Workers functions in the Serverless Framework layout: Doc --> - + + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/cloudflare/guide/functions) + - + # Cloudflare Workers - Functions - -If you are using Cloudflare as a provider, all *functions* inside the service are Cloudflare Workers. - + +If you are using Cloudflare as a provider, all _functions_ inside the service are Cloudflare Workers. + ## Configuration - + All of the Cloudflare Workers in your serverless service can be found in `serverless.yml` under the `functions` property. - + ```yml # serverless.yml - + service: - name: hello-world + name: hello-world provider: name: cloudflare config: - accountId: CLOUDFLARE_ACCOUNT_ID - zoneId: CLOUDFLARE_ZONE_ID + accountId: CLOUDFLARE_ACCOUNT_ID + zoneId: CLOUDFLARE_ZONE_ID plugins: - serverless-cloudflare-workers @@ -47,34 +49,34 @@ functions: headers: someKey: someValue ``` - + The `script` property points to the file containing your Cloudflare Worker. - + ```javascript // helloWorld.js - + addEventListener('fetch', event => { - event.respondWith(handleRequest(event.request)) - }) - - async function handleRequest(request) { - return new Response("Hello world") - } + event.respondWith(handleRequest(event.request)); +}); + +async function handleRequest(request) { + return new Response('Hello world'); +} ``` - + If you have an Enterprise Cloudflare account, you can add multiple Cloudflare Workers to your project. - + ```yml # serverless.yml - + service: - name: hello-world + name: hello-world provider: name: cloudflare config: - accountId: CLOUDFLARE_ACCOUNT_ID - zoneId: CLOUDFLARE_ZONE_ID + accountId: CLOUDFLARE_ACCOUNT_ID + zoneId: CLOUDFLARE_ZONE_ID plugins: - serverless-cloudflare-workers @@ -93,7 +95,6 @@ functions: headers: someKey: someValue - # Only Enterprise accounts would be allowed to add this second function and its corresponding route above foo: name: foo @@ -103,5 +104,5 @@ functions: url: example.com/foo/* method: GET ``` + The `script` property is what the Cloudflare Worker will be called on Cloudflare’s data centers. - diff --git a/docs/providers/cloudflare/guide/installation.md b/docs/providers/cloudflare/guide/installation.md index 193f5867b..9baf7a82d 100644 --- a/docs/providers/cloudflare/guide/installation.md +++ b/docs/providers/cloudflare/guide/installation.md @@ -7,12 +7,15 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/cloudflare/guide/installation) + # Cloudflare Workers - Installation - + ## Installing Cloudflare Workers + Cloudflare Workers don’t actually require any installation to run. However, You will need to set your Global API key from Cloudflare as an environmental variable named `CLOUDFLARE_AUTH_KEY`, and your Cloudflare account email as an environmental variable named `CLOUDFLARE_AUTH_EMAIL`. You can get your Global API key from your [Cloudflare profile](https://dash.cloudflare.com/profile) page. Environmental variables are variables that live inside your terminal. @@ -34,22 +37,23 @@ set CLOUDFLARE_AUTH_EMAIL=YOUR_CLOUDFLARE_EMAIL You’ll need to redefine your environmental variables after each time you close your terminal. ## Installing the Serverless Framework + Next, install the Serverless Framework via [npm](https://npmjs.org) which was already installed when you installed Node.js. - + Open up a terminal and type `npm install -g serverless` to install Serverless. - + ```bash npm install -g serverless ``` Once the installation process is done you can verify that Serverless is installed successfully by running the following command in your terminal: - + ```bash serverless ``` To see which version of serverless you have installed run: - + ```bash serverless --version ``` @@ -57,4 +61,5 @@ serverless --version Remember, you need at least version 1.31.0 to use Cloudflare Workers with Serverless. ## Installing the serverless-cloudflare-workers plugin -Finally, add our `serverless-cloudflare-workers` plugin to your project by running `npm install --save serverless-cloudflare-workers`. \ No newline at end of file + +Finally, add our `serverless-cloudflare-workers` plugin to your project by running `npm install --save serverless-cloudflare-workers`. diff --git a/docs/providers/cloudflare/guide/intro.md b/docs/providers/cloudflare/guide/intro.md index 428cef53d..5b3a58246 100644 --- a/docs/providers/cloudflare/guide/intro.md +++ b/docs/providers/cloudflare/guide/intro.md @@ -7,30 +7,32 @@ layout: Doc --> -### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/cloudflare/guide/intro) - +### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/cloudflare/guide/intro) + + # Cloudflare Workers - Introduction -The Serverless Framework helps you develop and deploy serverless applications using [Cloudflare Workers](https://www.cloudflare.com/products/cloudflare-workers/). It's a CLI that offers structure, automation, and best practices out-of-the-box, allowing you to focus on building sophisticated, event-driven, serverless architectures, comprised of [Functions](#functions) and [Events](#events). [One config file](#serverlessyml) directs where exactly this Worker will live, so you can modify code and have it re-built and re-deployed in moments. No visits to the browser required. +The Serverless Framework helps you develop and deploy serverless applications using [Cloudflare Workers](https://www.cloudflare.com/products/cloudflare-workers/). It's a CLI that offers structure, automation, and best practices out-of-the-box, allowing you to focus on building sophisticated, event-driven, serverless architectures, comprised of [Functions](#functions) and [Events](#events). [One config file](#serverlessyml) directs where exactly this Worker will live, so you can modify code and have it re-built and re-deployed in moments. No visits to the browser required. The Serverless Framework is different than other application frameworks because: -* It manages your code as well as your infrastructure -* It supports multiple languages + +- It manages your code as well as your infrastructure +- It supports multiple languages # Serverless.yml -The `serverless.yml` file is what molds the Worker(s) of your project. Using the [Serverless Cloudflare Workers plugin](https://github.com/cloudflare/serverless-cloudflare-workers, a `serverless.yml` will look like: +The `serverless.yml` file is what molds the Worker(s) of your project. Using the [Serverless Cloudflare Workers plugin](https://github.com/cloudflare/serverless-cloudflare-workers), a `serverless.yml` will look like: ```yml # serverless.yml service: - name: hello - webpack: true | PATH_TO_CONFIG - config: - accountId: ${env:CLOUDFLARE_ACCOUNT_ID} - zoneId: ${env:CLOUDFLARE_ZONE_ID} + name: hello + webpack: true | PATH_TO_CONFIG + config: + accountId: ${env:CLOUDFLARE_ACCOUNT_ID} + zoneId: ${env:CLOUDFLARE_ZONE_ID} provider: name: cloudflare @@ -43,15 +45,15 @@ functions: .. #### Services -A **Service** is the Serverless Framework's unit of organization. You can think of it as a project file, though you can have multiple services for a single application. It's where you define your Functions and the routes they will live on, all in one file entitled `serverless.yml`: +A **Service** is the Serverless Framework's unit of organization. You can think of it as a project file, though you can have multiple services for a single application. It's where you define your Functions and the routes they will live on, all in one file entitled `serverless.yml`: ```yml # serverless.yml service: - name: hello - config: - accountId: ${env:CLOUDFLARE_ACCOUNT_ID} - zoneId: ${env:CLOUDFLARE_ZONE_ID} + name: hello + config: + accountId: ${env:CLOUDFLARE_ACCOUNT_ID} + zoneId: ${env:CLOUDFLARE_ZONE_ID} provider: name: cloudflare @@ -66,15 +68,15 @@ functions: .. `config`: -- `accountId`: the account that *owns* the zone that you wish to deploy Workers too. Note: this may not be the account ID you are signed in as, but will be the account ID you see in the URL once you've selected the zone +- `accountId`: the account that _owns_ the zone that you wish to deploy Workers too. Note: this may not be the account ID you are signed in as, but will be the account ID you see in the URL once you've selected the zone - `zoneId`: the zone desired to deploy Workers to To find your zoneId and accountId, please see [API documentation on resource IDs](https://api.cloudflare.com/#getting-started-resource-ids) -#### Provider +#### Provider -A Provider tells the serverless frame what cloud provider you are using, in this case Cloudflare. +A Provider tells the serverless frame what cloud provider you are using, in this case Cloudflare. ``` provider: @@ -94,31 +96,31 @@ provider: A Function is a Cloudflare Worker - a single script including its bindings, routes and other config. It's an independent unit of deployment, like a microservice. It's merely code, deployed on Cloudflare’s 155+ PoPs points of presence, that is most often written to perform a single job as a Worker script. - `serverless.yml`: +`serverless.yml`: ```yml - functions: - bar: - name: scriptName - script: filename - webpack: true - environment: - some_key: - resources: ... - events: ... +functions: + bar: + name: scriptName + script: filename + webpack: true + environment: + some_key: + resources: ... + events: ... ``` `name`: overwrite the default name generated (e.g. replaces [`hello-foo-bar`](#name)) for the Worker script name -`script`: the path to the script from the current directory omitting the extension `.js` +`script`: the path to the script from the current directory omitting the extension `.js` -`webpack`(*optional*): specifies what webpack operation to perform on this individual Worker script. See webpack +`webpack`(_optional_): specifies what webpack operation to perform on this individual Worker script. See webpack -`environment`(*optional*) : any environment variables set as a global inside the script. See more in [Environment](#environment) +`environment`(_optional_) : any environment variables set as a global inside the script. See more in [Environment](#environment) -`resources`(*optional*) : see Resources below +`resources`(_optional_) : see Resources below -`events`(*optional*) : Any routing for a Worker is configured here. See Events below +`events`(_optional_) : Any routing for a Worker is configured here. See Events below ##### Webpack @@ -140,7 +142,7 @@ To get this working in your worker project, simply add `webpack: true | ` will deploy your worker and run the HTTP request(s) specified by the `url` and `method` against this deployed worker. This is useful for defining specific hooks into your application for testing. To truly test your worker, you can run [`cURL`](https://curl.haxx.se/) against your domain since the Worker will be deployed. +`serverless invoke ` will deploy your worker and run the HTTP request(s) specified by the `url` and `method` against this deployed worker. This is useful for defining specific hooks into your application for testing. To truly test your worker, you can run [`cURL`](https://curl.haxx.se/) against your domain since the Worker will be deployed. ##### name @@ -196,4 +198,4 @@ plugins: You can add our `serverless-cloudflare-workers` plugin to your project by running `npm install --save serverless-cloudflare-workers`. -* *`workers.dev` domains are not currently supported using Serverless, but you can track our progress on [this Github issue](https://github.com/cloudflare/serverless-cloudflare-workers/issues/36).* \ No newline at end of file +- _`workers.dev` domains are not currently supported using Serverless, but you can track our progress on [this Github issue](https://github.com/cloudflare/serverless-cloudflare-workers/issues/36)._ diff --git a/docs/providers/cloudflare/guide/quick-start.md b/docs/providers/cloudflare/guide/quick-start.md index 93577b68e..458be2c94 100644 --- a/docs/providers/cloudflare/guide/quick-start.md +++ b/docs/providers/cloudflare/guide/quick-start.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/cloudflare/guide/quick-start) + # Cloudflare Workers - Quickstart @@ -16,15 +18,17 @@ layout: Doc This guide is a walk through of using the Serverless Plugin to deploy Cloudflare Workers to a zone already proxied on Cloudflare. -*Note:`workers.dev` domains are not currently supported using Serverless, but you can track our progress on [this Github issue](https://github.com/cloudflare/serverless-cloudflare-workers/issues/36).* +_Note:`workers.dev` domains are not currently supported using Serverless, but you can track our progress on [this Github issue](https://github.com/cloudflare/serverless-cloudflare-workers/issues/36)._ ## Pre-requisites -Node.js `v6.5.0` or later. + +Node.js `v10.X` or later. Serverless CLI `v1.31.0` or later. You can run `npm install -g serverless` to install it. you also need our `serverless-cloudflare-workers` plugin. You can install it in your project with `npm install --save serverless-cloudflare-workers`. ## Create a new service To create a new service, you can use the `cloudflare-workers` template. Optionally specify a unique name and an optional path for your service. + ```bash # Create a new Serverless Service/Project $ serverless create --template cloudflare-workers --path new-project @@ -34,29 +38,29 @@ $ cd new-project $ npm install ``` -# Setup +# Setup ### Config -To deploy, you will need either the environment variables set or manually input the `accountId` and `zoneId` in your `serverless.yml` according to the *zone* you wish the Worker(s) to deploy to. +To deploy, you will need either the environment variables set or manually input the `accountId` and `zoneId` in your `serverless.yml` according to the _zone_ you wish the Worker(s) to deploy to. ```yaml # serverless.yml service: - name: hello - config: - accountId: ${env:CLOUDFLARE_ACCOUNT_ID} - zoneId: ${env:CLOUDFLARE_ZONE_ID} - functions: - functionName: - worker: scriptName - script: filename - events: ... + name: hello + config: + accountId: ${env:CLOUDFLARE_ACCOUNT_ID} + zoneId: ${env:CLOUDFLARE_ZONE_ID} + functions: + functionName: + worker: scriptName + script: filename + events: ... ``` Configure the [functions](#function) according to your specific routing and naming conventions or leave `functions` as is from what the template generated. When you deploy with the Framework by running `serverless deploy`, everything in `serverless.yml` will be deployed at once. -### Environment Variables +### Environment Variables You will need to set your Global API key from Cloudflare as an environmental variable named `CLOUDFLARE_AUTH_KEY`, and your Cloudflare account email as an environmental variable named `CLOUDFLARE_AUTH_EMAIL`. See: [How to find your API keys](https://support.cloudflare.com/hc/en-us/articles/200167836) @@ -107,7 +111,7 @@ async function helloWorld(request) { } ``` -*Note: Serverless plugin omits the extension `.js` in the `serverless.yml` file when referring to what script to run* +_Note: Serverless plugin omits the extension `.js` in the `serverless.yml` file when referring to what script to run_ ## Deploy, test and diagnose your service @@ -139,23 +143,23 @@ serverless invoke --function helloWorld Hello world ``` -Your Function must have the `events` field populated in order for the `serverless` tool to know exactly which route to request. +Your Function must have the `events` field populated in order for the `serverless` tool to know exactly which route to request. ```yml # serverless.yml -... +--- foo: - name: foo - script: bar - events: - - http: - url: example.com/foo/bar - # Defines the method used by serverless when the `invoke` command is used. Cloudflare Workers only support GET requests for now - method: GET + name: foo + script: bar + events: + - http: + url: example.com/foo/bar + # Defines the method used by serverless when the `invoke` command is used. Cloudflare Workers only support GET requests for now + method: GET ``` - ## Cleanup + If at any point, you no longer need your service, you can run the following command to remove the Functions, Events and Resources that were created. ```bash diff --git a/docs/providers/cloudflare/guide/services.md b/docs/providers/cloudflare/guide/services.md index 315cf8305..1aa44617d 100644 --- a/docs/providers/cloudflare/guide/services.md +++ b/docs/providers/cloudflare/guide/services.md @@ -5,28 +5,30 @@ menuOrder: 4 description: How to manage and configure Serverless services, which contain your Cloudflare Workers and their events. layout: Doc --> - + + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/cloudflare/guide/services) + - + # Cloudflare Workers - Services - + A `service` is like a project. It's where you define your Cloudflare Workers and the `events` you test them with, all in a file called `serverless.yml`. - + To get started building your first Serverless Framework project, create a `service`. - + ## Organization - + In the beginning of an application created by a team with an Enterprise Cloudflare account, and for the lifespan of an application made by a team with a Non-Enterprise Cloudflare account, we recommend you use a single Service to define all of the Functions and Events for that project. - + ```bash myService/ serverless.yml # Contains all functions and infrastructure resources ``` - -However, as your application grows as an Enterprise Cloudflare user, you can break it out into multiple services. A lot of people organize their services by workflows or data models, and group the functions related to those workflows and data models together in the service. - + +However, as your application grows as an Enterprise Cloudflare user, you can break it out into multiple services. A lot of people organize their services by workflows or data models, and group the functions related to those workflows and data models together in the service. + ```bash users/ serverless.yml # Contains 4 functions that do Users CRUD operations and the Users database @@ -35,57 +37,57 @@ posts/ comments/ serverless.yml # Contains 4 functions that do Comments CRUD operations and the Comments database ``` - + This makes sense since related functions usually use common infrastructure resources, and you want to keep those functions and resources together as a single unit of deployment, for better organization and separation of concerns. - + ## Creation - + To create a service, use the `create` command. You can also pass in a path to create a directory and auto-name your service: - + ```bash # Create service with cloudflare-workers template in the folder ./my-service serverless create --template cloudflare-workers --path my-service ``` - + Here are the available runtimes for Cloudflare Workers: - -* cloudflare-workers -* cloudflare-workers-enterprise -* cloudflare-workers-rust - + +- cloudflare-workers +- cloudflare-workers-enterprise +- cloudflare-workers-rust + Check out the [create command docs](../cli-reference/create) for all the details and options. - + ## Contents - + You'll see the following files in your working directory: - + - `serverless.yml` - `helloWorld.js` - + ### serverless.yml - + Each `service` configuration is managed in the `serverless.yml` file. The main responsibilities of this file are: - + - Declare a Serverless service - Define one or more functions in the service - - Define the provider the service will be deployed to - - Define any custom plugins to be used - - Define events that trigger each function to execute (e.g. HTTP requests) - - Allow events listed in the `events` section to automatically create the resources required for the `serverless invoke` command - +- Define the provider the service will be deployed to +- Define any custom plugins to be used +- Define events that trigger each function to execute (e.g. HTTP requests) +- Allow events listed in the `events` section to automatically create the resources required for the `serverless invoke` command + You can see the name of the service, the provider configuration and the first function inside the `functions` definition. Any further service configuration will be done in this file. - + ```yml # serverless.yml - + service: - name: hello-world + name: hello-world provider: name: cloudflare config: - accountId: CLOUDFLARE_ACCOUNT_ID - zoneId: CLOUDFLARE_ZONE_ID + accountId: CLOUDFLARE_ACCOUNT_ID + zoneId: CLOUDFLARE_ZONE_ID plugins: - serverless-cloudflare-workers @@ -104,7 +106,6 @@ functions: headers: someKey: someValue - # Only Enterprise accounts would be allowed to add this second function and its corresponding route above foo: name: foo @@ -113,84 +114,83 @@ functions: - http: url: example.com/foo/bar method: GET - ``` - + ### helloWorld.js - + The `helloWorld.js` file contains a barebones Cloudflare Worker that returns ‘hello world’. - + ## Deployment - + When you deploy a Service, all of the Functions, and Events in your `serverless.yml` are translated into calls to Cloudflare to create your Cloudflare Worker(s). - + To deploy a service, first `cd` into the relevant service directory: - + ```bash cd my-service ``` - + Then use the `deploy` command: - + ```bash serverless deploy ``` - -Check out the [deployment guide](./deploying.md) to learn more about deployments and how they work. Or, check out the [deploy command docs](../cli-reference/deploy.md) for all the details and options. - + +Check out the [deployment guide](./deploying.md) to learn more about deployments and how they work. Or, check out the [deploy command docs](../cli-reference/deploy.md) for all the details and options. + ## Removal - + To easily remove your Service from Cloudflare’s data centers, you can use the `remove` command. - + Run `serverless remove` to trigger the removal process. - + Serverless will start the removal and informs you about it's process on the console. A success message is printed once the whole service is removed. - + The removal process will only remove the service on your provider's infrastructure. The service directory will still remain on your local machine so you can still modify and (re)deploy it to another stage, region or provider later on. - + ## Version Pinning - + The Serverless framework is usually installed globally via `npm install -g serverless`. This way you have the Serverless CLI available for all your services. - + Installing tools globally has the downside that the version can't be pinned inside package.json. This can lead to issues if you upgrade Serverless, but your colleagues or CI system don't. You can use a feature in your serverless.yml without worrying that your CI system will deploy with an old version of Serverless. - + ### Pinning a Version - + To configure version pinning define a `frameworkVersion` property in your serverless.yaml. Whenever you run a Serverless command from the CLI it checks if your current Serverless version is matching the `frameworkVersion` range. The CLI uses [Semantic Versioning](http://semver.org/) so you can pin it to an exact version or provide a range. In general we recommend to pin to an exact version to ensure everybody in your team has the exact same setup and no unexpected problems happen. - + ### Examples - + #### Exact Version - + ```yml # serverless.yml - -frameworkVersion: "=1.0.3" + +frameworkVersion: '=1.0.3' ``` - + #### Version Range - + ```yml # serverless.yml - -frameworkVersion: ">=1.0.0 <2.0.0" + +frameworkVersion: '>=1.0.0 <2.0.0' ``` - - + ## Installing Serverless in an existing service - + If you already have a Serverless service, and would prefer to lock down the framework version using `package.json`, then you can install Serverless as follows: - + ```bash # from within a service npm install serverless --save-dev ``` - + ### Invoking Serverless locally - + To execute the locally installed Serverless executable you have to reference the binary out of the node modules directory. - + Example: + ``` node ./node_modules/serverless/bin/serverless deploy ``` diff --git a/docs/providers/cloudflare/guide/workflow.md b/docs/providers/cloudflare/guide/workflow.md index c6dda1f8d..ade5cc0b7 100644 --- a/docs/providers/cloudflare/guide/workflow.md +++ b/docs/providers/cloudflare/guide/workflow.md @@ -7,52 +7,61 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/cloudflare/guide/workflow) + - # Cloudflare Workers - Workflow + Generally, Cloudflare Workers can be written locally, deployed with serverless, and tested with the [`serverless invoke`](../cli-reference/invoke.md) command. However, using the [Cloudflare Workers Playground](https://cloudflareworkers.com/#) can help you test and view your worker’s results live if you need more insight while developing your Cloudflare Worker. - + Below is a list of general tips for developing Cloudflare Workers with Serverless. - + ### Development Workflow + Write your functions Use `serverless deploy` only when you've made changes to `serverless.yml` and in CI/CD systems. Use `serverless deploy -f myFunction` to rapidly deploy changes when you are working on a specific Cloudflare Workers Function. Use `serverless invoke -f myFunction` to test your Cloudflare Workers Functions. + ### Larger Projects -* For Non-Enterprise Cloudflare customers, combining multiple workers into one file or using [webpack](https://developers.cloudflare.com/workers/writing-workers/using-npm-modules/). -* Keep the Functions and Resources in your Serverless Services to a minimum. + +- For Non-Enterprise Cloudflare customers, combining multiple workers into one file or using [webpack](https://developers.cloudflare.com/workers/writing-workers/using-npm-modules/). +- Keep the Functions and Resources in your Serverless Services to a minimum. + ## Cheat Sheet + A handy list of commands to use when developing with the Serverless Framework. - + ##### Create A Service: + Creates a new Service: - + ```bash serverless create -p [SERVICE NAME] -t cloudflare-workers ``` ##### Deploy All + Use this when you have made changes to your Functions, Events or Resources in `serverless.yml` or you simply want to deploy all changes within your Service at the same time. - + ```bash serverless deploy ``` - ##### Deploy Function + Use this to quickly overwrite your Cloudflare Workers Functions, allowing you to develop faster if you have an Enterprise account that supports deploying multiple functions. - + ```bash serverless deploy -f [FUNCTION NAME] ``` - ##### Invoke Function + Invokes a Cloudflare Workers Function. - + ```bash serverless invoke -f [FUNCTION NAME] ``` diff --git a/docs/providers/fn/README.md b/docs/providers/fn/README.md index c263b37f2..77ded9d43 100644 --- a/docs/providers/fn/README.md +++ b/docs/providers/fn/README.md @@ -5,7 +5,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/) + # Fn Provider Documentation diff --git a/docs/providers/fn/cli-reference/README.md b/docs/providers/fn/cli-reference/README.md index 100b43b03..536372d02 100644 --- a/docs/providers/fn/cli-reference/README.md +++ b/docs/providers/fn/cli-reference/README.md @@ -5,11 +5,13 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/fn/cli-reference/) + # Serverless Fn CLI Reference -Welcome to the Serverless Fn CLI Reference! Please select a section on the left to get started. +Welcome to the Serverless Fn CLI Reference! Please select a section on the left to get started. If you have questions, join the [chat in gitter](https://gitter.im/serverless/serverless) or [post over on the forums](http://forum.serverless.com/). diff --git a/docs/providers/fn/cli-reference/create.md b/docs/providers/fn/cli-reference/create.md index ebe93c90b..dc89079ba 100644 --- a/docs/providers/fn/cli-reference/create.md +++ b/docs/providers/fn/cli-reference/create.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/fn/cli-reference/create) + # Fn - Create @@ -35,6 +37,7 @@ serverless create --template fn-nodejs --path my-service ``` ## Options + - `--template` or `-t` The name of one of the available templates. **Required if --template-url and --template-path are not present**. - `--template-url` or `-u` The name of one of the available templates. **Required if --template and --template-path are not present**. - `--template-path` The local path of your template. **Required if --template and --template-url are not present**. @@ -42,6 +45,7 @@ serverless create --template fn-nodejs --path my-service - `--name` or `-n` the name of the service in `serverless.yml`. ## Provided lifecycle events + - `create:create` ## Available Templates for Fn diff --git a/docs/providers/fn/cli-reference/deploy.md b/docs/providers/fn/cli-reference/deploy.md index 1e1f632c6..1a558f9ec 100644 --- a/docs/providers/fn/cli-reference/deploy.md +++ b/docs/providers/fn/cli-reference/deploy.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/fn/cli-reference/deploy) + # Fn - Deploy @@ -23,9 +25,12 @@ serverless deploy This is the simplest deployment usage possible. With this command Serverless will deploy your service to the configured Fn server. ## Options + +- `--config` or `-c` Path to your conifguration file, if other than `serverless.yml|.yaml|.js|.json`. - `--verbose` or `-v` Shows all stack events during deployment, and display any Stack Output. - `--function` or `-f` Invoke `deploy function` (see above). Convenience shortcut ## Provided lifecycle events + - `deploy:deploy` - `deploy:function:deploy` diff --git a/docs/providers/fn/cli-reference/info.md b/docs/providers/fn/cli-reference/info.md index 8288ce57c..c2b86fc12 100644 --- a/docs/providers/fn/cli-reference/info.md +++ b/docs/providers/fn/cli-reference/info.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/fn/cli-reference/info) + # Fn - Info @@ -19,6 +21,7 @@ serverless info ``` ## Provided lifecycle events + - `info:info` ## Examples diff --git a/docs/providers/fn/cli-reference/invoke.md b/docs/providers/fn/cli-reference/invoke.md index a3bfdf030..89fdf3d61 100644 --- a/docs/providers/fn/cli-reference/invoke.md +++ b/docs/providers/fn/cli-reference/invoke.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/fn/cli-reference/invoke) + # Fn - Invoke @@ -19,12 +21,14 @@ serverless invoke --function functionName ``` ## Options + - `--function` or `-f` The name of the function in your service that you want to invoke. **Required**. - `--data` or `-d` String data to be passed as an event to your function. By default data is read from standard input. - `--path` or `-p` The path to a json file with input data to be passed to the invoked function. This path is relative to the root directory of the service. - `--log` or `-l` If set to `true`, it will output logging data of the invocation. Default is `false`. ## Provided lifecycle events + - `invoke:invoke` ## Examples diff --git a/docs/providers/fn/cli-reference/logs.md b/docs/providers/fn/cli-reference/logs.md index 19d89b80e..a90ed0fc2 100644 --- a/docs/providers/fn/cli-reference/logs.md +++ b/docs/providers/fn/cli-reference/logs.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/fn/cli-reference/logs) + # Fn - Logs @@ -27,4 +29,5 @@ serverless logs -f hello ```bash serverless logs -f hello ``` + This will fetch the logs for hello for the most recent calls to it. diff --git a/docs/providers/fn/cli-reference/plugin-install.md b/docs/providers/fn/cli-reference/plugin-install.md index 432922f8a..6992e270b 100644 --- a/docs/providers/fn/cli-reference/plugin-install.md +++ b/docs/providers/fn/cli-reference/plugin-install.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/fn/cli-reference/plugin-install) + # Plugin Install @@ -22,9 +24,11 @@ serverless plugin install --name pluginName ``` ## Options + - `--name` or `-n` The plugins name. **Required**. ## Provided lifecycle events + - `plugin:install:install` ## Examples diff --git a/docs/providers/fn/cli-reference/plugin-list.md b/docs/providers/fn/cli-reference/plugin-list.md index d8282edbf..aff2b3888 100644 --- a/docs/providers/fn/cli-reference/plugin-list.md +++ b/docs/providers/fn/cli-reference/plugin-list.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/fn/cli-reference/plugin-list) + # Plugin List @@ -19,7 +21,9 @@ serverless plugin list ``` ## Options -- *None* + +- _None_ ## Provided lifecycle events + - `plugin:list:list` diff --git a/docs/providers/fn/cli-reference/plugin-search.md b/docs/providers/fn/cli-reference/plugin-search.md index c73b944f9..68256016d 100644 --- a/docs/providers/fn/cli-reference/plugin-search.md +++ b/docs/providers/fn/cli-reference/plugin-search.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/fn/cli-reference/plugin-search) + # Plugin Search @@ -19,9 +21,11 @@ serverless plugin search --query query ``` ## Options + - `--query` or `-q` The query you want to use for your search. **Required**. ## Provided lifecycle events + - `plugin:search:search` ## Examples diff --git a/docs/providers/fn/cli-reference/plugin-uninstall.md b/docs/providers/fn/cli-reference/plugin-uninstall.md index f19402e9b..18279bbea 100644 --- a/docs/providers/fn/cli-reference/plugin-uninstall.md +++ b/docs/providers/fn/cli-reference/plugin-uninstall.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/fn/cli-reference/plugin-uninstall) + # Plugin Uninstall @@ -19,9 +21,11 @@ serverless plugin uninstall --name pluginName ``` ## Options + - `--name` or `-n` The plugins name. **Required**. ## Provided lifecycle events + - `plugin:uninstall:uninstall` ## Examples diff --git a/docs/providers/fn/cli-reference/remove.md b/docs/providers/fn/cli-reference/remove.md index 0738d220d..a45ea0e5b 100644 --- a/docs/providers/fn/cli-reference/remove.md +++ b/docs/providers/fn/cli-reference/remove.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/fn/cli-reference/remove) + # Fn - Remove @@ -21,4 +23,5 @@ serverless remove It will remove the Fn Function functions from your Fn server. ## Provided lifecycle events + - `remove:remove` diff --git a/docs/providers/fn/events/README.md b/docs/providers/fn/events/README.md index d87c6c698..03c396f83 100644 --- a/docs/providers/fn/events/README.md +++ b/docs/providers/fn/events/README.md @@ -1,11 +1,13 @@ + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/fn/events/) + # Serverless Fn Events @@ -15,4 +17,3 @@ Welcome to the Serverless Fn Events Glossary! Please select a section on the left to get started. If you have questions, join the [chat in gitter](https://gitter.im/serverless/serverless) or [post over on the forums](http://forum.serverless.com/) - diff --git a/docs/providers/fn/events/http.md b/docs/providers/fn/events/http.md index 6d37b4ad9..886e41b28 100644 --- a/docs/providers/fn/events/http.md +++ b/docs/providers/fn/events/http.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/fn/events/http) + # Fn HTTP Events @@ -29,8 +31,8 @@ functions: # Your "Functions" version: 0.0.1 runtime: go events: - - http: - path: /hello + - http: + path: /hello ``` The events section in the yaml above makes it so that the Function hi will be diff --git a/docs/providers/fn/guide/README.md b/docs/providers/fn/guide/README.md index 3a725c7c1..02e5be26b 100644 --- a/docs/providers/fn/guide/README.md +++ b/docs/providers/fn/guide/README.md @@ -1,11 +1,13 @@ + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/fn/guide/) + # Serverless Fn Guide diff --git a/docs/providers/fn/guide/debugging.md b/docs/providers/fn/guide/debugging.md index ba10ed4c6..09cd4c46a 100644 --- a/docs/providers/fn/guide/debugging.md +++ b/docs/providers/fn/guide/debugging.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/fn/guide/debugging) + # Fn - Debugging @@ -19,15 +21,15 @@ Let's imagine that we have deployed the following Nodejs code as a Fn function u ```javascript const fdk = require('@fnproject/fdk'); -fdk.handle((input) => { - input = JSON.parse(input) - let name = 'World'; - if (input.name) { - name = input.name; - } - const response = { message: `Hello ${name}` }; - console.error(`I show up in the logs name was: ${name}`); - return response; +fdk.handle(input => { + input = JSON.parse(input); + let name = 'World'; + if (input.name) { + name = input.name; + } + const response = { message: `Hello ${name}` }; + console.error(`I show up in the logs name was: ${name}`); + return response; }); ``` @@ -35,7 +37,7 @@ And its corresponding Serverless YAML file: ```yml service: - name: hello-world + name: hello-world provider: name: fn @@ -49,8 +51,8 @@ functions: runtime: node format: json events: - - http: - path: /hello + - http: + path: /hello ``` Let's invoke correctly that function diff --git a/docs/providers/fn/guide/deploying.md b/docs/providers/fn/guide/deploying.md index 40463c854..30b2967e6 100644 --- a/docs/providers/fn/guide/deploying.md +++ b/docs/providers/fn/guide/deploying.md @@ -7,12 +7,14 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/fn/guide/deploying) + # Fn - Deploying -The Serverless Framework was designed to provision your Fn Functions and Events. It does this via a couple of methods designed for different types of deployments. +The Serverless Framework was designed to provision your Fn Functions and Events. It does this via a couple of methods designed for different types of deployments. ## Deploy All @@ -24,6 +26,8 @@ serverless deploy Use this method when you have updated your Function, Event or Resource configuration in `serverless.yml` and you want to deploy that change (or multiple changes at the same time) to your Fn cluster. +**Note:** You can specify a different configuration file name with the the `--config` option. + ### How It Works The Serverless Framework translates all syntax in `serverless.yml` to [Fn](https://github.com/fnproject/fn) calls to provision your Functions. @@ -33,7 +37,6 @@ For each function in your `serverless.yml` file, Fn will create an Fn Function. For example, let's take the following example `serverless.yml` file: ```yaml - service: hello-world functions: # Your "Functions" @@ -42,9 +45,8 @@ functions: # Your "Functions" version: 0.0.1 runtime: go events: - - http: - path: /hello - + - http: + path: /hello ``` When deploying that file FN will provide you with one endpoint that you can hit at: `FN_API_URL/r/hello-world/hello` diff --git a/docs/providers/fn/guide/events.md b/docs/providers/fn/guide/events.md index f9910e491..6b05a8c2e 100644 --- a/docs/providers/fn/guide/events.md +++ b/docs/providers/fn/guide/events.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/fn/guide/events) + # Fn - Events diff --git a/docs/providers/fn/guide/installation.md b/docs/providers/fn/guide/installation.md index d3803b8d0..97b895562 100644 --- a/docs/providers/fn/guide/installation.md +++ b/docs/providers/fn/guide/installation.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/fn/guide/installation) + # Fn - Installation diff --git a/docs/providers/fn/guide/intro.md b/docs/providers/fn/guide/intro.md index a18a032c5..d01400dc0 100644 --- a/docs/providers/fn/guide/intro.md +++ b/docs/providers/fn/guide/intro.md @@ -7,16 +7,19 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/fn/guide/intro) + # Fn - Introduction -The Serverless Framework helps you develop and deploy serverless applications using Fn. It's a CLI that offers structure, automation and best practices out-of-the-box, allowing you to focus on building sophisticated, event-driven, serverless architectures, comprised of [Functions](#functions) and [Events](#events). +The Serverless Framework helps you develop and deploy serverless applications using Fn. It's a CLI that offers structure, automation and best practices out-of-the-box, allowing you to focus on building sophisticated, event-driven, serverless architectures, comprised of [Functions](#functions) and [Events](#events). The Serverless Framework is different than other application frameworks because: -* It manages your code as well as your infrastructure -* It supports multiple languages (Node.js, Python, Ruby, Go) + +- It manages your code as well as your infrastructure +- It supports multiple languages (Node.js, Python, Ruby, Go) ## Core Concepts @@ -24,25 +27,25 @@ Here are the Serverless Framework's main concepts and how they pertain to Fn. ### Functions -A Function is a [Fn Function](http://fnproject.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 [Fn Function](http://fnproject.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* -* *Performing a scheduled task* (To be added in newer versions) +- _Saving a user to the database_ +- _Processing a file in a database_ +- _Performing a scheduled task_ (To be added in newer versions) -You can perform multiple jobs in your code, but we don't recommend doing that without good reason. Separation of concerns is best and the Framework is designed to help you easily develop and deploy Functions, as well as manage lots of them. +You can perform multiple jobs in your code, but we don't recommend doing that without good reason. Separation of concerns is best and the Framework is designed to help you easily develop and deploy Functions, as well as manage lots of them. ### Events -Anything that triggers an Fn Event to execute is regarded by the Framework as an **Event**. Events are platform events on Fn such as: +Anything that triggers an Fn Event to execute is regarded by the Framework as an **Event**. Events are platform events on Fn such as: -* *An API Gateway HTTP endpoint (e.g., for a REST API)* -* *A Kafka queue message (e.g., a message)* -* *A scheduled timer (e.g., run every 5 minutes)* (To be added in newer versions) +- _An API Gateway HTTP endpoint (e.g., for a REST API)_ +- _A Kafka queue message (e.g., a message)_ +- _A scheduled timer (e.g., run every 5 minutes)_ (To be added in newer versions) ### Services -A **Service** is the Serverless Framework's unit of organization. You can think of it as a project file, though you can have multiple services for a single application. It's where you define your Functions and the Events that trigger them, all in one file entitled `serverless.yml` (or `serverless.json` or `serverless.js`). It looks like this: +A **Service** is the Serverless Framework's unit of organization. You can think of it as a project file, though you can have multiple services for a single application. It's where you define your Functions and the Events that trigger them, all in one file by default entitled `serverless.yml` (or `serverless.json` or `serverless.js`). It looks like this: ```yml # serverless.yml @@ -55,8 +58,8 @@ functions: # Your "Functions" version: 0.0.1 runtime: go events: - - http: - path: /hello + - http: + path: /hello ``` -When you deploy with the Framework by running `serverless deploy`, everything in `serverless.yml` is deployed at once. +When you deploy with the Framework by running `serverless deploy`, everything in `serverless.yml` (or the file specified with the `--config` option) is deployed at once. diff --git a/docs/providers/fn/guide/quick-start.md b/docs/providers/fn/guide/quick-start.md index f5949e9d3..c7256f67c 100644 --- a/docs/providers/fn/guide/quick-start.md +++ b/docs/providers/fn/guide/quick-start.md @@ -12,7 +12,7 @@ layout: Doc 1. Node.js `v6.5.0` or later. 2. Serverless CLI `v1.20` or later. You can run -`npm install -g serverless` to install it. + `npm install -g serverless` to install it. 3. Install Fn & Dependencies(./installation.md). ## Create a new service @@ -32,37 +32,38 @@ $ npm install 1. **Deploy the Service** - Use this when you have made changes to your Functions, Events or Resources in `serverless.yml` or you simply want to deploy all changes within your Service at the same time. +Use this when you have made changes to your Functions, Events or Resources in `serverless.yml` or you simply want to deploy all changes within your Service at the same time. - ```bash - serverless deploy -v - ``` +```bash +serverless deploy -v +``` 2. **Deploy the Function** - Use this to quickly upload and overwrite your function code, allowing you to develop faster. +Use this to quickly upload and overwrite your function code, allowing you to develop faster. - ```bash - serverless deploy -f hello - ``` +```bash +serverless deploy -f hello +``` 3. **Invoke the Function** - Invokes the Function and returns results. +Invokes the Function and returns results. - ```bash - $ serverless invoke --function hello --data '{"name":"Bob"}' -l - Serverless: Calling Function: hello - { message: 'Hello Bob' } - I show up in the logs name was: Bob - ``` +```bash +$ serverless invoke --function hello --data '{"name":"Bob"}' -l +Serverless: Calling Function: hello +{ message: 'Hello Bob' } +I show up in the logs name was: Bob +``` 4. **Fetch the Function Logs** - Open up a separate tab in your console and view logs for a specific Function using this command. - ```bash - serverless logs -f hello - ``` +Open up a separate tab in your console and view logs for a specific Function using this command. + +```bash +serverless logs -f hello +``` ## Cleanup diff --git a/docs/providers/fn/guide/workflow.md b/docs/providers/fn/guide/workflow.md index 37feee16b..e229b67ca 100644 --- a/docs/providers/fn/guide/workflow.md +++ b/docs/providers/fn/guide/workflow.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/fn/guide/workflow) + # Fn - Workflow @@ -20,46 +22,58 @@ Intro. Quick recommendations and tips for various processes. 2. Use `serverless deploy` only when you've made changes to `serverless.yml` and in CI/CD systems. 3. Use `serverless deploy -f myFunction` to rapidly deploy changes when you are working on a specific Fn Function. 4. Use `serverless invoke -f myFunction -l` to test your Fn Functions. -6. Write tests to run locally. +5. Write tests to run locally. ### Larger Projects -* Break your application/project into multiple Serverless Services. -* Model your Serverless Services around Data Models or Workflows. -* Keep the Functions and Resources in your Serverless Services to a minimum. + +- Break your application/project into multiple Serverless Services. +- Model your Serverless Services around Data Models or Workflows. +- Keep the Functions and Resources in your Serverless Services to a minimum. ## Cheat Sheet + A handy list of commands to use when developing with the Serverless Framework. ##### Create A Service: + Creates a new Service ``` serverless create -p [SERVICE NAME] -t fn-nodejs ``` + ``` serverless create -p [SERVICE NAME] -t fn-go ``` ##### Deploy All + Use this when you have made changes to your Functions, Events or Resources in `serverless.yml` or you simply want to deploy all changes within your Service at the same time. + ``` serverless deploy ``` ##### Deploy Function + Use this to quickly overwrite your Fn Functions, allowing you to develop faster. + ``` serverless deploy -f [FUNCTION NAME] ``` ##### Invoke Function + Invokes an Fn Function and returns logs. + ``` serverless invoke -f [FUNCTION NAME] -l ``` ##### Streaming Logs + Open up a separate tab in your console and stream all logs for a specific Function using this command. + ``` serverless logs -f [FUNCTION NAME] ``` diff --git a/docs/providers/google/README.md b/docs/providers/google/README.md index 521bc3cf9..b7e64fa85 100644 --- a/docs/providers/google/README.md +++ b/docs/providers/google/README.md @@ -5,7 +5,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/) + # Serverless Google Cloud Functions Provider Documentation diff --git a/docs/providers/google/cli-reference/README.md b/docs/providers/google/cli-reference/README.md index 3b930b8ff..9597b4621 100644 --- a/docs/providers/google/cli-reference/README.md +++ b/docs/providers/google/cli-reference/README.md @@ -5,12 +5,14 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/google/cli-reference/) + # Serverless Google Cloud Functions CLI Reference -Welcome to the Serverless Google Cloud Functions CLI Reference! Please select a section on the left to get started. +Welcome to the Serverless Google Cloud Functions CLI Reference! Please select a section on the left to get started. If you have questions, join the [chat in gitter](https://gitter.im/serverless/serverless) or [post over on the forums](http://forum.serverless.com/). diff --git a/docs/providers/google/cli-reference/create.md b/docs/providers/google/cli-reference/create.md index ded7d0eac..f7a7f12f2 100644 --- a/docs/providers/google/cli-reference/create.md +++ b/docs/providers/google/cli-reference/create.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/google/cli-reference/create) + # Google - Create diff --git a/docs/providers/google/cli-reference/deploy.md b/docs/providers/google/cli-reference/deploy.md index 0eea151a7..73757016d 100644 --- a/docs/providers/google/cli-reference/deploy.md +++ b/docs/providers/google/cli-reference/deploy.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/google/cli-reference/deploy) + # Deploy @@ -18,6 +20,10 @@ The `serverless deploy` command deploys your entire service via the Google Cloud serverless deploy ``` +## Options + +- `--config` or `-c` Path to your conifguration file, if other than `serverless.yml|.yaml|.js|.json`. + ## Artifacts After the `serverless deploy` command runs all created deployment artifacts are placed in the `.serverless` folder of the service. diff --git a/docs/providers/google/cli-reference/info.md b/docs/providers/google/cli-reference/info.md index 37d24eaf0..75eebec85 100644 --- a/docs/providers/google/cli-reference/info.md +++ b/docs/providers/google/cli-reference/info.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/google/cli-reference/info) + # Google - Info diff --git a/docs/providers/google/cli-reference/install.md b/docs/providers/google/cli-reference/install.md index baa2ee4be..0406ffa75 100644 --- a/docs/providers/google/cli-reference/install.md +++ b/docs/providers/google/cli-reference/install.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/google/cli-reference/install) + # Google - Install @@ -23,6 +25,13 @@ serverless install --url https://github.com/some/service - `--url` or `-u` The services GitHub URL. **Required**. - `--name` or `-n` Name for the service. +## Supported Code Hosting Platforms + +- GitHub +- GitHub Enterprise +- GitLab +- BitBucket + ## Examples ### Installing a service from a GitHub URL diff --git a/docs/providers/google/cli-reference/invoke-local.md b/docs/providers/google/cli-reference/invoke-local.md index 3d82f15ea..4dcc28c3d 100644 --- a/docs/providers/google/cli-reference/invoke-local.md +++ b/docs/providers/google/cli-reference/invoke-local.md @@ -22,13 +22,13 @@ serverless invoke local -f functionName ## Options -* `--function` or `-f` The name of the function in your service that you want to invoke. **Required**. -* `--data` or `-d` Data you want to pass into the function -* `--path` or `-p` Path to JSON or YAML file holding input data. This path is relative to the root directory of the service. -* `--raw` Pass data as a raw string even if it is JSON. If not set, JSON data are parsed and passed as an object. -* `--contextPath` or `-x`, The path to a json file holding input context to be passed to the invoked function. This path is relative to the root directory of the service. -* `--context` or `-c`, String data to be passed as a context to your function. Same like with `--data`, context included in `--contextPath` will overwrite the context you passed with `--context` flag. -* `--env` or `-e` String representing an environment variable to set when invoking your function, in the form `=`. Can be repeated for more than one environment variable. +- `--function` or `-f` The name of the function in your service that you want to invoke. **Required**. +- `--data` or `-d` Data you want to pass into the function +- `--path` or `-p` Path to JSON or YAML file holding input data. This path is relative to the root directory of the service. +- `--raw` Pass data as a raw string even if it is JSON. If not set, JSON data are parsed and passed as an object. +- `--contextPath` or `-x`, The path to a json file holding input context to be passed to the invoked function. This path is relative to the root directory of the service. +- `--context` or `-c`, String data to be passed as a context to your function. Same like with `--data`, context included in `--contextPath` will overwrite the context you passed with `--context` flag. +- `--env` or `-e` String representing an environment variable to set when invoking your function, in the form `=`. Can be repeated for more than one environment variable. > Keep in mind that if you pass both `--path` and `--data`, the data included in the `--path` file will overwrite the data you passed with the `--data` flag. diff --git a/docs/providers/google/cli-reference/invoke.md b/docs/providers/google/cli-reference/invoke.md index f49dacdb6..ec17e40fc 100644 --- a/docs/providers/google/cli-reference/invoke.md +++ b/docs/providers/google/cli-reference/invoke.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/google/cli-reference/invoke) + # Google - Invoke diff --git a/docs/providers/google/cli-reference/login.md b/docs/providers/google/cli-reference/login.md index 6097a15eb..a0977cebc 100644 --- a/docs/providers/google/cli-reference/login.md +++ b/docs/providers/google/cli-reference/login.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/google/cli-reference/login) + # Login diff --git a/docs/providers/google/cli-reference/logs.md b/docs/providers/google/cli-reference/logs.md index fd1a6444a..7afb0e7a6 100644 --- a/docs/providers/google/cli-reference/logs.md +++ b/docs/providers/google/cli-reference/logs.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/google/cli-reference/logs) + # Google - Logs diff --git a/docs/providers/google/cli-reference/package.md b/docs/providers/google/cli-reference/package.md index 023575d6a..76d4ddf0d 100644 --- a/docs/providers/google/cli-reference/package.md +++ b/docs/providers/google/cli-reference/package.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/google/cli-reference/package) + # Google - package diff --git a/docs/providers/google/cli-reference/plugin-install.md b/docs/providers/google/cli-reference/plugin-install.md index 03b41ef35..5270a9346 100644 --- a/docs/providers/google/cli-reference/plugin-install.md +++ b/docs/providers/google/cli-reference/plugin-install.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/google/cli-reference/plugin-install) + # Plugin Install @@ -22,9 +24,11 @@ serverless plugin install --name pluginName ``` ## Options + - `--name` or `-n` The plugins name. **Required**. ## Provided lifecycle events + - `plugin:install:install` ## Examples diff --git a/docs/providers/google/cli-reference/plugin-list.md b/docs/providers/google/cli-reference/plugin-list.md index d9b5a17b7..67cdf426c 100644 --- a/docs/providers/google/cli-reference/plugin-list.md +++ b/docs/providers/google/cli-reference/plugin-list.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/google/cli-reference/plugin-list) + # Plugin List @@ -19,7 +21,9 @@ serverless plugin list ``` ## Options -- *None* + +- _None_ ## Provided lifecycle events + - `plugin:list:list` diff --git a/docs/providers/google/cli-reference/plugin-search.md b/docs/providers/google/cli-reference/plugin-search.md index c0f1130bd..c1c13282b 100644 --- a/docs/providers/google/cli-reference/plugin-search.md +++ b/docs/providers/google/cli-reference/plugin-search.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/google/cli-reference/plugin-search) + # Plugin Search @@ -19,9 +21,11 @@ serverless plugin search --query query ``` ## Options + - `--query` or `-q` The query you want to use for your search. **Required**. ## Provided lifecycle events + - `plugin:search:search` ## Examples diff --git a/docs/providers/google/cli-reference/plugin-uninstall.md b/docs/providers/google/cli-reference/plugin-uninstall.md index 8e0e2f8ab..81d5e86bc 100644 --- a/docs/providers/google/cli-reference/plugin-uninstall.md +++ b/docs/providers/google/cli-reference/plugin-uninstall.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/google/cli-reference/plugin-uninstall) + # Plugin Uninstall @@ -19,9 +21,11 @@ serverless plugin uninstall --name pluginName ``` ## Options + - `--name` or `-n` The plugins name. **Required**. ## Provided lifecycle events + - `plugin:uninstall:uninstall` ## Examples diff --git a/docs/providers/google/cli-reference/print.md b/docs/providers/google/cli-reference/print.md index bfeb5427d..d47f768c1 100644 --- a/docs/providers/google/cli-reference/print.md +++ b/docs/providers/google/cli-reference/print.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/google/cli-reference/print) + # Print diff --git a/docs/providers/google/cli-reference/remove.md b/docs/providers/google/cli-reference/remove.md index e091effb3..592483a2d 100644 --- a/docs/providers/google/cli-reference/remove.md +++ b/docs/providers/google/cli-reference/remove.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/google/cli-reference/remove) + # Google - Remove diff --git a/docs/providers/google/events/README.md b/docs/providers/google/events/README.md index e171761a2..34e2039b5 100644 --- a/docs/providers/google/events/README.md +++ b/docs/providers/google/events/README.md @@ -5,7 +5,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/google/events/) + # Serverless Google Cloud Functions Events diff --git a/docs/providers/google/events/event.md b/docs/providers/google/events/event.md index 3978158ec..1e29b7faf 100644 --- a/docs/providers/google/events/event.md +++ b/docs/providers/google/events/event.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/google/events/event) + ## Event diff --git a/docs/providers/google/events/http.md b/docs/providers/google/events/http.md index bd486e147..28e3eaa1d 100644 --- a/docs/providers/google/events/http.md +++ b/docs/providers/google/events/http.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/google/events/http) + # HTTP @@ -39,7 +41,6 @@ functions: ```javascript // index.js - exports.first = (request, response) => { response.status(200).send('Hello World!'); }; @@ -49,7 +50,7 @@ exports.first = (request, response) => { The configuration for Google Cloud Functions is a bit different than some other providers: -* Your deployed endpoint from above will accept GET, POST, and all other HTTP verbs. If you want to disallow certain verbs, [you can do that within the method body](https://cloud.google.com/functions/docs/writing/http#handling_http_methods). -* All Google Cloud Functions are deployed as just the handler name at the root of the URL pathname. In the example above, this means your function is deployed to `https://YOUR_URL/http`. As a result, you cannot configure nested routes such as `http/hello` in your `serverless.yml` file. Instead, Google passes all URLs that appear to be subdirectories of your URL to your handler function so that you can determine the appropriate behavior. The complete path is still available as `req.path` and can be parsed to provide nested routes, path/URL parameters, and more. +- Your deployed endpoint from above will accept GET, POST, and all other HTTP verbs. If you want to disallow certain verbs, [you can do that within the method body](https://cloud.google.com/functions/docs/writing/http#handling_http_methods). +- All Google Cloud Functions are deployed as just the handler name at the root of the URL pathname. In the example above, this means your function is deployed to `https://YOUR_URL/http`. As a result, you cannot configure nested routes such as `http/hello` in your `serverless.yml` file. Instead, Google passes all URLs that appear to be subdirectories of your URL to your handler function so that you can determine the appropriate behavior. The complete path is still available as `req.path` and can be parsed to provide nested routes, path/URL parameters, and more. **Note:** See the documentation about the [function handlers](../guide/functions.md) to learn how your handler signature should look like to work with this type of event. diff --git a/docs/providers/google/examples/README.md b/docs/providers/google/examples/README.md index 8b9c291e9..8c4a43126 100644 --- a/docs/providers/google/examples/README.md +++ b/docs/providers/google/examples/README.md @@ -5,16 +5,18 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/google/examples/) + # Serverless Google Cloud Functions Examples Have an example? Submit a PR or [open an issue](https://github.com/serverless/examples/issues). ⚡️ -| Example | Runtime | -|:--------------------------- |:-----| -| [Google Node Simple](https://serverless.com/examples/google-node-simple-http-endpoint/)
    Boilerplate project repository for Google Cloud provider with Serverless Framework. | nodeJS | -| [Google Python Simple](https://serverless.com/examples/google-python-simple-http-endpoint/)
    Boilerplate project repository for Google Cloud provider with Serverless Framework. | Python | +| Example | Runtime | +| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :------ | +| [Google Node Simple](https://serverless.com/examples/google-node-simple-http-endpoint/)
    Boilerplate project repository for Google Cloud provider with Serverless Framework. | nodeJS | +| [Google Python Simple](https://serverless.com/examples/google-python-simple-http-endpoint/)
    Boilerplate project repository for Google Cloud provider with Serverless Framework. | Python | If you have questions, join the [chat in gitter](https://gitter.im/serverless/serverless) or [post over on the forums](https://forum.serverless.com/) diff --git a/docs/providers/google/examples/hello-world/README.md b/docs/providers/google/examples/hello-world/README.md index b19c3e07c..328371843 100644 --- a/docs/providers/google/examples/hello-world/README.md +++ b/docs/providers/google/examples/hello-world/README.md @@ -6,7 +6,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/google/examples/hello-world/) + # Hello World Serverless Example 🌍 @@ -15,7 +17,7 @@ Welcome to the Hello World example. Pick your language of choice: -* [JavaScript](./node) -* [Python](./python) +- [JavaScript](./node) +- [Python](./python) [View all examples](https://www.serverless.com/framework/docs/providers/google/examples/) diff --git a/docs/providers/google/examples/hello-world/node/README.md b/docs/providers/google/examples/hello-world/node/README.md index 5802b9779..00e3d064b 100644 --- a/docs/providers/google/examples/hello-world/node/README.md +++ b/docs/providers/google/examples/hello-world/node/README.md @@ -6,7 +6,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/google/examples/hello-world/node/) + # Hello World Node.js Example diff --git a/docs/providers/google/examples/hello-world/python/README.md b/docs/providers/google/examples/hello-world/python/README.md index 37a7a0935..403c166a5 100644 --- a/docs/providers/google/examples/hello-world/python/README.md +++ b/docs/providers/google/examples/hello-world/python/README.md @@ -6,7 +6,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/google/examples/hello-world/python/) + # Hello World Python Example diff --git a/docs/providers/google/guide/README.md b/docs/providers/google/guide/README.md index 12c45109c..4fb44333d 100644 --- a/docs/providers/google/guide/README.md +++ b/docs/providers/google/guide/README.md @@ -5,7 +5,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/google/guide/) + # Serverless Google Cloud Functions Guide diff --git a/docs/providers/google/guide/credentials.md b/docs/providers/google/guide/credentials.md index b34e1a92e..4d51dd4bc 100644 --- a/docs/providers/google/guide/credentials.md +++ b/docs/providers/google/guide/credentials.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/google/guide/credentials) + # Google - Credentials @@ -31,9 +33,9 @@ A Google Cloud Project is required to use Google Cloud Functions. Here's how to 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. -4. Wait until the Project was successfully created and Google will redirect you to your new Project. -5. Verify your currently within your new Project by looking at the dropdown next to the search bar. This should mark your new Project as selected. +4. Click on "Create" to start the creation process. +5. Wait until the Project was successfully created and Google will redirect you to your new Project. +6. Verify your currently within your new Project by looking at the dropdown next to the search bar. This should mark your new Project as selected. ## Enable the necessary APIs diff --git a/docs/providers/google/guide/deploying.md b/docs/providers/google/guide/deploying.md index c7faeda32..3b3d05f32 100644 --- a/docs/providers/google/guide/deploying.md +++ b/docs/providers/google/guide/deploying.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/google/guide/deploying) + # Google - Deploying @@ -24,6 +26,8 @@ serverless deploy Use this method when you have updated your Function, Events or Resource configuration in `serverless.yml` and you want to deploy that change (or multiple changes at the same time) to the Google Cloud. +**Note:** You can specify a different configuration file name with the the `--config` option. + ### How It Works The Serverless Framework translates all syntax in `serverless.yml` to a Google Deployment Manager configuration template. diff --git a/docs/providers/google/guide/events.md b/docs/providers/google/guide/events.md index b736f7ebb..8b55e2e5c 100644 --- a/docs/providers/google/guide/events.md +++ b/docs/providers/google/guide/events.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/google/guide/events) + # Google - Events diff --git a/docs/providers/google/guide/functions.md b/docs/providers/google/guide/functions.md index 47ac6f789..3c437afc6 100644 --- a/docs/providers/google/guide/functions.md +++ b/docs/providers/google/guide/functions.md @@ -7,12 +7,14 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/google/guide/functions) + # Google - Functions -If you are using Google Cloud Functions as a provider, all *functions* inside the service are Google Cloud Functions. +If you are using Google Cloud Functions as a provider, all _functions_ inside the service are Google Cloud Functions. ## Configuration @@ -39,8 +41,7 @@ You can specify an array of functions, which is useful if you separate your func ```yml # serverless.yml -... - +--- functions: - ${file(./foo-functions.yml)} - ${file(./bar-functions.yml)} @@ -54,7 +55,6 @@ deleteFoo: handler: handler.foo ``` - ## Handler The `handler` property should be the function name you've exported in your entrypoint file. @@ -63,7 +63,7 @@ When you e.g. export a function with the name `http` in `index.js` your `handler ```javascript // index.js -exports.http = (request, response) => {} +exports.http = (request, response) => {}; ``` **A note about index.js and the entrypoint file** diff --git a/docs/providers/google/guide/installation.md b/docs/providers/google/guide/installation.md index 914c7d1c4..a63e22385 100644 --- a/docs/providers/google/guide/installation.md +++ b/docs/providers/google/guide/installation.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/google/guide/installation) + # Google - Installation diff --git a/docs/providers/google/guide/intro.md b/docs/providers/google/guide/intro.md index 6f03ea684..cfd0be40a 100644 --- a/docs/providers/google/guide/intro.md +++ b/docs/providers/google/guide/intro.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/google/guide/intro) + # Google - Introduction @@ -27,9 +29,9 @@ Here are the Framework's main concepts and how they pertain to Google Cloud Func A Function is a [Google Cloud Function](https://cloud.google.com/functions/). 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* -- *Performing a scheduled task* +- _Saving a user to the database_ +- _Processing a file in a database_ +- _Performing a scheduled task_ You can perform multiple jobs in your code, but we don't recommend doing that without good reason. Separation of concerns is best and the Framework is designed to help you easily develop and deploy Functions, as well as manage lots of them. @@ -37,16 +39,16 @@ You can perform multiple jobs in your code, but we don't recommend doing that wi Anything that triggers a Google Cloud Function to execute is regarded by the Framework as an **Event**. Events are platform events on Google Cloud Functions such as: -- *An HTTP Trigger (e.g., for a REST API)* -- *A pubSub event (e.g., run function when message is sent to topic)* -- *A Storage event (e.g., Image uploaded into bucket)* -- *And more...* +- _An HTTP Trigger (e.g., for a REST API)_ +- _A pubSub event (e.g., run function when message is sent to topic)_ +- _A Storage event (e.g., Image uploaded into bucket)_ +- _And more..._ When you define an event for your Google Cloud Function in the Serverless Framework, the Framework will automatically translate the event with its function into a corresponding [deployment resource](https://cloud.google.com/deployment-manager/docs/configuration/supported-resource-types). This way the event is configured so that your functions can listen to it. ### Services -A **Service** is the Framework's unit of organization. You can think of it as a project file, though you can have multiple services for a single application. It's where you define your Functions, the Events that trigger them, and the Resources your Functions use, all in one file entitled `serverless.yml` (or `serverless.json` or `serverless.js`). It looks like this: +A **Service** is the Framework's unit of organization. You can think of it as a project file, though you can have multiple services for a single application. It's where you define your Functions, the Events that trigger them, and the Resources your Functions use, all in one file by default entitled `serverless.yml` (or `serverless.json` or `serverless.js`). It looks like this: ```yml # serverless.yml @@ -59,7 +61,7 @@ functions: # Your "Functions" - http: create ``` -When you deploy with the Framework by running `serverless deploy`, everything in `serverless.yml` is deployed at once. +When you deploy with the Framework by running `serverless deploy`, everything in `serverless.yml` (or the file specified with the `--config` option) is deployed at once. ### Plugins diff --git a/docs/providers/google/guide/packaging.md b/docs/providers/google/guide/packaging.md index 5d0e8d424..686185d56 100644 --- a/docs/providers/google/guide/packaging.md +++ b/docs/providers/google/guide/packaging.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/google/guide/packaging) + # Google - Packaging @@ -41,7 +43,7 @@ previously excluded files and directories. Exclude all node_modules but then re-include a specific modules (in this case node-fetch) using `exclude` exclusively -``` yml +```yml package: exclude: - node_modules/** @@ -50,7 +52,7 @@ package: Exclude all files but `handler.js` using `exclude` and `include` -``` yml +```yml package: exclude: - src/** @@ -111,6 +113,7 @@ functions: exclude: - some-file.js ``` + You can also select which functions to be packaged separately, and have the rest use the service package by setting the `individually` flag at the function level: ```yml diff --git a/docs/providers/google/guide/plugins.md b/docs/providers/google/guide/plugins.md index 78c8a6e24..e7095ee0e 100644 --- a/docs/providers/google/guide/plugins.md +++ b/docs/providers/google/guide/plugins.md @@ -7,19 +7,21 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/google/guide/plugins) + # Google - Plugins -A Plugin is custom Javascript code that creates new or extends existing commands within the Serverless Framework. The Serverless Framework is merely a group of Plugins that are provided in the core. If you or your organization have a specific workflow, install a pre-written Plugin or write a plugin to customize the Framework to your needs. External Plugins are written exactly the same way as the core Plugins. +A Plugin is custom Javascript code that creates new or extends existing commands within the Serverless Framework. The Serverless Framework is merely a group of Plugins that are provided in the core. If you or your organization have a specific workflow, install a pre-written Plugin or write a plugin to customize the Framework to your needs. External Plugins are written exactly the same way as the core Plugins. - [How to create serverless plugins - Part 1](https://serverless.com/blog/writing-serverless-plugins/) - [How to create serverless plugins - Part 2](https://serverless.com/blog/writing-serverless-plugins-2/) ## Installing Plugins -External Plugins are added on a per service basis and are not applied globally. Make sure you are in your Service's root directory, then install the corresponding Plugin with the help of npm: +External Plugins are added on a per service basis and are not applied globally. Make sure you are in your Service's root directory, then install the corresponding Plugin with the help of npm: ``` npm install --save custom-serverless-plugin @@ -33,9 +35,11 @@ We need to tell Serverless that we want to use the plugin inside our service. We plugins: - custom-serverless-plugin ``` + The `plugins` section supports two formats: Array object: + ```yml plugins: - plugin1 @@ -43,6 +47,7 @@ plugins: ``` Enhanced plugins object: + ```yml plugins: localPath: './custom_serverless_plugins' @@ -66,18 +71,21 @@ custom: If you are working on a plugin or have a plugin that is just designed for one project they can be loaded from the local folder. Local plugins can be added in the `plugins` array in `serverless.yml`. By default local plugins can be added to the `.serverless_plugins` directory at the root of your service, and in the `plugins` array in `serverless.yml`. + ```yml plugins: - custom-serverless-plugin ``` Local plugins folder can be changed by enhancing `plugins` object: + ```yml plugins: localPath: './custom_serverless_plugins' modules: - custom-serverless-plugin ``` + The `custom-serverless-plugin` will be loaded from the `custom_serverless_plugins` directory at the root of your service. If the `localPath` is not provided or empty `.serverless_plugins` directory will be taken as the `localPath`. The plugin will be loaded based on being named `custom-serverless-plugin.js` or `custom-serverless-plugin\index.js` in the root of `localPath` folder (`.serverless_plugins` by default). @@ -102,15 +110,15 @@ In this case `plugin1` is loaded before `plugin2`. #### Plugin -Code which defines *Commands*, any *Events* within a *Command*, and any *Hooks* assigned to an *Lifecycle Event*. +Code which defines _Commands_, any _Events_ within a _Command_, and any _Hooks_ assigned to an _Lifecycle Event_. - Command // CLI configuration, commands, subcommands, options - LifecycleEvent(s) // Events that happen sequentially when the command is run - - Hook(s) // Code that runs when a Lifecycle Event happens during a Command + - Hook(s) // Code that runs when a Lifecycle Event happens during a Command #### Command -A CLI *Command* that can be called by a user, e.g. `serverless deploy`. A Command has no logic, but simply defines the CLI configuration (e.g. command, subcommands, parameters) and the *Lifecycle Events* for the command. Every command defines its own lifecycle events. +A CLI _Command_ that can be called by a user, e.g. `serverless deploy`. A Command has no logic, but simply defines the CLI configuration (e.g. command, subcommands, parameters) and the _Lifecycle Events_ for the command. Every command defines its own lifecycle events. ```javascript 'use strict'; @@ -119,10 +127,7 @@ class MyPlugin { constructor() { this.commands = { deploy: { - lifecycleEvents: [ - 'resources', - 'functions' - ] + lifecycleEvents: ['resources', 'functions'], }, }; } @@ -133,7 +138,7 @@ module.exports = MyPlugin; #### Lifecycle Events -Events that fire sequentially during a Command. The above example list two Events. However, for each Event, and additional `before` and `after` event is created. Therefore, six Events exist in the above example: +Events that fire sequentially during a Command. The above example list two Events. However, for each Event, and additional `before` and `after` event is created. Therefore, six Events exist in the above example: - `before:deploy:resources` - `deploy:resources` @@ -155,17 +160,14 @@ class Deploy { constructor() { this.commands = { deploy: { - lifecycleEvents: [ - 'resources', - 'functions' - ] + lifecycleEvents: ['resources', 'functions'], }, }; this.hooks = { 'before:deploy:resources': this.beforeDeployResources, 'deploy:resources': this.deployResources, - 'after:deploy:functions': this.afterDeployFunctions + 'after:deploy:functions': this.afterDeployFunctions, }; } @@ -196,20 +198,14 @@ class MyPlugin { constructor() { this.commands = { deploy: { - lifecycleEvents: [ - 'resources', - 'functions' - ], + lifecycleEvents: ['resources', 'functions'], commands: { function: { - lifecycleEvents: [ - 'package', - 'deploy' - ], + lifecycleEvents: ['package', 'deploy'], }, }, }, - } + }; } } @@ -226,7 +222,7 @@ Option Shortcuts are passed in with a single dash (`-`) like this: `serverless f The `options` object will be passed in as the second parameter to the constructor of your plugin. -In it, you can optionally add a `shortcut` property, as well as a `required` property. The Framework will return an error if a `required` Option is not included. +In it, you can optionally add a `shortcut` property, as well as a `required` property. The Framework will return an error if a `required` Option is not included. **Note:** At this time, the Serverless Framework does not use parameters. @@ -240,22 +236,20 @@ class Deploy { this.commands = { deploy: { - lifecycleEvents: [ - 'functions' - ], + lifecycleEvents: ['functions'], options: { function: { usage: 'Specify the function you want to deploy (e.g. "--function myFunction")', shortcut: 'f', - required: true - } - } + required: true, + }, + }, }, }; this.hooks = { - 'deploy:functions': this.deployFunction.bind(this) - } + 'deploy:functions': this.deployFunction.bind(this), + }; } deployFunction() { @@ -287,21 +281,19 @@ class ProviderDeploy { this.commands = { deploy: { - lifecycleEvents: [ - 'functions' - ], + lifecycleEvents: ['functions'], options: { function: { usage: 'Specify the function you want to deploy (e.g. "--function myFunction")', - required: true - } - } + required: true, + }, + }, }, }; this.hooks = { - 'deploy:functions': this.deployFunction.bind(this) - } + 'deploy:functions': this.deployFunction.bind(this), + }; } deployFunction() { @@ -328,15 +320,13 @@ class MyPlugin { this.commands = { log: { - lifecycleEvents: [ - 'serverless' - ], + lifecycleEvents: ['serverless'], }, }; this.hooks = { - 'log:serverless': this.logServerless.bind(this) - } + 'log:serverless': this.logServerless.bind(this), + }; } logServerless() { diff --git a/docs/providers/google/guide/services.md b/docs/providers/google/guide/services.md index bb8423f0e..6fbaf9fd7 100644 --- a/docs/providers/google/guide/services.md +++ b/docs/providers/google/guide/services.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/google/guide/services) + # Google - Services @@ -25,7 +27,7 @@ myService/ serverless.yml # Contains all functions and infrastructure resources ``` -However, as your application grows, you can break it out into multiple services. A lot of people organize their services by workflows or data models, and group the functions related to those workflows and data models together in the service. +However, as your application grows, you can break it out into multiple services. A lot of people organize their services by workflows or data models, and group the functions related to those workflows and data models together in the service. ```bash users/ @@ -49,9 +51,9 @@ serverless create --template google-nodejs --path my-service Here are the available runtimes for Google Cloud Functions: -* google-nodejs -* google-go -* google-python +- google-nodejs +- google-go +- google-python Check out the [create command docs](../cli-reference/create) for all the details and options. @@ -114,7 +116,7 @@ Then use the `deploy` command: serverless deploy ``` -Check out the [deployment guide](./deploying.md) to learn more about deployments and how they work. Or, check out the [deploy command docs](../cli-reference/deploy.md) for all the details and options. +Check out the [deployment guide](./deploying.md) to learn more about deployments and how they work. Or, check out the [deploy command docs](../cli-reference/deploy.md) for all the details and options. ## Removal @@ -143,7 +145,7 @@ To configure version pinning define a `frameworkVersion` property in your server ```yml # serverless.yml -frameworkVersion: "=1.0.3" +frameworkVersion: '=1.0.3' ``` #### Version Range @@ -151,10 +153,9 @@ frameworkVersion: "=1.0.3" ```yml # serverless.yml -frameworkVersion: ">=1.0.0 <2.0.0" +frameworkVersion: '>=1.0.0 <2.0.0' ``` - ## Installing Serverless in an existing service If you already have a Serverless service, and would prefer to lock down the framework version using `package.json`, then you can install Serverless as follows: @@ -169,6 +170,7 @@ npm install serverless --save-dev To execute the locally installed Serverless executable you have to reference the binary out of the node modules directory. Example: + ``` node ./node_modules/serverless/bin/serverless deploy ``` diff --git a/docs/providers/google/guide/variables.md b/docs/providers/google/guide/variables.md index 965107e18..fa12f7bf8 100644 --- a/docs/providers/google/guide/variables.md +++ b/docs/providers/google/guide/variables.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/google/guide/variables) + # Google - Variables @@ -24,6 +26,7 @@ The Serverless framework provides a powerful variable system which allows you to **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. ## Reference Properties In serverless.yml + To self-reference properties in `serverless.yml`, use the `${self:someProperty}` syntax in your `serverless.yml`. This functionality is recursive, so you can go as deep in the object tree as you want. ```yml @@ -51,7 +54,8 @@ functions: In the above example you're setting a global event resource for all functions by referencing the `resource` property in the same `serverless.yml` file. This way, you can easily change the event resource for all functions whenever you like. ## Reference Variables in other Files -You can reference variables in other YAML or JSON files. To reference variables in other YAML files use the `${file(./myFile.yml):someProperty}` syntax in your `serverless.yml` configuration file. To reference variables in other JSON files use the `${file(./myFile.json):someProperty}` syntax. It is important that the file you are referencing has the correct suffix, or file extension, for its file type (`.yml` for YAML or `.json` for JSON) in order for it to be interpreted correctly. Here's an example: + +You can reference variables in other YAML or JSON files. To reference variables in other YAML files use the `${file(./myFile.yml):someProperty}` syntax in your `serverless.yml` configuration file. To reference variables in other JSON files use the `${file(./myFile.json):someProperty}` syntax. It is important that the file you are referencing has the correct suffix, or file extension, for its file type (`.yml` for YAML or `.json` for JSON) in order for it to be interpreted correctly. Here's an example: ```yml # myCustomFile.yml @@ -73,13 +77,13 @@ functions: eventType: providers/cloud.pubsub/eventTypes/topics.publish resource: ${file(./myCustomFile.yml):topic} # Or you can reference a specific property world: - handler: pubSub.hello - events: - eventType: providers/cloud.pubsub/eventTypes/topics.publish - resource: ${self:custom.topic} # This would also work in this case + handler: pubSub.hello + events: + eventType: providers/cloud.pubsub/eventTypes/topics.publish + resource: ${self:custom.topic} # This would also work in this case ``` -In the above example, you're referencing the entire `myCustomFile.yml` file in the `custom` property. You need to pass the path relative to your service directory. You can also request specific properties in that file as shown in the `topic` property. It's completely recursive and you can go as deep as you want. Additionally you can request properties that contain arrays from either YAML or JSON reference files. Here's a YAML example for an events array: +In the above example, you're referencing the entire `myCustomFile.yml` file in the `custom` property. You need to pass the path relative to your service directory. You can also request specific properties in that file as shown in the `topic` property. It's completely recursive and you can go as deep as you want. Additionally you can request properties that contain arrays from either YAML or JSON reference files. Here's a YAML example for an events array: ```yml myevents: @@ -89,18 +93,22 @@ myevents: ``` and for JSON: + ```json { - "myevents": [{ - "event" : { - "eventType": "providers/cloud.pubsub/eventTypes/topic.publish", - "resource" : "projects/*/topics/my-topic" + "myevents": [ + { + "event": { + "eventType": "providers/cloud.pubsub/eventTypes/topic.publish", + "resource": "projects/*/topics/my-topic" + } } - }] + ] } ``` In your serverless.yml, depending on the type of your source file, either have the following syntax for YAML + ```yml functions: hello: @@ -109,6 +117,7 @@ functions: ``` or for a JSON reference file use this sytax: + ```yml functions: hello: @@ -127,9 +136,9 @@ References can be either named or unnamed exports. To use the exported `someModu ```javascript // resources.js module.exports.topic = () => { - // Code that generates dynamic data - return 'projects/*/topics/my-topic'; -} + // Code that generates dynamic data + return 'projects/*/topics/my-topic'; +}; ``` ```js @@ -137,9 +146,9 @@ module.exports.topic = () => { module.exports = () => { return { property1: 'some value', - property2: 'some other value' - } -} + property2: 'some other value', + }; +}; ``` ```yml @@ -164,11 +173,11 @@ You can also return an object and reference a specific property. Just make sure ```javascript // myCustomFile.js module.exports.pubSub = () => { - // Code that generates dynamic data - return { - resource: 'projects/*/topics/my-topic' - }; -} + // Code that generates dynamic data + return { + resource: 'projects/*/topics/my-topic', + }; +}; ``` ```yml diff --git a/docs/providers/google/guide/workflow.md b/docs/providers/google/guide/workflow.md index 8b2db4cf6..6eeda3fd0 100644 --- a/docs/providers/google/guide/workflow.md +++ b/docs/providers/google/guide/workflow.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/google/guide/workflow) + # Google - Workflow @@ -18,15 +20,15 @@ Intro. Quick recommendations and tips for various processes. 1. Write your functions 2. Use `serverless deploy` when you've made changes to `serverless.yml` and in CI/CD systems -3. Use `serverless invoke --function myFunction ` to test your Google Cloud Functions +3. Use `serverless invoke --function myFunction` to test your Google Cloud Functions 4. Open up a separate tab in your console and see logs in there via `serverless logs --function myFunction` 5. Write tests to run locally ### Larger Projects - Break your application / project into multiple Serverless Services -- Model your Serverless Services around Data Models or Workflows -- Keep the Functions and Resources in your Serverless Services to a minimum +- Model your Serverless Services around Data Models or Workflows +- Keep the Functions and Resources in your Serverless Services to a minimum ## Cheat Sheet diff --git a/docs/providers/kubeless/README.md b/docs/providers/kubeless/README.md index 49489494a..396fb8002 100644 --- a/docs/providers/kubeless/README.md +++ b/docs/providers/kubeless/README.md @@ -5,7 +5,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/) + # Kubeless Provider Documentation diff --git a/docs/providers/kubeless/cli-reference/README.md b/docs/providers/kubeless/cli-reference/README.md index 246c0aed1..f2ea6d0f9 100644 --- a/docs/providers/kubeless/cli-reference/README.md +++ b/docs/providers/kubeless/cli-reference/README.md @@ -5,11 +5,13 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/kubeless/cli-reference/) + # Serverless Kubeless CLI Reference -Welcome to the Serverless Kubeless CLI Reference! Please select a section on the left to get started. +Welcome to the Serverless Kubeless CLI Reference! Please select a section on the left to get started. If you have questions, join the [chat in gitter](https://gitter.im/serverless/serverless) or [post over on the forums](http://forum.serverless.com/). diff --git a/docs/providers/kubeless/cli-reference/create.md b/docs/providers/kubeless/cli-reference/create.md index 68a1bd6b3..42e39abb9 100644 --- a/docs/providers/kubeless/cli-reference/create.md +++ b/docs/providers/kubeless/cli-reference/create.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/kubeless/cli-reference/create) + # Kubeless - Create @@ -35,6 +37,7 @@ serverless create --template kubeless-nodejs --path my-service ``` ## Options + - `--template` or `-t` The name of one of the available templates. **Required if --template-url and --template-path are not present**. - `--template-url` or `-u` The name of one of the available templates. **Required if --template and --template-path are not present**. - `--template-path` The local path of your template. **Required if --template and --template-url are not present**. @@ -42,6 +45,7 @@ serverless create --template kubeless-nodejs --path my-service - `--name` or `-n` the name of the service in `serverless.yml`. ## Provided lifecycle events + - `create:create` ## Available Templates for Kubeless diff --git a/docs/providers/kubeless/cli-reference/deploy.md b/docs/providers/kubeless/cli-reference/deploy.md index dbfa1232d..35bf7d9de 100644 --- a/docs/providers/kubeless/cli-reference/deploy.md +++ b/docs/providers/kubeless/cli-reference/deploy.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/kubeless/cli-reference/deploy) + # Kubeless - Deploy @@ -23,6 +25,8 @@ serverless deploy This is the simplest deployment usage possible. With this command Serverless will deploy your service to the default Kubernetes cluster in your kubeconfig file. ## Options + +- `--config` or `-c` Path to your conifguration file, if other than `serverless.yml|.yaml|.js|.json`. - `--noDeploy` or `-n` Skips the deployment steps and leaves artifacts in the `.serverless` directory. - `--verbose` or `-v` Shows all stack events during deployment, and display any Stack Output. - `--package` or `-p` The path of a previously packaged deployment to get deployed (skips packaging step). @@ -33,6 +37,7 @@ This is the simplest deployment usage possible. With this command Serverless wil After the `serverless deploy` command runs all created deployment artifacts are placed in the `.serverless` folder of the service. ## Provided lifecycle events + - `deploy:cleanup` - `deploy:initialize` - `deploy:setupProviderConfiguration` diff --git a/docs/providers/kubeless/cli-reference/info.md b/docs/providers/kubeless/cli-reference/info.md index a38028071..b3b3927e2 100644 --- a/docs/providers/kubeless/cli-reference/info.md +++ b/docs/providers/kubeless/cli-reference/info.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/kubeless/cli-reference/info) + # Kubeless - Info @@ -19,9 +21,11 @@ serverless info ``` ## Options + - `--verbose` or `-v` Shows the metadata of the Kubernetes objects. ## Provided lifecycle events + - `info:info` ## Examples @@ -38,7 +42,7 @@ $ serverless info -v Service Information "hello" Cluster IP: 10.0.0.203 Type: ClusterIP -Ports: +Ports: Protocol: TCP Port: 8080 Target Port: 8080 @@ -50,7 +54,7 @@ Function Info Handler: handler.hello Runtime: python2.7 Trigger: HTTP -Dependencies: +Dependencies: Metadata: Self Link: /apis/k8s.io/v1/namespaces/default/functions/hello UID: 7c214cab-8976-11e7-b8c4-0800275c88b3 diff --git a/docs/providers/kubeless/cli-reference/invoke.md b/docs/providers/kubeless/cli-reference/invoke.md index cee892cc5..792cf6e00 100644 --- a/docs/providers/kubeless/cli-reference/invoke.md +++ b/docs/providers/kubeless/cli-reference/invoke.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/kubeless/cli-reference/invoke) + # Kubeless - Invoke @@ -19,12 +21,14 @@ serverless invoke --function functionName ``` ## Options + - `--function` or `-f` The name of the function in your service that you want to invoke. **Required**. - `--data` or `-d` String data to be passed as an event to your function. By default data is read from standard input. - `--path` or `-p` The path to a json file with input data to be passed to the invoked function. This path is relative to the root directory of the service. - `--log` or `-l` If set to `true`, it will output logging data of the invocation. Default is `false`. ## Provided lifecycle events + - `invoke:invoke` ## Examples diff --git a/docs/providers/kubeless/cli-reference/logs.md b/docs/providers/kubeless/cli-reference/logs.md index a6eeeb6fc..d68d669b8 100644 --- a/docs/providers/kubeless/cli-reference/logs.md +++ b/docs/providers/kubeless/cli-reference/logs.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/kubeless/cli-reference/logs) + # Kubeless - Logs @@ -54,11 +56,13 @@ serverless logs -f hello ```bash serverless logs -f hello --startTime 5h ``` + This will fetch the logs that happened in the past 5 hours. ```bash serverless logs -f hello --startTime 1469694264 ``` + This will fetch the logs that happened starting at epoch `1469694264`. ```bash @@ -70,4 +74,5 @@ Serverless will tail the platform log output and print new log messages coming i ```bash serverless logs -f hello --filter serverless ``` + This will fetch only the logs that contain the string `serverless` diff --git a/docs/providers/kubeless/cli-reference/plugin-install.md b/docs/providers/kubeless/cli-reference/plugin-install.md index d59a3e875..d8b96883d 100644 --- a/docs/providers/kubeless/cli-reference/plugin-install.md +++ b/docs/providers/kubeless/cli-reference/plugin-install.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/kubeless/cli-reference/plugin-install) + # Plugin Install @@ -22,9 +24,11 @@ serverless plugin install --name pluginName ``` ## Options + - `--name` or `-n` The plugins name. **Required**. ## Provided lifecycle events + - `plugin:install:install` ## Examples diff --git a/docs/providers/kubeless/cli-reference/plugin-list.md b/docs/providers/kubeless/cli-reference/plugin-list.md index bf1a25fb3..d0aef3df8 100644 --- a/docs/providers/kubeless/cli-reference/plugin-list.md +++ b/docs/providers/kubeless/cli-reference/plugin-list.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/kubeless/cli-reference/plugin-list) + # Plugin List @@ -19,7 +21,9 @@ serverless plugin list ``` ## Options -- *None* + +- _None_ ## Provided lifecycle events + - `plugin:list:list` diff --git a/docs/providers/kubeless/cli-reference/plugin-search.md b/docs/providers/kubeless/cli-reference/plugin-search.md index f594936b1..e7a77070a 100644 --- a/docs/providers/kubeless/cli-reference/plugin-search.md +++ b/docs/providers/kubeless/cli-reference/plugin-search.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/kubeless/cli-reference/plugin-search) + # Plugin Search @@ -19,9 +21,11 @@ serverless plugin search --query query ``` ## Options + - `--query` or `-q` The query you want to use for your search. **Required**. ## Provided lifecycle events + - `plugin:search:search` ## Examples diff --git a/docs/providers/kubeless/cli-reference/plugin-uninstall.md b/docs/providers/kubeless/cli-reference/plugin-uninstall.md index 9e9bc7472..abc0548b4 100644 --- a/docs/providers/kubeless/cli-reference/plugin-uninstall.md +++ b/docs/providers/kubeless/cli-reference/plugin-uninstall.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/kubeless/cli-reference/plugin-uninstall) + # Plugin Uninstall @@ -19,9 +21,11 @@ serverless plugin uninstall --name pluginName ``` ## Options + - `--name` or `-n` The plugins name. **Required**. ## Provided lifecycle events + - `plugin:uninstall:uninstall` ## Examples diff --git a/docs/providers/kubeless/cli-reference/remove.md b/docs/providers/kubeless/cli-reference/remove.md index ea1bd14b6..06260ce55 100644 --- a/docs/providers/kubeless/cli-reference/remove.md +++ b/docs/providers/kubeless/cli-reference/remove.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/kubeless/cli-reference/remove) + # Kubeless - Remove @@ -21,7 +23,9 @@ 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/README.md b/docs/providers/kubeless/events/README.md index 05e2139b9..75ac29613 100644 --- a/docs/providers/kubeless/events/README.md +++ b/docs/providers/kubeless/events/README.md @@ -1,11 +1,13 @@ + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/kubeless/events/) + # Serverless Kubeless Events @@ -15,4 +17,3 @@ Welcome to the Serverless Kubeless Events Glossary! Please select a section on the left to get started. If you have questions, join the [chat in gitter](https://gitter.im/serverless/serverless) or [post over on the forums](http://forum.serverless.com/) - diff --git a/docs/providers/kubeless/events/http.md b/docs/providers/kubeless/events/http.md index 3bb72182e..79d32acb3 100644 --- a/docs/providers/kubeless/events/http.md +++ b/docs/providers/kubeless/events/http.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/kubeless/events/http) + # Kubeless HTTP Events @@ -77,7 +79,6 @@ functions: events: - http: path: /delete - ``` If the events HTTP definitions contain a `path` attribute, when deploying this Serverless YAML definition, Kubeless will create the needed [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) rules to redirect each of the requests to the right service. You will need to create an [Ingress Controller](https://kubernetes.io/docs/concepts/services-networking/ingress/#ingress-controllers) to make use of your Ingress rule(s): diff --git a/docs/providers/kubeless/events/pubsub.md b/docs/providers/kubeless/events/pubsub.md index cd2f707ae..bf95f47bc 100644 --- a/docs/providers/kubeless/events/pubsub.md +++ b/docs/providers/kubeless/events/pubsub.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/kubeless/events/pubsub) + # Kubeless PubSub Events diff --git a/docs/providers/kubeless/events/scheduler.md b/docs/providers/kubeless/events/scheduler.md index 387bcfe63..7c2a027a6 100644 --- a/docs/providers/kubeless/events/scheduler.md +++ b/docs/providers/kubeless/events/scheduler.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/kubeless/events/schedule) + # Kubeless Scheduled Events diff --git a/docs/providers/kubeless/guide/README.md b/docs/providers/kubeless/guide/README.md index 52f54ab66..0b3c1641f 100644 --- a/docs/providers/kubeless/guide/README.md +++ b/docs/providers/kubeless/guide/README.md @@ -1,11 +1,13 @@ + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/kubeless/guide/) + # Serverless Kubeless Guide diff --git a/docs/providers/kubeless/guide/debugging.md b/docs/providers/kubeless/guide/debugging.md index 4dc5b4eb3..0f151a73b 100644 --- a/docs/providers/kubeless/guide/debugging.md +++ b/docs/providers/kubeless/guide/debugging.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/kubeless/guide/debugging) + # Kubeless - Debugging @@ -22,12 +24,12 @@ import json def find(event, context): term = event['data']['term'] - url = "https://feeds.capitalbikeshare.com/stations/stations.json" + url = "https://feeds.capitalbikeshare.com/stations/stations.json" response = urllib2.urlopen(url) stations = json.loads(response.read()) - + hits = [] - + for station in stations["stationBeanList"]: if station["stAddress1"].find(term) > -1: hits.append(station) @@ -78,26 +80,26 @@ Serverless: Calling function: bikesearch... availableBikes: 9, id: 80, location: '' } ] -``` +``` -What happens when something goes wrong? The function currently has no error handling, so that's easy enough to test. Let's invoke the function again with a typo (use *trm* as the name of the input parameter instead of *term*): +What happens when something goes wrong? The function currently has no error handling, so that's easy enough to test. Let's invoke the function again with a typo (use _trm_ as the name of the input parameter instead of _term_): ``` serverless invoke --function bikesearch --data '{"trm":"Albemarle"}' -l # Output Serverless: Calling function: bikesearch... - + Serverless Error --------------------------------------- - + Internal Server Error - + Get Support -------------------------------------------- Docs: docs.serverless.com Bugs: github.com/serverless/serverless/issues Forums: forum.serverless.com Chat: gitter.im/serverless/serverless - + Your Environment Information ----------------------------- OS: darwin Node Version: 8.3.0 diff --git a/docs/providers/kubeless/guide/deploying.md b/docs/providers/kubeless/guide/deploying.md index cdb4883d9..1065260ca 100644 --- a/docs/providers/kubeless/guide/deploying.md +++ b/docs/providers/kubeless/guide/deploying.md @@ -7,12 +7,14 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/kubeless/guide/deploying) + # Kubeless - Deploying -The Serverless Framework was designed to provision your Kubeless Functions and Events. It does this via a couple of methods designed for different types of deployments. +The Serverless Framework was designed to provision your Kubeless Functions and Events. It does this via a couple of methods designed for different types of deployments. ## Deploy All @@ -24,6 +26,8 @@ serverless deploy -v Use this method when you have updated your Function, Event or Resource configuration in `serverless.yml` and you want to deploy that change (or multiple changes at the same time) to your Kubernetes cluster. +**Note:** You can specify a different configuration file name with the the `--config` option. + ### How It Works The Serverless Framework translates all syntax in `serverless.yml` to [the Function object API](https://github.com/kubeless/kubeless/blob/master/pkg/spec/spec.go) calls to provision your Functions and Events. @@ -73,7 +77,6 @@ rs/hello-699783077 1 1 1 2m Kubeless will create a [Kubernetes Deployment](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/) for your function and a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) for each event. - ## Deploy Function 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. diff --git a/docs/providers/kubeless/guide/events.md b/docs/providers/kubeless/guide/events.md index 4f5a42c06..e33ac78a2 100644 --- a/docs/providers/kubeless/guide/events.md +++ b/docs/providers/kubeless/guide/events.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/kubeless/guide/events) + # Kubeless - Events diff --git a/docs/providers/kubeless/guide/functions.md b/docs/providers/kubeless/guide/functions.md index d71927557..34e420c88 100644 --- a/docs/providers/kubeless/guide/functions.md +++ b/docs/providers/kubeless/guide/functions.md @@ -7,12 +7,14 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/kubeless/guide/functions) + # Kubeless - Functions -If you are using Kubeless as a provider, all *functions* inside the service are Kubernetes Function.v1.k8s.io objects. +If you are using Kubeless as a provider, all _functions_ inside the service are Kubernetes Function.v1.k8s.io objects. ## Configuration @@ -89,8 +91,7 @@ You can specify an array of functions, which is useful if you separate your func ```yml # serverless.yml -... - +--- functions: - ${file(./foo-functions.yml)} - ${file(./bar-functions.yml)} @@ -120,9 +121,9 @@ Please see the following repository for sample projects using those runtimes: For installing dependencies the standard dependency file should be placed in the function folder: - - For Python functions, it will use the file `requirements.txt` - - For Nodejs functions, `dependencies` in the `package.json` file will be installed - - For Ruby functions, it will use the file `Gemfile.rb` +- For Python functions, it will use the file `requirements.txt` +- For Nodejs functions, `dependencies` in the `package.json` file will be installed +- For Ruby functions, it will use the file `Gemfile.rb` If one of the above files is found, the depencies will be installed using a [`Init Container`](https://kubernetes.io/docs/concepts/workloads/pods/init-containers/). @@ -169,6 +170,7 @@ functions: environment: TABLE_NAME: tableName2 ``` + ## Secrets Kubernetes [secrets](https://kubernetes.io/docs/concepts/configuration/secret/) can be linked to your serverless function by mounting the file or defining environment variables. These can be configured as such in `serverless.yml`. @@ -179,6 +181,7 @@ Given the kubernetes secret named `secret1` created by: We can access it like so: **Mounting** + ```yml service: service-name provider: @@ -194,10 +197,10 @@ functions: handler: handler.users secrets: - secret1 - ``` **Environment Variables** + ```yml service: service-name provider: @@ -215,15 +218,14 @@ functions: # Note that the secret cannot have any `/`'s in the name handler: handler.users environment: - # The environment variable `PROD_USER_PASSWORD` would be set to "happy" + # The environment variable `PROD_USER_PASSWORD` would be set to "happy" - name: PROD_USER_PASSWORD valueFrom: - secretKeyRef: - - name: secret1 - key: password + secretKeyRef: + - name: secret1 + key: password ``` - ## Labels Using the `labels` configuration makes it possible to add `key` / `value` labels to your functions. diff --git a/docs/providers/kubeless/guide/installation.md b/docs/providers/kubeless/guide/installation.md index 483a87516..08655a3b7 100644 --- a/docs/providers/kubeless/guide/installation.md +++ b/docs/providers/kubeless/guide/installation.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/kubeless/guide/installation) + # Kubeless - Installation @@ -49,4 +51,3 @@ To see which version of serverless you have installed run: ```bash serverless --version ``` - diff --git a/docs/providers/kubeless/guide/intro.md b/docs/providers/kubeless/guide/intro.md index 4a2fdd533..708abcb8f 100644 --- a/docs/providers/kubeless/guide/intro.md +++ b/docs/providers/kubeless/guide/intro.md @@ -7,16 +7,19 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/kubeless/guide/intro) + # Kubeless - Introduction -The Serverless Framework helps you develop and deploy serverless applications using Kubeless. It's a CLI that offers structure, automation and best practices out-of-the-box, allowing you to focus on building sophisticated, event-driven, serverless architectures, comprised of [Functions](#functions) and [Events](#events). +The Serverless Framework helps you develop and deploy serverless applications using Kubeless. It's a CLI that offers structure, automation and best practices out-of-the-box, allowing you to focus on building sophisticated, event-driven, serverless architectures, comprised of [Functions](#functions) and [Events](#events). The Serverless Framework is different than other application frameworks because: -* It manages your code as well as your infrastructure -* It supports multiple languages (Node.js, Python, Ruby) + +- It manages your code as well as your infrastructure +- It supports multiple languages (Node.js, Python, Ruby) ## Core Concepts @@ -24,25 +27,25 @@ Here are the Serverless Framework's main concepts and how they pertain to Kubele ### Functions -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: +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* -* *Performing a scheduled task* (To be added in newer versions) +- _Saving a user to the database_ +- _Processing a file in a database_ +- _Performing a scheduled task_ (To be added in newer versions) -You can perform multiple jobs in your code, but we don't recommend doing that without good reason. Separation of concerns is best and the Framework is designed to help you easily develop and deploy Functions, as well as manage lots of them. +You can perform multiple jobs in your code, but we don't recommend doing that without good reason. Separation of concerns is best and the Framework is designed to help you easily develop and deploy Functions, as well as manage lots of them. ### Events -Anything that triggers an Kubeless Event to execute is regarded by the Framework as an **Event**. Events are platform events on Kubeless such as: +Anything that triggers an Kubeless Event to execute is regarded by the Framework as an **Event**. Events are platform events on Kubeless such as: -* *An API Gateway HTTP endpoint (e.g., for a REST API)* -* *A Kafka queue message (e.g., a message)* -* *A scheduled timer (e.g., run every 5 minutes)* (To be added in newer versions) +- _An API Gateway HTTP endpoint (e.g., for a REST API)_ +- _A Kafka queue message (e.g., a message)_ +- _A scheduled timer (e.g., run every 5 minutes)_ (To be added in newer versions) ### Services -A **Service** is the Serverless Framework's unit of organization (not to be confused with [Kubernetes Services](https://kubernetes.io/docs/concepts/services-networking/service/). You can think of it as a project file, though you can have multiple services for a single application. It's where you define your Functions and the Events that trigger them, all in one file entitled `serverless.yml` (or `serverless.json` or `serverless.js`). It looks like this: +A **Service** is the Serverless Framework's unit of organization (not to be confused with [Kubernetes Services](https://kubernetes.io/docs/concepts/services-networking/service/). You can think of it as a project file, though you can have multiple services for a single application. It's where you define your Functions and the Events that trigger them, all in one file by default entitled `serverless.yml` (or `serverless.json` or `serverless.js`). It looks like this: ```yml # serverless.yml @@ -53,8 +56,8 @@ functions: # Your "Functions" usersCreate: handler: hello.hello # The code to call as a response to the event events: # The "Events" that trigger this function - - http: + - http: path: /hello ``` -When you deploy with the Framework by running `serverless deploy`, everything in `serverless.yml` is deployed at once. +When you deploy with the Framework by running `serverless deploy`, everything in `serverless.yml` (or the file specified with the `--config` option) is deployed at once. diff --git a/docs/providers/kubeless/guide/packaging.md b/docs/providers/kubeless/guide/packaging.md index 2b788756c..b138cebc8 100644 --- a/docs/providers/kubeless/guide/packaging.md +++ b/docs/providers/kubeless/guide/packaging.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/kubeless/guide/packaging) + # Kubeless - Packaging @@ -51,7 +53,7 @@ previously excluded files and directories. Exclude all node_modules but then re-include a specific modules (in this case node-fetch) using `exclude` exclusively -``` yml +```yml package: exclude: - node_modules/** @@ -60,7 +62,7 @@ package: Exclude all files but `handler.js` using `exclude` and `include` -``` yml +```yml package: exclude: - src/** diff --git a/docs/providers/kubeless/guide/quick-start.md b/docs/providers/kubeless/guide/quick-start.md index 7af1071f5..2fea9ac08 100644 --- a/docs/providers/kubeless/guide/quick-start.md +++ b/docs/providers/kubeless/guide/quick-start.md @@ -12,8 +12,8 @@ layout: Doc 1. Node.js `v6.5.0` or later. 2. Serverless CLI `v1.20` or later. You can run -`npm install -g serverless` to install it. -3. Install Kubeless & Dependencies(./installation.md). + `npm install -g serverless` to install it. +3. Install Kubeless & Dependencies(./installation.md). ## Create a new service @@ -32,36 +32,37 @@ $ npm install 1. **Deploy the Service** - Use this when you have made changes to your Functions, Events or Resources in `serverless.yml` or you simply want to deploy all changes within your Service at the same time. +Use this when you have made changes to your Functions, Events or Resources in `serverless.yml` or you simply want to deploy all changes within your Service at the same time. - ```bash - serverless deploy -v - ``` +```bash +serverless deploy -v +``` 2. **Deploy the Function** - Use this to quickly upload and overwrite your function code, allowing you to develop faster. +Use this to quickly upload and overwrite your function code, allowing you to develop faster. - ```bash - serverless deploy function -f capitalize - ``` +```bash +serverless deploy function -f capitalize +``` 3. **Invoke the Function** - Invokes the Function and returns results. +Invokes the Function and returns results. - ```bash - $ serverless invoke --function capitalize --data '"WELCOME TO KUBELESS!"' -l - # results +```bash +$ serverless invoke --function capitalize --data '"WELCOME TO KUBELESS!"' -l +# results "Welcome To Kubeless!" - ``` +``` 4. **Fetch the Function Logs** - Open up a separate tab in your console and stream all logs for a specific Function using this command. - ```bash - serverless logs -f capitalize -t - ``` +Open up a separate tab in your console and stream all logs for a specific Function using this command. + +```bash +serverless logs -f capitalize -t +``` ## Cleanup diff --git a/docs/providers/kubeless/guide/services.md b/docs/providers/kubeless/guide/services.md index a34b0d2fe..81095ed0b 100644 --- a/docs/providers/kubeless/guide/services.md +++ b/docs/providers/kubeless/guide/services.md @@ -7,25 +7,27 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/kubeless/guide/services) + # Kubeless - Services -A `service` in the Serverless Framework is like a project ((not to be confused with [Kubernetes Services](https://kubernetes.io/docs/concepts/services-networking/service/). It's where you define your Kubeless Functions and the `events` that trigger them, all in a file called `serverless.yml`. +A `service` in the Serverless Framework is like a project ((not to be confused with [Kubernetes Services](https://kubernetes.io/docs/concepts/services-networking/service/). It's where you define your Kubeless Functions and the `events` that trigger them, all in a file called `serverless.yml`. To get started building your first Serverless Framework project, create a `service`. ## Organization -In the beginning of an application, many people use a single Service to define all of the Functions and Events. This is what we recommend in the beginning. +In the beginning of an application, many people use a single Service to define all of the Functions and Events. This is what we recommend in the beginning. ```bash myService/ serverless.yml # Contains all functions and infrastructure resources ``` -However, as your application grows, you can break it out into multiple services. A lot of people organize their services by workflows or data models, and group the functions related to those workflows and data models together in the service. +However, as your application grows, you can break it out into multiple services. A lot of people organize their services by workflows or data models, and group the functions related to those workflows and data models together in the service. ```bash users/ @@ -35,11 +37,12 @@ posts/ comments/ serverless.yml # Contains 4 functions that do Comments CRUD operations and the Comments database ``` + This makes sense since related functions usually use common infrastructure resources, and you want to keep those functions and resources together as a single unit of deployment, for better organization and separation of concerns. ## Creation -To create a service, use the `create` command. You must also pass in a runtime (e.g., node.js, python etc.) you would like to write the service in. You can also pass in a path to create a directory and auto-name your service: +To create a service, use the `create` command. You must also pass in a runtime (e.g., node.js, python etc.) you would like to write the service in. You can also pass in a path to create a directory and auto-name your service: ```bash # Create service with the Python template in the folder ./new-project @@ -48,14 +51,15 @@ $ serverless create --template kubeless-python --path new-project Here are the available runtimes for Kubeless using the Serverless plugin: -* kubeless-python -* kubeless-nodejs +- kubeless-python +- kubeless-nodejs Check out the [create command docs](../cli-reference/create) for all the details and options. ## Contents You'll see the following files in your working directory: + - `serverless.yml` - `handler.py` - `package.json` @@ -66,11 +70,11 @@ Each `service` configuration is managed in the `serverless.yml` file. The main r - Declare a Serverless service - Define one or more functions in the service - - Define the provider the service will be deployed to (in our case, kubeless) - - Define any custom plugins to be used (in our case, we will need to use the serverless-kubeless plugin) - - Define events that trigger each function to execute (e.g. HTTP requests) - - Allow events listed in the `events` section to automatically create the resources required for the event upon deployment - - Allow flexible configuration using Serverless Variables + - Define the provider the service will be deployed to (in our case, kubeless) + - Define any custom plugins to be used (in our case, we will need to use the serverless-kubeless plugin) + - Define events that trigger each function to execute (e.g. HTTP requests) + - Allow events listed in the `events` section to automatically create the resources required for the event upon deployment + - Allow flexible configuration using Serverless Variables You can see the name of the service, the provider configuration and the first function inside the `functions` definition which points to the `handler.py` file. Any further service configuration will be done in this file. @@ -112,7 +116,7 @@ To deploy a service, use the `deploy` command: serverless deploy ``` -Check out the [deployment guide](https://serverless.com/framework/docs/providers/kubeless/guide/deploying/) to learn more about deployments and how they work. Or, check out the [deploy command docs](../cli-reference/deploy) for all the details and options. +Check out the [deployment guide](https://serverless.com/framework/docs/providers/kubeless/guide/deploying/) to learn more about deployments and how they work. Or, check out the [deploy command docs](../cli-reference/deploy) for all the details and options. ## Removal @@ -164,4 +168,4 @@ provider: runtime: python2.7 … -``` \ No newline at end of file +``` diff --git a/docs/providers/kubeless/guide/workflow.md b/docs/providers/kubeless/guide/workflow.md index 8d78f10d8..5653e82e0 100644 --- a/docs/providers/kubeless/guide/workflow.md +++ b/docs/providers/kubeless/guide/workflow.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/kubeless/guide/workflow) + # Kubeless - Workflow @@ -24,43 +26,55 @@ Intro. Quick recommendations and tips for various processes. 6. Write tests to run locally. ### Larger Projects -* Break your application/project into multiple Serverless Services. -* Model your Serverless Services around Data Models or Workflows. -* Keep the Functions and Resources in your Serverless Services to a minimum. + +- Break your application/project into multiple Serverless Services. +- Model your Serverless Services around Data Models or Workflows. +- Keep the Functions and Resources in your Serverless Services to a minimum. ## Cheat Sheet + A handy list of commands to use when developing with the Serverless Framework. ##### Create A Service: + Creates a new Service ``` serverless create -p [SERVICE NAME] -t kubeless-python ``` + ``` serverless create -p [SERVICE NAME] -t kubeless-nodejs ``` ##### Deploy All + Use this when you have made changes to your Functions, Events or Resources in `serverless.yml` or you simply want to deploy all changes within your Service at the same time. + ``` serverless deploy ``` ##### Deploy Function + Use this to quickly overwrite your Kubeless Functinos, allowing you to develop faster. + ``` serverless deploy function -f [FUNCTION NAME] ``` ##### Invoke Function + Invokes an Kubeless Function and returns logs. + ``` serverless invoke function -f [FUNCTION NAME] -l ``` ##### Streaming Logs + Open up a separate tab in your console and stream all logs for a specific Function using this command. + ``` serverless logs -f [FUNCTION NAME] ``` diff --git a/docs/providers/openwhisk/README.md b/docs/providers/openwhisk/README.md index f6da72ed7..7c025fd5c 100644 --- a/docs/providers/openwhisk/README.md +++ b/docs/providers/openwhisk/README.md @@ -5,7 +5,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/) + # Apache OpenWhisk Provider Documentation diff --git a/docs/providers/openwhisk/cli-reference/README.md b/docs/providers/openwhisk/cli-reference/README.md index e0452d707..b2c87c74f 100644 --- a/docs/providers/openwhisk/cli-reference/README.md +++ b/docs/providers/openwhisk/cli-reference/README.md @@ -5,12 +5,14 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/openwhisk/cli-reference/) + # Serverless Apache OpenWhisk CLI Reference -Welcome to the Serverless Apache OpenWhisk CLI Reference! Please select a section on the left to get started. +Welcome to the Serverless Apache OpenWhisk CLI Reference! Please select a section on the left to get started. **Note:** Before continuing [Apache OpenWhisk system credentials](../guide/credentials.md) are required for using the CLI. diff --git a/docs/providers/openwhisk/cli-reference/config-credentials.md b/docs/providers/openwhisk/cli-reference/config-credentials.md index 27c1d0890..4413ae9dd 100644 --- a/docs/providers/openwhisk/cli-reference/config-credentials.md +++ b/docs/providers/openwhisk/cli-reference/config-credentials.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/openwhisk/cli-reference/config-credentials) + # OpenWhisk - Config Credentials diff --git a/docs/providers/openwhisk/cli-reference/create.md b/docs/providers/openwhisk/cli-reference/create.md index f0bb2a4cb..e6ce3777b 100644 --- a/docs/providers/openwhisk/cli-reference/create.md +++ b/docs/providers/openwhisk/cli-reference/create.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/openwhisk/cli-reference/create) + # OpenWhisk - Create @@ -27,6 +29,7 @@ serverless create --template openwhisk-nodejs --path myService ``` ## Options + - `--template` or `-t` The name of one of the available templates. **Required if --template-url and --template-path are not present**. - `--template-url` or `-u` The name of one of the available templates. **Required if --template and --template-path are not present**. - `--template-path` The local path of your template. **Required if --template and --template-url are not present**. @@ -34,6 +37,7 @@ serverless create --template openwhisk-nodejs --path myService - `--name` or `-n` the name of the service in `serverless.yml`. ## Provided lifecycle events + - `create:create` ## Available Templates diff --git a/docs/providers/openwhisk/cli-reference/deploy-function.md b/docs/providers/openwhisk/cli-reference/deploy-function.md index bcbcbd584..d16c58def 100644 --- a/docs/providers/openwhisk/cli-reference/deploy-function.md +++ b/docs/providers/openwhisk/cli-reference/deploy-function.md @@ -7,12 +7,14 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/openwhisk/cli-reference/deploy-function) + # OpenWhisk - Deploy Function -The `sls deploy function` command deploys an individual function. This command simply compiles a deployment package with a single function handler. This is a much faster way of deploying changes in code. +The `sls deploy function` command deploys an individual function. This command simply compiles a deployment package with a single function handler. This is a much faster way of deploying changes in code. ```bash serverless deploy function -f functionName @@ -22,4 +24,5 @@ serverless deploy function -f functionName properties such as environment variables and events will **not** be deployed. ## Options + - `--function` or `-f` The name of the function which should be deployed diff --git a/docs/providers/openwhisk/cli-reference/deploy.md b/docs/providers/openwhisk/cli-reference/deploy.md index 6adb35ee9..aa0753e45 100644 --- a/docs/providers/openwhisk/cli-reference/deploy.md +++ b/docs/providers/openwhisk/cli-reference/deploy.md @@ -7,18 +7,22 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/openwhisk/cli-reference/deploy) + # OpenWhisk - Deploy -The `sls deploy` command deploys your entire service via the Apache OpenWhisk platform API. Run this command when you have made service changes (i.e., you edited `serverless.yml`). Use `serverless deploy function -f myFunction` when you have made code changes and you want to quickly upload your updated code to Apache OpenWhisk. +The `sls deploy` command deploys your entire service via the Apache OpenWhisk platform API. Run this command when you have made service changes (i.e., you edited `serverless.yml`). Use `serverless deploy function -f myFunction` when you have made code changes and you want to quickly upload your updated code to Apache OpenWhisk. ```bash serverless deploy ``` ## Options + +- `--config` or `-c` Path to your conifguration file, if other than `serverless.yml|.yaml|.js|.json`. - `--noDeploy` or `-n` Skips the deployment steps and leaves artifacts in the `.serverless` directory - `--verbose` or `-v` Shows all stack events during deployment, and display any Stack Output. - `--function` or `-f` Invoke `deploy function` (see above). Convenience shortcut - cannot be used with `--package`. @@ -39,6 +43,7 @@ This is the simplest deployment usage possible. With this command Serverless wil OpenWhisk platform endpoints. ## Provided lifecycle events + - `deploy:cleanup` - `deploy:initialize` - `deploy:setupProviderConfiguration` diff --git a/docs/providers/openwhisk/cli-reference/info.md b/docs/providers/openwhisk/cli-reference/info.md index f9312c96c..a06b68f50 100644 --- a/docs/providers/openwhisk/cli-reference/info.md +++ b/docs/providers/openwhisk/cli-reference/info.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/openwhisk/cli-reference/info) + # OpenWhisk - Info @@ -19,9 +21,11 @@ serverless info ``` ## Options + - `--verbose` or `-v` Shows displays any Stack Output. ## Provided lifecycle events + - `info:info` ## Examples diff --git a/docs/providers/openwhisk/cli-reference/install.md b/docs/providers/openwhisk/cli-reference/install.md index a92475c13..88a2ff607 100644 --- a/docs/providers/openwhisk/cli-reference/install.md +++ b/docs/providers/openwhisk/cli-reference/install.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/openwhisk/cli-reference/install) + # OpenWhisk - Install @@ -19,12 +21,21 @@ serverless install --url https://github.com/some/service ``` ## Options + - `--url` or `-u` The services GitHub URL. **Required**. - `--name` or `-n` Name for the service. ## Provided lifecycle events + - `install:install` +## Supported Code Hosting Platforms + +- GitHub +- GitHub Enterprise +- GitLab +- BitBucket + ## Examples ### Installing a service from a GitHub URL diff --git a/docs/providers/openwhisk/cli-reference/invoke-local.md b/docs/providers/openwhisk/cli-reference/invoke-local.md index 4bb1ab52d..d393521c0 100644 --- a/docs/providers/openwhisk/cli-reference/invoke-local.md +++ b/docs/providers/openwhisk/cli-reference/invoke-local.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/openwhisk/cli-reference/invoke-local) + # OpenWhisk - Invoke Local @@ -18,13 +20,14 @@ This runs your code locally by emulating the Apache OpenWhisk environment. Pleas serverless invoke local --function functionName ``` -__*Please note that only the JavaScript and Python runtimes are supported with this command.*__ +**_Please note that only the JavaScript and Python runtimes are supported with this command._** ## Options - `--function` or `-f` The name of the function in your service that you want to invoke locally. **Required**. - `--path` or `-p` The path to a json file holding input data to be passed to the invoked function. This path is relative to the root directory of the service. The json file should have event and context properties to hold your mocked event and context data. - `--data` or `-d` String data to be passed as an event to your function. Keep in mind that if you pass both `--path` and `--data`, the data included in the `--path` file will overwrite the data you passed with the `--data` flag. + * `--env` or `-e` String representing an environment variable to set when invoking your function, in the form `=`. Can be repeated for more than one environment variable. ## Examples diff --git a/docs/providers/openwhisk/cli-reference/invoke.md b/docs/providers/openwhisk/cli-reference/invoke.md index b007ad4f6..8435db20a 100644 --- a/docs/providers/openwhisk/cli-reference/invoke.md +++ b/docs/providers/openwhisk/cli-reference/invoke.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/openwhisk/cli-reference/invoke) + # OpenWhisk - Invoke @@ -19,6 +21,7 @@ serverless invoke [local] --function functionName ``` ## Options + - `--function` or `-f` The name of the function in your service that you want to invoke. **Required**. - `--data` or `-d` String data to be passed as an event to your function. By default data is read from standard input. - `--path` or `-p` The path to a json file with input data to be passed to the invoked function. This path is relative to the root directory of the service. @@ -26,6 +29,7 @@ serverless invoke [local] --function functionName - `--log` or `-l` If set to `true` and invocation type is `RequestResponse`, it will output logging data of the invocation. Default is `false`. ## Provided lifecycle events + - `invoke:invoke` # Invoke Local @@ -37,9 +41,10 @@ serverless invoke local --function functionName ``` ## Options + - `--function` or `-f` The name of the function in your service that you want to invoke locally. **Required**. - `--path` or `-p` The path to a json file holding input data to be passed to the invoked function. This path is relative to the -root directory of the service. The json file should have event and context properties to hold your mocked event and context data. + root directory of the service. The json file should have event and context properties to hold your mocked event and context data. - `--data` or `-d` String data to be passed as an event to your function. Keep in mind that if you pass both `--path` and `--data`, the data included in the `--path` file will overwrite the data you passed with the `--data` flag. ## Examples diff --git a/docs/providers/openwhisk/cli-reference/login.md b/docs/providers/openwhisk/cli-reference/login.md index 366bae169..6963da5a3 100644 --- a/docs/providers/openwhisk/cli-reference/login.md +++ b/docs/providers/openwhisk/cli-reference/login.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/openwhisk/cli-reference/login) + # Login diff --git a/docs/providers/openwhisk/cli-reference/logs.md b/docs/providers/openwhisk/cli-reference/logs.md index 48966c72f..59b37ae74 100644 --- a/docs/providers/openwhisk/cli-reference/logs.md +++ b/docs/providers/openwhisk/cli-reference/logs.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/openwhisk/cli-reference/logs) + # OpenWhisk - Logs @@ -56,11 +58,13 @@ serverless logs -f hello ```bash serverless logs -f hello --startTime 5h ``` + This will fetch the logs that happened in the past 5 hours. ```bash serverless logs -f hello --startTime 1469694264 ``` + This will fetch the logs that happened starting at epoch `1469694264`. ```bash @@ -72,4 +76,5 @@ Serverless will tail the platform log output and print new log messages coming i ```bash serverless logs -f hello --filter serverless ``` + This will fetch only the logs that contain the string `serverless` diff --git a/docs/providers/openwhisk/cli-reference/plugin-install.md b/docs/providers/openwhisk/cli-reference/plugin-install.md index 7dd6983ab..e2c0d8b40 100644 --- a/docs/providers/openwhisk/cli-reference/plugin-install.md +++ b/docs/providers/openwhisk/cli-reference/plugin-install.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/openwhisk/cli-reference/plugin-install) + # Plugin Install @@ -22,9 +24,11 @@ serverless plugin install --name pluginName ``` ## Options + - `--name` or `-n` The plugins name. **Required**. ## Provided lifecycle events + - `plugin:install:install` ## Examples diff --git a/docs/providers/openwhisk/cli-reference/plugin-list.md b/docs/providers/openwhisk/cli-reference/plugin-list.md index cd9e53ce6..05733a1ab 100644 --- a/docs/providers/openwhisk/cli-reference/plugin-list.md +++ b/docs/providers/openwhisk/cli-reference/plugin-list.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/openwhisk/cli-reference/plugin-list) + # Plugin List @@ -19,7 +21,9 @@ serverless plugin list ``` ## Options -- *None* + +- _None_ ## Provided lifecycle events + - `plugin:list:list` diff --git a/docs/providers/openwhisk/cli-reference/plugin-search.md b/docs/providers/openwhisk/cli-reference/plugin-search.md index 6a62b91b9..cc1e8a4c3 100644 --- a/docs/providers/openwhisk/cli-reference/plugin-search.md +++ b/docs/providers/openwhisk/cli-reference/plugin-search.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/openwhisk/cli-reference/plugin-search) + # Plugin Search @@ -19,9 +21,11 @@ serverless plugin search --query query ``` ## Options + - `--query` or `-q` The query you want to use for your search. **Required**. ## Provided lifecycle events + - `plugin:search:search` ## Examples diff --git a/docs/providers/openwhisk/cli-reference/plugin-uninstall.md b/docs/providers/openwhisk/cli-reference/plugin-uninstall.md index ebd58731a..62418afce 100644 --- a/docs/providers/openwhisk/cli-reference/plugin-uninstall.md +++ b/docs/providers/openwhisk/cli-reference/plugin-uninstall.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/openwhisk/cli-reference/plugin-uninstall) + # Plugin Uninstall @@ -19,9 +21,11 @@ serverless plugin uninstall --name pluginName ``` ## Options + - `--name` or `-n` The plugins name. **Required**. ## Provided lifecycle events + - `plugin:uninstall:uninstall` ## Examples diff --git a/docs/providers/openwhisk/cli-reference/print.md b/docs/providers/openwhisk/cli-reference/print.md index b48882404..f8aade890 100644 --- a/docs/providers/openwhisk/cli-reference/print.md +++ b/docs/providers/openwhisk/cli-reference/print.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/openwhisk/cli-reference/print) + # Print @@ -42,13 +44,13 @@ custom: functions: hello: - handler: handler.hello - events: - - schedule: ${self:custom.globalSchedule} + handler: handler.hello + events: + - schedule: ${self:custom.globalSchedule} world: - handler: handler.world - events: - - schedule: ${self:custom.globalSchedule} + handler: handler.world + events: + - schedule: ${self:custom.globalSchedule} ``` Using `sls print` will resolve the variables in the `schedule` blocks. diff --git a/docs/providers/openwhisk/cli-reference/remove.md b/docs/providers/openwhisk/cli-reference/remove.md index f531ce51e..fd2f7c58a 100644 --- a/docs/providers/openwhisk/cli-reference/remove.md +++ b/docs/providers/openwhisk/cli-reference/remove.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/openwhisk/cli-reference/remove) + # OpenWhisk - Remove @@ -19,6 +21,7 @@ serverless remove ``` ## Provided lifecycle events + - `remove:remove` ## Examples diff --git a/docs/providers/openwhisk/cli-reference/slstats.md b/docs/providers/openwhisk/cli-reference/slstats.md index e07997b14..e7d7b4509 100644 --- a/docs/providers/openwhisk/cli-reference/slstats.md +++ b/docs/providers/openwhisk/cli-reference/slstats.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/openwhisk/cli-reference/slstats) + # Serverless Statistics @@ -19,10 +21,12 @@ serverless slstats --enable ``` ## Options + - `--enable` or `-e`. - `--disable` or `-d` ## Provided lifecycle events + - `slstats:slstats` ## Examples diff --git a/docs/providers/openwhisk/events/README.md b/docs/providers/openwhisk/events/README.md index e63b685cc..d8f397c22 100644 --- a/docs/providers/openwhisk/events/README.md +++ b/docs/providers/openwhisk/events/README.md @@ -1,11 +1,13 @@ + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/openwhisk/events/) + # Serverless Apache OpenWhisk Events diff --git a/docs/providers/openwhisk/events/apigateway.md b/docs/providers/openwhisk/events/apigateway.md index e448ceb01..78b32c41d 100644 --- a/docs/providers/openwhisk/events/apigateway.md +++ b/docs/providers/openwhisk/events/apigateway.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/openwhisk/events/apigateway) + # API Gateway @@ -144,8 +146,8 @@ functions: This feature comes with the following restrictions: -- *Path parameters are only supported when `resp` is configured as`http`.* -- *Individual path parameter values are not included as separate event parameters. Users have to manually parse values from the full `__ow_path` value.* +- _Path parameters are only supported when `resp` is configured as`http`._ +- _Individual path parameter values are not included as separate event parameters. Users have to manually parse values from the full `__ow_path` value._ ### Security diff --git a/docs/providers/openwhisk/events/cloudant.md b/docs/providers/openwhisk/events/cloudant.md index 2f8cccc3e..918cfabda 100644 --- a/docs/providers/openwhisk/events/cloudant.md +++ b/docs/providers/openwhisk/events/cloudant.md @@ -7,12 +7,14 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/openwhisk/events/cloudant) + # Cloudant -This event allows you to connect functions to [IBM Cloudant](https://cloudant.com/), a NoSQL database-as-a-service based upon [Apache CouchDB](http://couchdb.apache.org/). Functions are invoked for each database modification that occurs. +This event allows you to connect functions to [IBM Cloudant](https://cloudant.com/), a NoSQL database-as-a-service based upon [Apache CouchDB](http://couchdb.apache.org/). Functions are invoked for each database modification that occurs. This event utilise the trigger feed provided by the [Cloudant package](https://github.com/openwhisk/openwhisk-package-cloudant). @@ -24,7 +26,7 @@ This event utilise the trigger feed provided by the [Cloudant package](https://g /${BLUEMIX_ORG}_${BLUEMIX_SPACE}/Bluemix_${SERVICE_NAME}_Credentials-1 ``` -## Configuration +## Configuration Users need to pass the database credentials and the database name to listen to changes on when defining the event. @@ -37,13 +39,12 @@ Developers only need to add the database to listen to for each event. ```yaml # serverless.yaml functions: - index: - handler: users.main - events: - - cloudant: - package: /${BLUEMIX_ORG}_${BLUEMIX_SPACE}/Bluemix_${SERVICE_NAME}_Credentials-1 - db: db_name - + index: + handler: users.main + events: + - cloudant: + package: /${BLUEMIX_ORG}_${BLUEMIX_SPACE}/Bluemix_${SERVICE_NAME}_Credentials-1 + db: db_name ``` The configuration will create a trigger called `${serviceName}_${fnName}_cloudant_${db}` and a rule called `${serviceName}_${fnName}_cloudant_${db}_rule` to bind the function to the database update events. @@ -57,14 +58,14 @@ Authentication credentials for the Cloudant event source can be defined explicit ```yaml # serverless.yaml functions: - index: - handler: users.main - events: - - cloudant: - host: xxx-yyy-zzz-bluemix.cloudant.com - username: USERNAME - password: PASSWORD - db: db_name + index: + handler: users.main + events: + - cloudant: + host: xxx-yyy-zzz-bluemix.cloudant.com + username: USERNAME + password: PASSWORD + db: db_name ``` ### Adding Optional Parameters @@ -73,7 +74,7 @@ The following optional feed parameters are also supported: - `max` - Maximum number of triggers to fire. Defaults to infinite. - `filter` - Filter function defined on a design document. -- `query` - Optional query parameters for the filter function. +- `query` - Optional query parameters for the filter function. ```yaml # serverless.yaml @@ -81,10 +82,10 @@ functions: index: handler: users.main events: - - cloudant: + - cloudant: ... - max: 10000 - query: + max: 10000 + query: status: new filter: mailbox/by_status ``` @@ -99,15 +100,15 @@ functions: index: handler: users.main events: - - cloudant: + - cloudant: package: /${BLUEMIX_ORG}_${BLUEMIX_SPACE}/Bluemix_${SERVICE_NAME}_Credentials-1 db: my_db trigger: db_events - rule: connect_index_to_db + rule: connect_index_to_db another: handler: users.another events: - - trigger: db_events + - trigger: db_events ``` ## Event Details @@ -122,12 +123,12 @@ The JSON representation of the trigger event is as follows: ```json { - "id": "6ca436c44074c4c2aa6a40c9a188b348", - "seq": "2-g1AAAAL9aJyV-GJCaEuqx4-BktQkYp_dmIfC", - "changes": [ - { - "rev": "2-da3f80848a480379486fb4a2ad98fa16" - } - ] + "id": "6ca436c44074c4c2aa6a40c9a188b348", + "seq": "2-g1AAAAL9aJyV-GJCaEuqx4-BktQkYp_dmIfC", + "changes": [ + { + "rev": "2-da3f80848a480379486fb4a2ad98fa16" + } + ] } ``` diff --git a/docs/providers/openwhisk/events/messagehub.md b/docs/providers/openwhisk/events/messagehub.md index 833660d07..7afa116b9 100644 --- a/docs/providers/openwhisk/events/messagehub.md +++ b/docs/providers/openwhisk/events/messagehub.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/openwhisk/events/messagehub) + # Message Hub @@ -24,7 +26,7 @@ This event utilise the trigger feed provided by the [Message Hub package](https: /${BLUEMIX_ORG}_${BLUEMIX_SPACE}/Bluemix_${SERVICE_NAME}_Credentials-1 ``` -## Configuration +## Configuration Users need to pass the message hub credentials and the kafka topic to listen for messages on when defining the event. @@ -37,16 +39,15 @@ Developers only need to add the kafka topic to listen for messages on with each ```yaml # serverless.yaml functions: - index: - handler: users.main - events: - - message_hub: - package: /${BLUEMIX_ORG}_${BLUEMIX_SPACE}/Bluemix_${SERVICE_NAME}_Credentials-1 - topic: my_kafka_topic - + index: + handler: users.main + events: + - message_hub: + package: /${BLUEMIX_ORG}_${BLUEMIX_SPACE}/Bluemix_${SERVICE_NAME}_Credentials-1 + topic: my_kafka_topic ``` -*Optional parameters `json`, `binary_key`, `binary_value` are also supported.* +_Optional parameters `json`, `binary_key`, `binary_value` are also supported._ The configuration will create a trigger called `${serviceName}_${fnName}_messagehub_${db}` and a rule called `${serviceName}_${fnName}_messagehub_${db}_rule` to bind the function to the database update events. @@ -59,18 +60,18 @@ Authentication credentials for the Message Hub event source can be defined expli ```yaml # serverless.yaml functions: - index: - handler: users.main - events: - - message_hub: - topic: my_kafka_topic - brokers: afka01-prod01.messagehub.services.us-south.bluemix.net:9093 - user: USERNAME - password: PASSWORD - admin_url: https://kafka-admin-prod01.messagehub.services.us-south.bluemix.net:443 - json: true - binary_key: true - binary_value: true + index: + handler: users.main + events: + - message_hub: + topic: my_kafka_topic + brokers: afka01-prod01.messagehub.services.us-south.bluemix.net:9093 + user: USERNAME + password: PASSWORD + admin_url: https://kafka-admin-prod01.messagehub.services.us-south.bluemix.net:443 + json: true + binary_key: true + binary_value: true ``` `topic`, `brokers`, `user`, `password` and `admin_url` are mandatory parameters. @@ -85,34 +86,35 @@ functions: index: handler: users.main events: - - message_hub: + - message_hub: package: /${BLUEMIX_ORG}_${BLUEMIX_SPACE}/Bluemix_${SERVICE_NAME}_Credentials-1 topic: my_kafka_topic trigger: log_events - rule: connect_index_to_kafka + rule: connect_index_to_kafka another: handler: users.another events: - - trigger: log_events + - trigger: log_events ``` ## Event Details -The payload of that trigger event will contain a `messages` field which is an array of messages that have been posted since the last time your trigger fired. +The payload of that trigger event will contain a `messages` field which is an array of messages that have been posted since the last time your trigger fired. The JSON representation of a sample event is as follows: ```json { - "messages": [ - { - "partition": 0, - "key": "U29tZSBrZXk=", - "offset": 421760, - "topic": "mytopic", - "value": "Some value" - } - ] + "messages": [ + { + "partition": 0, + "key": "U29tZSBrZXk=", + "offset": 421760, + "topic": "mytopic", + "value": "Some value" + } + ] } ``` + For more details on the exact semantics of the message properties, please see the [trigger feed documentation](https://github.com/openwhisk/openwhisk-package-kafka). diff --git a/docs/providers/openwhisk/events/schedule.md b/docs/providers/openwhisk/events/schedule.md index 86f2690fe..7edad0ba3 100644 --- a/docs/providers/openwhisk/events/schedule.md +++ b/docs/providers/openwhisk/events/schedule.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/openwhisk/events/schedule) + # Schedule @@ -35,8 +37,7 @@ functions: - schedule: cron(* * * * *) // run every minute ``` -This automatically generates a new trigger (``${service}_crawl_schedule_trigger`) -and rule (`${service}_crawl_schedule_rule`) during deployment. +This automatically generates a new trigger (``\${service}\_crawl_schedule_trigger`) and rule (`\${service}\_crawl_schedule_rule`) during deployment. ### Customise Parameters diff --git a/docs/providers/openwhisk/events/triggers.md b/docs/providers/openwhisk/events/triggers.md index f7e465b31..6c97a0c89 100644 --- a/docs/providers/openwhisk/events/triggers.md +++ b/docs/providers/openwhisk/events/triggers.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/openwhisk/events/streams) + # Triggers @@ -30,6 +32,7 @@ functions: events: - trigger: my_trigger ``` + This configuration will create a trigger called `servicename-my_trigger` with an active rule binding `my_function` to this event stream. ## Customising Rules @@ -42,8 +45,8 @@ functions: handler: index.main events: - trigger: - name: "my_trigger" - rule: "rule_name" + name: 'my_trigger' + rule: 'rule_name' ``` ## Customising Triggers diff --git a/docs/providers/openwhisk/examples/README.md b/docs/providers/openwhisk/examples/README.md index efb151111..25c9bb174 100644 --- a/docs/providers/openwhisk/examples/README.md +++ b/docs/providers/openwhisk/examples/README.md @@ -5,19 +5,21 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/openwhisk/examples/) + # Serverless Apache OpenWhisk Examples Have an example? Submit a PR or [open an issue](https://github.com/serverless/examples/issues). ⚡️ -| Example | Runtime | -| :--------------------------------------- | :------ | -| [OpenWhisk Node Simple](https://serverless.com/examples/openwhisk-node-simple/)
    Boilerplate project repository for OpenWhisk provider with Serverless Framework. | nodeJS | -| [OpenWhisk Python Simple](https://serverless.com/examples/openwhisk-python-simple/)
    Boilerplate project repository for OpenWhisk provider with Serverless Framework. | python | -| [OpenWhisk PHP Simple](https://serverless.com/examples/openwhisk-php-simple/)
    Boilerplate project repository for OpenWhisk provider with Serverless Framework. | php | -| [OpenWhisk Ruby Simple](https://serverless.com/examples/openwhisk-ruby-simple/)
    Boilerplate project repository for OpenWhisk provider with Serverless Framework. | ruby | -| [OpenWhisk Swift Simple](https://serverless.com/examples/openwhisk-swift-simple/)
    Boilerplate project repository for OpenWhisk provider with Serverless Framework. | swift | +| Example | Runtime | +| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------ | +| [OpenWhisk Node Simple](https://serverless.com/examples/openwhisk-node-simple/)
    Boilerplate project repository for OpenWhisk provider with Serverless Framework. | nodeJS | +| [OpenWhisk Python Simple](https://serverless.com/examples/openwhisk-python-simple/)
    Boilerplate project repository for OpenWhisk provider with Serverless Framework. | python | +| [OpenWhisk PHP Simple](https://serverless.com/examples/openwhisk-php-simple/)
    Boilerplate project repository for OpenWhisk provider with Serverless Framework. | php | +| [OpenWhisk Ruby Simple](https://serverless.com/examples/openwhisk-ruby-simple/)
    Boilerplate project repository for OpenWhisk provider with Serverless Framework. | ruby | +| [OpenWhisk Swift Simple](https://serverless.com/examples/openwhisk-swift-simple/)
    Boilerplate project repository for OpenWhisk provider with Serverless Framework. | swift | If you have questions, join the [chat in gitter](https://gitter.im/serverless/serverless) or [post over on the forums](https://forum.serverless.com/) diff --git a/docs/providers/openwhisk/examples/hello-world/README.md b/docs/providers/openwhisk/examples/hello-world/README.md index 81a38b238..ba5ef113a 100644 --- a/docs/providers/openwhisk/examples/hello-world/README.md +++ b/docs/providers/openwhisk/examples/hello-world/README.md @@ -6,7 +6,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/openwhisk/examples/hello-world/) + # Hello World Serverless Example 🌍 @@ -15,10 +17,10 @@ Welcome to the Hello World example. Pick your language of choice: -* [JavaScript](./node) -* [PHP](./php) -* [Python](./python) -* [Ruby](./ruby) -* [Swift](./swift) +- [JavaScript](./node) +- [PHP](./php) +- [Python](./python) +- [Ruby](./ruby) +- [Swift](./swift) [View all examples](https://www.serverless.com/framework/docs/providers/openwhisk/examples/) diff --git a/docs/providers/openwhisk/examples/hello-world/node/README.md b/docs/providers/openwhisk/examples/hello-world/node/README.md index c2d1ab8ce..4b532e83b 100644 --- a/docs/providers/openwhisk/examples/hello-world/node/README.md +++ b/docs/providers/openwhisk/examples/hello-world/node/README.md @@ -6,7 +6,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/openwhisk/examples/hello-world/node/) + # Hello World Node.js Example @@ -14,15 +16,19 @@ layout: Doc Make sure `serverless` is installed. [See installation guide](../../../guide/installation.md). ## 1. Create a service -`serverless create --template openwhisk-nodejs --path myService` or `sls create --template openwhisk-nodejs --path myService`, where 'myService' is a new folder to be created with template service files. Change directories into this new folder. + +`serverless create --template openwhisk-nodejs --path myService` or `sls create --template openwhisk-nodejs --path myService`, where 'myService' is a new folder to be created with template service files. Change directories into this new folder. ## 2. Install Provider Plugin -`npm install ` in the service directory. + +`npm install` in the service directory. ## 3. Deploy + `serverless deploy` or `sls deploy`. `sls` is shorthand for the Serverless CLI command ## 4. Invoke deployed function + `serverless invoke --function helloWorld` or `serverless invoke -f helloWorld` `-f` is shorthand for `--function` diff --git a/docs/providers/openwhisk/examples/hello-world/node/handler.js b/docs/providers/openwhisk/examples/hello-world/node/handler.js index 98edcb3ea..0687ec854 100644 --- a/docs/providers/openwhisk/examples/hello-world/node/handler.js +++ b/docs/providers/openwhisk/examples/hello-world/node/handler.js @@ -1,7 +1,7 @@ 'use strict'; // Your function handler -module.exports.helloWorldHandler = function (params) { +module.exports.helloWorldHandler = function(params) { const name = params.name || 'World'; return { payload: `Hello, ${name}!` }; }; diff --git a/docs/providers/openwhisk/examples/hello-world/node/serverless.yml b/docs/providers/openwhisk/examples/hello-world/node/serverless.yml index 3b24c663b..85806026b 100644 --- a/docs/providers/openwhisk/examples/hello-world/node/serverless.yml +++ b/docs/providers/openwhisk/examples/hello-world/node/serverless.yml @@ -10,6 +10,6 @@ functions: events: - http: GET hello -# remember to run npm install to download the provider plugin. +# remember to run npm install to download the provider plugin. plugins: - - serverless-openwhisk + - serverless-openwhisk diff --git a/docs/providers/openwhisk/examples/hello-world/php/README.md b/docs/providers/openwhisk/examples/hello-world/php/README.md index 944b0e6e3..d9f9cf8da 100644 --- a/docs/providers/openwhisk/examples/hello-world/php/README.md +++ b/docs/providers/openwhisk/examples/hello-world/php/README.md @@ -6,7 +6,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/openwhisk/examples/hello-world/php/) + # Hello World PHP Example @@ -14,15 +16,19 @@ layout: Doc Make sure `serverless` is installed. [See installation guide](../../../guide/installation.md). ## 1. Create a service -`serverless create --template openwhisk-php --path myService` or `sls create --template openwhisk-php --path myService`, where 'myService' is a new folder to be created with template service files. Change directories into this new folder. + +`serverless create --template openwhisk-php --path myService` or `sls create --template openwhisk-php --path myService`, where 'myService' is a new folder to be created with template service files. Change directories into this new folder. ## 2. Install Provider Plugin + Run `npm install` in the service directory. ## 3. Deploy + `serverless deploy` or `sls deploy`. `sls` is shorthand for the Serverless CLI command ## 4. Invoke deployed function + `serverless invoke --function helloWorld` or `serverless invoke -f helloWorld` `-f` is shorthand for `--function` diff --git a/docs/providers/openwhisk/examples/hello-world/php/serverless.yml b/docs/providers/openwhisk/examples/hello-world/php/serverless.yml index c81933e48..817c49472 100644 --- a/docs/providers/openwhisk/examples/hello-world/php/serverless.yml +++ b/docs/providers/openwhisk/examples/hello-world/php/serverless.yml @@ -11,6 +11,6 @@ functions: events: - http: GET hello -# remember to run npm install to download the provider plugin. +# remember to run npm install to download the provider plugin. plugins: - - serverless-openwhisk + - serverless-openwhisk diff --git a/docs/providers/openwhisk/examples/hello-world/python/README.md b/docs/providers/openwhisk/examples/hello-world/python/README.md index dd09ceed7..a835598d5 100644 --- a/docs/providers/openwhisk/examples/hello-world/python/README.md +++ b/docs/providers/openwhisk/examples/hello-world/python/README.md @@ -6,7 +6,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/openwhisk/examples/hello-world/python/) + # Hello World Python Example @@ -14,15 +16,19 @@ layout: Doc Make sure `serverless` is installed. [See installation guide](../../../guide/installation.md). ## 1. Create a service -`serverless create --template openwhisk-python --path myService` or `sls create --template openwhisk-python --path myService`, where 'myService' is a new folder to be created with template service files. Change directories into this new folder. + +`serverless create --template openwhisk-python --path myService` or `sls create --template openwhisk-python --path myService`, where 'myService' is a new folder to be created with template service files. Change directories into this new folder. ## 2. Install Provider Plugin + `npm install` in the service directory. ## 3. Deploy + `serverless deploy` or `sls deploy`. `sls` is shorthand for the Serverless CLI command ## 4. Invoke deployed function + `serverless invoke --function helloWorld` or `serverless invoke -f helloWorld` `-f` is shorthand for `--function` diff --git a/docs/providers/openwhisk/examples/hello-world/python/serverless.yml b/docs/providers/openwhisk/examples/hello-world/python/serverless.yml index 23dbe7a93..589deb408 100644 --- a/docs/providers/openwhisk/examples/hello-world/python/serverless.yml +++ b/docs/providers/openwhisk/examples/hello-world/python/serverless.yml @@ -11,6 +11,6 @@ functions: events: - http: GET hello -# remember to run npm install to download the provider plugin. +# remember to run npm install to download the provider plugin. plugins: - - serverless-openwhisk + - serverless-openwhisk diff --git a/docs/providers/openwhisk/examples/hello-world/ruby/README.md b/docs/providers/openwhisk/examples/hello-world/ruby/README.md index 24a6ad805..0986f31e4 100644 --- a/docs/providers/openwhisk/examples/hello-world/ruby/README.md +++ b/docs/providers/openwhisk/examples/hello-world/ruby/README.md @@ -15,17 +15,17 @@ This is a template Ruby service for the OpenWhisk platform. Before you can deplo Before you can deploy your service to OpenWhisk, you need to have an account registered with the platform. -- *Want to run the platform locally?* Please read the project's [*Quick Start*](https://github.com/openwhisk/openwhisk#quick-start) guide for deploying it locally. -- *Want to use a hosted provider?* Please sign up for an account with [IBM Bluemix](https://console.ng.bluemix.net/) and then follow the instructions for getting access to [OpenWhisk on Bluemix](https://console.ng.bluemix.net/openwhisk/). +- _Want to run the platform locally?_ Please read the project's [_Quick Start_](https://github.com/openwhisk/openwhisk#quick-start) guide for deploying it locally. +- _Want to use a hosted provider?_ Please sign up for an account with [IBM Bluemix](https://console.ng.bluemix.net/) and then follow the instructions for getting access to [OpenWhisk on Bluemix](https://console.ng.bluemix.net/openwhisk/). Account credentials for OpenWhisk can be provided through a configuration file or environment variables. This plugin requires the API endpoint, namespace and authentication credentials. -**Do you want to use a configuration file for storing these values?** Please [follow the instructions](https://console.ng.bluemix.net/openwhisk/cli) for setting up the OpenWhisk command-line utility. This tool stores account credentials in the `.wskprops` file in the user's home directory. The plugin automatically extracts credentials from this file at runtime. No further configuration is needed. +**Do you want to use a configuration file for storing these values?** Please [follow the instructions](https://console.ng.bluemix.net/openwhisk/cli) for setting up the OpenWhisk command-line utility. This tool stores account credentials in the `.wskprops` file in the user's home directory. The plugin automatically extracts credentials from this file at runtime. No further configuration is needed. **Do you want to use environment variables for credentials?** Use the following environment variables to be pass in account credentials. These values override anything extracted from the configuration file. -- *OW_APIHOST* - Platform endpoint, e.g. `openwhisk.ng.bluemix.net` -- *OW_AUTH* - Authentication key, e.g. `xxxxxx:yyyyy +- _OW_APIHOST_ - Platform endpoint, e.g. `openwhisk.ng.bluemix.net` +- _OW_AUTH_ - Authentication key, e.g. `xxxxxx:yyyyy ### Have you installed the provider plugin? @@ -45,8 +45,6 @@ Use the `serverless` command to deploy your service. The sample `handler.js` fil serverless deploy ``` - - ### Issues / Feedback / Feature Requests? If you have any issues, comments or want to see new features, please file an issue in the project repository: diff --git a/docs/providers/openwhisk/examples/hello-world/swift/README.md b/docs/providers/openwhisk/examples/hello-world/swift/README.md index 55904219f..04b43bb41 100644 --- a/docs/providers/openwhisk/examples/hello-world/swift/README.md +++ b/docs/providers/openwhisk/examples/hello-world/swift/README.md @@ -6,7 +6,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/openwhisk/examples/hello-world/swift/) + # Hello World Swift Example @@ -14,15 +16,19 @@ layout: Doc Make sure `serverless` is installed. [See installation guide](../../../guide/installation.md). ## 1. Create a service -`serverless create --template openwhisk-swift --path myService` or `sls create --template openwhisk-swift --path myService`, where 'myService' is a new folder to be created with template service files. Change directories into this new folder. + +`serverless create --template openwhisk-swift --path myService` or `sls create --template openwhisk-swift --path myService`, where 'myService' is a new folder to be created with template service files. Change directories into this new folder. ## 2. Install Provider Plugin + `npm install` in the service directory. ## 3. Deploy + `serverless deploy` or `sls deploy`. `sls` is shorthand for the Serverless CLI command ## 4. Invoke deployed function + `serverless invoke --function helloWorld` or `serverless invoke -f helloWorld` `-f` is shorthand for `--function` diff --git a/docs/providers/openwhisk/examples/hello-world/swift/serverless.yml b/docs/providers/openwhisk/examples/hello-world/swift/serverless.yml index 2a8f2a56c..a0c372c96 100644 --- a/docs/providers/openwhisk/examples/hello-world/swift/serverless.yml +++ b/docs/providers/openwhisk/examples/hello-world/swift/serverless.yml @@ -11,6 +11,6 @@ functions: events: - http: GET hello -# remember to run npm install to download the provider plugin. +# remember to run npm install to download the provider plugin. plugins: - - serverless-openwhisk + - serverless-openwhisk diff --git a/docs/providers/openwhisk/guide/README.md b/docs/providers/openwhisk/guide/README.md index 8861c09b7..e7015357b 100644 --- a/docs/providers/openwhisk/guide/README.md +++ b/docs/providers/openwhisk/guide/README.md @@ -5,7 +5,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/openwhisk/guide/) + # Serverless Apache OpenWhisk Guide diff --git a/docs/providers/openwhisk/guide/credentials.md b/docs/providers/openwhisk/guide/credentials.md index 02abd7dc6..5fa8d579a 100644 --- a/docs/providers/openwhisk/guide/credentials.md +++ b/docs/providers/openwhisk/guide/credentials.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/openwhisk/guide/credentials) + # OpenWhisk - Credentials @@ -26,23 +28,23 @@ Here's how to get started… - Sign up for a free account @ [IBM Cloud](https://console.bluemix.net/) -IBM Cloud comes with a [lite account](https://console.bluemix.net/registration/) that does not need credit card details to register. Lite accounts provide free access to certain platform services and do not expire after a limited time period. +IBM Cloud comes with a [lite account](https://console.bluemix.net/registration/) that does not need credit card details to register. Lite accounts provide free access to certain platform services and do not expire after a limited time period. **All IBM Cloud users get access to the [Free Tier for IBM Cloud Functions](https://console.ng.bluemix.net/openwhisk/learn/pricing). This includes 400,000 GB-seconds of serverless function compute time per month.** -Additional execution time is charged at $0.000017 per GB-second of execution, rounded to the nearest 100ms. +Additional execution time is charged at \$0.000017 per GB-second of execution, rounded to the nearest 100ms. ### Install the IBM Cloud CLI Following the [instructions on this page](https://console.bluemix.net/docs/cli/index.html#overview) to download and install the IBM Cloud CLI. -*On Linux, you can run this command:* +_On Linux, you can run this command:_ ``` curl -fsSL https://clis.ng.bluemix.net/install/linux | sh ``` -*On OS X, you can run this command:* +_On OS X, you can run this command:_ ``` curl -fsSL https://clis.ng.bluemix.net/install/osx | sh @@ -78,7 +80,7 @@ ibmcloud wsk property get --auth #### Regions -Cloud Functions is available with the following regions US-South (`api.ng.bluemix.net`), London (`api.eu-gb.bluemix.net`), Frankfurt (` api.eu-de.bluemix.net`). Use the appropriate [API endpoint](https://console.bluemix.net/docs/overview/ibm-cloud.html#ov_intro_reg) to target Cloud Functions in that region. +Cloud Functions is available with the following regions US-South (`api.ng.bluemix.net`), London (`api.eu-gb.bluemix.net`), Frankfurt (`api.eu-de.bluemix.net`). Use the appropriate [API endpoint](https://console.bluemix.net/docs/overview/ibm-cloud.html#ov_intro_reg) to target Cloud Functions in that region. #### Organisations and Spaces @@ -86,7 +88,7 @@ Organisations and spaces for your account can be viewed on this page: [https://c Accounts normally have a default organisation using the account email address. Default space name is usually `dev`. -*After running the login command, authentication credentials will be stored in the `.wskprops` file under your home directory.* +_After running the login command, authentication credentials will be stored in the `.wskprops` file under your home directory._ ## Register with OpenWhisk platform (Self-Hosted) @@ -108,15 +110,14 @@ cd openwhisk/tools/vagrant This platform will now be running inside a virtual machine at the following IP address: `192.168.33.13` -**Please note:** *If you are using a self-hosted platform, the `ignore_certs` property in `serverless.yaml` needs to be `true`. This allows the client to be used against local deployments of OpenWhisk with a self-signed certificate.* +**Please note:** _If you are using a self-hosted platform, the `ignore_certs` property in `serverless.yaml` needs to be `true`. This allows the client to be used against local deployments of OpenWhisk with a self-signed certificate._ ```yaml service: testing provider: name: openwhisk ignore_certs: true -functions: - ... +functions: ... ``` ### Access Account Credentials @@ -177,7 +178,7 @@ Credentials are stored in `~/.wskprops`, which you can edit directly if needed. ##### Edit file manually -The following configuration values should be stored in a new file (`.wskprops`) in your home directory. Replace the `PLATFORM_API_HOST`, `USER_AUTH_KEY` and (optionally) `ACCESS_TOKEN` values will the credentials from above. +The following configuration values should be stored in a new file (`.wskprops`) in your home directory. Replace the `PLATFORM_API_HOST`, `USER_AUTH_KEY` and (optionally) `ACCESS_TOKEN` values will the credentials from above. ``` APIHOST=PLATFORM_API_HOST diff --git a/docs/providers/openwhisk/guide/deploying.md b/docs/providers/openwhisk/guide/deploying.md index 3236e3c88..c286e00a2 100644 --- a/docs/providers/openwhisk/guide/deploying.md +++ b/docs/providers/openwhisk/guide/deploying.md @@ -7,12 +7,14 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/openwhisk/guide/deploying) + # OpenWhisk - Deploying -The Serverless Framework was designed to provision your Apache OpenWhisk Functions, Triggers and Rules safely and quickly. It does this via a couple of methods designed for different types of deployments. +The Serverless Framework was designed to provision your Apache OpenWhisk Functions, Triggers and Rules safely and quickly. It does this via a couple of methods designed for different types of deployments. ## Deploy All @@ -24,21 +26,23 @@ serverless deploy Use this method when you have updated your Function, Event or Resource configuration in `serverless.yml` and you want to deploy that change (or multiple changes at the same time) to Apache OpenWhisk. +**Note:** You can specify a different configuration file name with the the `--config` option. + ### How It Works The Serverless Framework translates all syntax in `serverless.yml` to [platform API](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/openwhisk/openwhisk/master/core/controller/src/main/resources/whiskswagger.json) calls to provision your Actions, Triggers, Rules and APIs. -* Provider plugin parses `serverless.yml` configuration and translates to OpenWhisk resources. -* The code of your Functions is then packaged into zip files. -* Resources are deployed in the following order: *Functions, Function Sequences, API Routes, Triggers, Feeds, Rules.* -* Resources stages are deployed sequentially due to potential dependencies between the stages. -* Resources within a stage are deployed in parallel. -* Stages without any resources defined will be skipped. +- Provider plugin parses `serverless.yml` configuration and translates to OpenWhisk resources. +- The code of your Functions is then packaged into zip files. +- Resources are deployed in the following order: _Functions, Function Sequences, API Routes, Triggers, Feeds, Rules._ +- Resources stages are deployed sequentially due to potential dependencies between the stages. +- Resources within a stage are deployed in parallel. +- Stages without any resources defined will be skipped. ### Tips -* Use this in your CI/CD systems, as it is the safest method of deployment. -* Apache OpenWhisk has a [maximum action artifact](http://bit.ly/2vQIC9V) size of 48MB. This might be an issue if you are using lots of NPM packages. JavaScript build tools like webpack can help to minify your code and save space. +- Use this in your CI/CD systems, as it is the safest method of deployment. +- Apache OpenWhisk has a [maximum action artifact](http://bit.ly/2vQIC9V) size of 48MB. This might be an issue if you are using lots of NPM packages. JavaScript build tools like webpack can help to minify your code and save space. Check out the [deploy command docs](../cli-reference/deploy.md) for all details and options. @@ -52,13 +56,13 @@ serverless deploy function --function myFunction ### How It Works -* The Framework packages up the targeted Apache OpenWhisk Action into a zip file. -* That zip file is deployed to Apache OpenWhisk using the platform API. +- The Framework packages up the targeted Apache OpenWhisk Action into a zip file. +- That zip file is deployed to Apache OpenWhisk using the platform API. ### Tips -* Use this when you are developing and want to test on Apache OpenWhisk because it's much faster. -* During development, people will often run this command several times, as opposed to `serverless deploy` which is only run when larger infrastructure provisioning is required. +- Use this when you are developing and want to test on Apache OpenWhisk because it's much faster. +- During development, people will often run this command several times, as opposed to `serverless deploy` which is only run when larger infrastructure provisioning is required. Check out the [deploy command docs](../cli-reference/deploy.md) for all details and options. diff --git a/docs/providers/openwhisk/guide/events.md b/docs/providers/openwhisk/guide/events.md index b2ad809d1..33b86866d 100644 --- a/docs/providers/openwhisk/guide/events.md +++ b/docs/providers/openwhisk/guide/events.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/openwhisk/guide/events) + # OpenWhisk - Events @@ -45,12 +47,12 @@ functions: events: # All events associated with this function - http: GET /users/create - http: POST /users/update - - trigger: "custom trigger" + - trigger: 'custom trigger' ``` ## Types -The Serverless Framework supports all of the Apache OpenWhisk events and more. Instead of listing them here, we've put them in a separate section, since they have a lot of configurations and functionality. [Check out the events section for more information.](../events) +The Serverless Framework supports all of the Apache OpenWhisk events and more. Instead of listing them here, we've put them in a separate section, since they have a lot of configurations and functionality. [Check out the events section for more information.](../events) ## Deploying diff --git a/docs/providers/openwhisk/guide/functions.md b/docs/providers/openwhisk/guide/functions.md index 263057f8c..8c38f3595 100644 --- a/docs/providers/openwhisk/guide/functions.md +++ b/docs/providers/openwhisk/guide/functions.md @@ -7,12 +7,14 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/openwhisk/guide/functions) + # OpenWhisk - Functions -If you are using OpenWhisk as a provider, all *functions* inside the service are OpenWhisk Actions. +If you are using OpenWhisk as a provider, all _functions_ inside the service are OpenWhisk Actions. ## Configuration @@ -43,7 +45,7 @@ The `handler` property points to the file and module containing the code you wan ```javascript // handler.js -exports.handler = function(params) {} +exports.handler = function(params) {}; ``` You can add as many functions as you want within this property. @@ -104,8 +106,7 @@ You can specify an array of functions, which is useful if you separate your func ```yml # serverless.yml -... - +--- functions: - ${file(./foo-functions.yml)} - ${file(./bar-functions.yml)} @@ -123,7 +124,7 @@ deleteFoo: OpenWhisk provides a concept called "packages" to manage related actions. Packages can contain multiple actions under a common identifier in a namespace. Configuration values needed by all actions in a package can be set as default properties on the package, rather than individually on each action. -*Packages are identified using the following format:* `/namespaceName/packageName/actionName`. +_Packages are identified using the following format:_ `/namespaceName/packageName/actionName`. ### Implicit Packages @@ -133,10 +134,10 @@ Functions can be assigned to packages by setting the function `name` with a pack functions: foo: handler: handler.foo - name: "myPackage/foo" + name: 'myPackage/foo' bar: handler: handler.bar - name: "myPackage/bar" + name: 'myPackage/bar' ``` In this example, two new actions (`foo` & `bar`) will be created using the `myPackage` package. @@ -151,7 +152,7 @@ Packages can also be defined explicitly to set shared configuration parameters. functions: foo: handler: handler.foo - name: "myPackage/foo" + name: 'myPackage/foo' resources: packages: @@ -161,11 +162,11 @@ resources: hello: world ``` -*Explicit packages support the following properties: `name`, `parameters`, `annotations` and `shared`.* +_Explicit packages support the following properties: `name`, `parameters`, `annotations` and `shared`._ ## Binding Services (IBM Cloud Functions) -***This feature requires the [IBM Cloud CLI](https://console.bluemix.net/docs/cli/reference/bluemix_cli/download_cli.html#download_install) and [IBM Cloud Functions plugin](https://console.bluemix.net/openwhisk/learn/cli) to be installed.*** +**_This feature requires the [IBM Cloud CLI](https://console.bluemix.net/docs/cli/reference/bluemix_cli/download_cli.html#download_install) and [IBM Cloud Functions plugin](https://console.bluemix.net/openwhisk/learn/cli) to be installed._** IBM Cloud Functions supports [automatic binding of service credentials](https://console.bluemix.net/docs/openwhisk/binding_services.html#binding_services) to actions using the CLI. @@ -176,7 +177,7 @@ This feature is also available through the `serverless.yaml` file using the `bin ```yaml functions: my_function: - handler: file_name.handler + handler: file_name.handler bind: - service: name: cloud-object-storage @@ -186,10 +187,10 @@ functions: The `service` configuration supports the following properties. - `name`: identifier for the cloud service -- `instance`: instance name for service (*optional*) -- `key`: key name for instance and service (*optional*) +- `instance`: instance name for service (_optional_) +- `key`: key name for instance and service (_optional_) -*If the `instance` or `key` properties are missing, the first available instance and key found will be used.* +_If the `instance` or `key` properties are missing, the first available instance and key found will be used._ Binding services removes the need to manually create default parameters for service keys from platform services. @@ -206,6 +207,7 @@ resources: name: cloud-object-storage instance: my-cos-storage ``` + ## Runtimes The OpenWhisk provider plugin supports the following runtimes. diff --git a/docs/providers/openwhisk/guide/installation.md b/docs/providers/openwhisk/guide/installation.md index c7f1cb0b5..861f2c6e9 100644 --- a/docs/providers/openwhisk/guide/installation.md +++ b/docs/providers/openwhisk/guide/installation.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/openwhisk/guide/installation) + # OpenWhisk - Installation diff --git a/docs/providers/openwhisk/guide/intro.md b/docs/providers/openwhisk/guide/intro.md index 06738f35b..fd6b4b951 100644 --- a/docs/providers/openwhisk/guide/intro.md +++ b/docs/providers/openwhisk/guide/intro.md @@ -7,16 +7,19 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/openwhisk/guide/intro) + # OpenWhisk - Introduction -The Serverless Framework helps you develop and deploy serverless applications using Apache OpenWhisk. It's a CLI that offers structure, automation and best practices out-of-the-box, allowing you to focus on building sophisticated, event-driven, serverless architectures, comprised of [Functions](#functions) and [Events](#events). +The Serverless Framework helps you develop and deploy serverless applications using Apache OpenWhisk. It's a CLI that offers structure, automation and best practices out-of-the-box, allowing you to focus on building sophisticated, event-driven, serverless architectures, comprised of [Functions](#functions) and [Events](#events). The Serverless Framework is different than other application frameworks because: -* It manages your code as well as your infrastructure -* It supports multiple languages (Node.js, Python, PHP, Ruby, Swift, Java, and more) + +- It manages your code as well as your infrastructure +- It supports multiple languages (Node.js, Python, PHP, Ruby, Swift, Java, and more) ## Core Concepts @@ -24,30 +27,30 @@ Here are the Framework's main concepts and how they pertain to Apache OpenWhisk ### Functions -A Function is an [Apache OpenWhisk Action](http://bit.ly/2wMfe3s). 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 an [Apache OpenWhisk Action](http://bit.ly/2wMfe3s). 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* -* *Performing a scheduled task* +- _Saving a user to the database_ +- _Processing a file in a database_ +- _Performing a scheduled task_ -You can perform multiple jobs in your code, but we don't recommend doing that without good reason. Separation of concerns is best and the Framework is designed to help you easily develop and deploy Functions, as well as manage lots of them. +You can perform multiple jobs in your code, but we don't recommend doing that without good reason. Separation of concerns is best and the Framework is designed to help you easily develop and deploy Functions, as well as manage lots of them. ### Events -Anything that triggers an Apache OpenWhisk Action to execute is regarded by the Framework as an **Event**. Events are platform events on Apache OpenWhisk such as: +Anything that triggers an Apache OpenWhisk Action to execute is regarded by the Framework as an **Event**. Events are platform events on Apache OpenWhisk such as: -* *An API Gateway HTTP endpoint (e.g., for a REST API)* -* *A NoSQL database update (e.g., for a user profile)* -* *A scheduled timer (e.g., run every 5 minutes)* -* *A Kafka queue message (e.g., a message)* -* *A Webhook fires (e.g., Github project update)* -* *And more...* +- _An API Gateway HTTP endpoint (e.g., for a REST API)_ +- _A NoSQL database update (e.g., for a user profile)_ +- _A scheduled timer (e.g., run every 5 minutes)_ +- _A Kafka queue message (e.g., a message)_ +- _A Webhook fires (e.g., Github project update)_ +- _And more..._ When you define an event for your Apache OpenWhisk Action in the Serverless Framework, the Framework will automatically translate this into [Triggers and Rules](http://bit.ly/2xQmFE8) needed for that event and configure your functions to listen to it. ### Services -A **Service** is the Framework's unit of organization. You can think of it as a project file, though you can have multiple services for a single application. It's where you define your Functions, the Events that trigger them, and the Resources your Functions use, all in one file entitled `serverless.yml` (or `serverless.json` or `serverless.js`). It looks like this: +A **Service** is the Framework's unit of organization. You can think of it as a project file, though you can have multiple services for a single application. It's where you define your Functions, the Events that trigger them, and the Resources your Functions use, all in one file by default entitled `serverless.yml` (or `serverless.json` or `serverless.js`). It looks like this: ```yml # serverless.yml @@ -62,11 +65,12 @@ functions: # Your "Functions" events: - http: delete /users/delete ``` -When you deploy with the Framework by running `serverless deploy`, everything in `serverless.yml` is deployed at once. + +When you deploy with the Framework by running `serverless deploy`, everything in `serverless.yml` (or the file specified with the `--config` option) is deployed at once. ### Plugins -You can overwrite or extend the functionality of the Framework using **Plugins**. Every `serverless.yml` can contain a `plugins:` property, which features multiple plugins. +You can overwrite or extend the functionality of the Framework using **Plugins**. Every `serverless.yml` can contain a `plugins:` property, which features multiple plugins. ```yml # serverless.yml diff --git a/docs/providers/openwhisk/guide/packaging.md b/docs/providers/openwhisk/guide/packaging.md index 0ad78da8a..35825add7 100644 --- a/docs/providers/openwhisk/guide/packaging.md +++ b/docs/providers/openwhisk/guide/packaging.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/openwhisk/guide/packaging) + # OpenWhisk - Packaging @@ -47,7 +49,7 @@ previously excluded files and directories. Exclude all node_modules but then re-include a specific modules (in this case node-fetch) using `exclude` exclusively -``` yml +```yml package: exclude: - node_modules/** @@ -56,7 +58,7 @@ package: Exclude all files but `handler.js` using `exclude` and `include` -``` yml +```yml package: exclude: - src/** diff --git a/docs/providers/openwhisk/guide/plugins.md b/docs/providers/openwhisk/guide/plugins.md index c0d27405f..a0b769921 100644 --- a/docs/providers/openwhisk/guide/plugins.md +++ b/docs/providers/openwhisk/guide/plugins.md @@ -7,19 +7,21 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/openwhisk/guide/plugins) + # OpenWhisk - Plugins -A Plugin is custom Javascript code that creates new or extends existing commands within the Serverless Framework. The Serverless Framework is merely a group of Plugins that are provided in the core. If you or your organization have a specific workflow, install a pre-written Plugin or write a plugin to customize the Framework to your needs. External Plugins are written exactly the same way as the core Plugins. +A Plugin is custom Javascript code that creates new or extends existing commands within the Serverless Framework. The Serverless Framework is merely a group of Plugins that are provided in the core. If you or your organization have a specific workflow, install a pre-written Plugin or write a plugin to customize the Framework to your needs. External Plugins are written exactly the same way as the core Plugins. - [How to create serverless plugins - Part 1](https://serverless.com/blog/writing-serverless-plugins/) - [How to create serverless plugins - Part 2](https://serverless.com/blog/writing-serverless-plugins-2/) ## Installing Plugins -External Plugins are added on a per service basis and are not applied globally. Make sure you are in your Service's root directory, then install the corresponding Plugin with the help of NPM: +External Plugins are added on a per service basis and are not applied globally. Make sure you are in your Service's root directory, then install the corresponding Plugin with the help of NPM: ``` npm install --save custom-serverless-plugin @@ -33,9 +35,11 @@ We need to tell Serverless that we want to use the plugin inside our service. We plugins: - custom-serverless-plugin ``` + The `plugins` section supports two formats: Array object: + ```yml plugins: - plugin1 @@ -43,6 +47,7 @@ plugins: ``` Enhanced plugins object: + ```yml plugins: localPath: './custom_serverless_plugins' @@ -66,18 +71,21 @@ custom: If you are working on a plugin or have a plugin that is just designed for one project they can be loaded from the local folder. Local plugins can be added in the `plugins` array in `serverless.yml`. By default local plugins can be added to the `.serverless_plugins` directory at the root of your service, and in the `plugins` array in `serverless.yml`. + ```yml plugins: - custom-serverless-plugin ``` Local plugins folder can be changed by enhancing `plugins` object: + ```yml plugins: localPath: './custom_serverless_plugins' modules: - custom-serverless-plugin ``` + The `custom-serverless-plugin` will be loaded from the `custom_serverless_plugins` directory at the root of your service. If the `localPath` is not provided or empty `.serverless_plugins` directory will be taken as the `localPath`. The plugin will be loaded based on being named `custom-serverless-plugin.js` or `custom-serverless-plugin\index.js` in the root of `localPath` folder (`.serverless_plugins` by default). @@ -102,15 +110,15 @@ In this case `plugin1` is loaded before `plugin2`. #### Plugin -Code which defines *Commands*, any *Events* within a *Command*, and any *Hooks* assigned to an *Lifecycle Event*. +Code which defines _Commands_, any _Events_ within a _Command_, and any _Hooks_ assigned to an _Lifecycle Event_. -* Command // CLI configuration, commands, subcommands, options - * LifecycleEvent(s) // Events that happen sequentially when the command is run - * Hook(s) // Code that runs when a Lifecycle Event happens during a Command +- Command // CLI configuration, commands, subcommands, options + - LifecycleEvent(s) // Events that happen sequentially when the command is run + - Hook(s) // Code that runs when a Lifecycle Event happens during a Command #### Command -A CLI *Command* that can be called by a user, e.g. `serverless deploy`. A Command has no logic, but simply defines the CLI configuration (e.g. command, subcommands, parameters) and the *Lifecycle Events* for the command. Every command defines its own lifecycle events. +A CLI _Command_ that can be called by a user, e.g. `serverless deploy`. A Command has no logic, but simply defines the CLI configuration (e.g. command, subcommands, parameters) and the _Lifecycle Events_ for the command. Every command defines its own lifecycle events. ```javascript 'use strict'; @@ -119,10 +127,7 @@ class MyPlugin { constructor() { this.commands = { deploy: { - lifecycleEvents: [ - 'resources', - 'functions' - ] + lifecycleEvents: ['resources', 'functions'], }, }; } @@ -133,7 +138,7 @@ module.exports = MyPlugin; #### Lifecycle Events -Events that fire sequentially during a Command. The above example list two Events. However, for each Event, and additional `before` and `after` event is created. Therefore, six Events exist in the above example: +Events that fire sequentially during a Command. The above example list two Events. However, for each Event, and additional `before` and `after` event is created. Therefore, six Events exist in the above example: - `before:deploy:resources` - `deploy:resources` @@ -155,17 +160,14 @@ class Deploy { constructor() { this.commands = { deploy: { - lifecycleEvents: [ - 'resources', - 'functions' - ] + lifecycleEvents: ['resources', 'functions'], }, }; this.hooks = { 'before:deploy:resources': this.beforeDeployResources, 'deploy:resources': this.deployResources, - 'after:deploy:functions': this.afterDeployFunctions + 'after:deploy:functions': this.afterDeployFunctions, }; } @@ -196,20 +198,14 @@ class MyPlugin { constructor() { this.commands = { deploy: { - lifecycleEvents: [ - 'resources', - 'functions' - ], + lifecycleEvents: ['resources', 'functions'], commands: { function: { - lifecycleEvents: [ - 'package', - 'deploy' - ], + lifecycleEvents: ['package', 'deploy'], }, }, }, - } + }; } } @@ -226,7 +222,7 @@ Option Shortcuts are passed in with a single dash (`-`) like this: `serverless f The `options` object will be passed in as the second parameter to the constructor of your plugin. -In it, you can optionally add a `shortcut` property, as well as a `required` property. The Framework will return an error if a `required` Option is not included. +In it, you can optionally add a `shortcut` property, as well as a `required` property. The Framework will return an error if a `required` Option is not included. **Note:** At this time, the Serverless Framework does not use parameters. @@ -240,22 +236,20 @@ class Deploy { this.commands = { deploy: { - lifecycleEvents: [ - 'functions' - ], + lifecycleEvents: ['functions'], options: { function: { usage: 'Specify the function you want to deploy (e.g. "--function myFunction")', shortcut: 'f', - required: true - } - } + required: true, + }, + }, }, }; this.hooks = { - 'deploy:functions': this.deployFunction.bind(this) - } + 'deploy:functions': this.deployFunction.bind(this), + }; } deployFunction() { @@ -287,21 +281,19 @@ class ProviderDeploy { this.commands = { deploy: { - lifecycleEvents: [ - 'functions' - ], + lifecycleEvents: ['functions'], options: { function: { usage: 'Specify the function you want to deploy (e.g. "--function myFunction")', - required: true - } - } + required: true, + }, + }, }, }; this.hooks = { - 'deploy:functions': this.deployFunction.bind(this) - } + 'deploy:functions': this.deployFunction.bind(this), + }; } deployFunction() { @@ -328,15 +320,13 @@ class MyPlugin { this.commands = { log: { - lifecycleEvents: [ - 'serverless' - ], + lifecycleEvents: ['serverless'], }, }; this.hooks = { - 'log:serverless': this.logServerless.bind(this) - } + 'log:serverless': this.logServerless.bind(this), + }; } logServerless() { diff --git a/docs/providers/openwhisk/guide/quick-start.md b/docs/providers/openwhisk/guide/quick-start.md index a62bf6419..953a9ce98 100644 --- a/docs/providers/openwhisk/guide/quick-start.md +++ b/docs/providers/openwhisk/guide/quick-start.md @@ -12,11 +12,11 @@ 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. + `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://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.* + _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._ ```bash $ npm install --global serverless serverless-openwhisk @@ -34,6 +34,7 @@ $ cd my-service # Install npm dependencies $ npm install ``` + **Using a self-hosted version of the platform?** Ensure you set the `ignore_certs` option in the `serverless.yaml` prior to deployment. @@ -48,44 +49,45 @@ provider: 1. **Deploy the Service** - Use this when you have made changes to your Functions, Events or Resources in `serverless.yml` or you simply want to deploy all changes within your Service at the same time. +Use this when you have made changes to your Functions, Events or Resources in `serverless.yml` or you simply want to deploy all changes within your Service at the same time. - ```bash - serverless deploy -v - ``` +```bash +serverless deploy -v +``` 2. **Deploy the Function** - Use this to quickly upload and overwrite your function code, allowing you to develop faster. +Use this to quickly upload and overwrite your function code, allowing you to develop faster. - ```bash - serverless deploy function -f hello - ``` +```bash +serverless deploy function -f hello +``` 3. **Invoke the Function** - Invokes the Function and returns results. +Invokes the Function and returns results. - ```bash - serverless invoke --function hello - # results - { - "payload": "Hello, World!" - } +```bash +serverless invoke --function hello +# results +{ + "payload": "Hello, World!" +} - serverless invoke --function hello --data '{"name": "OpenWhisk"}' - #results - { - "payload": "Hello, OpenWhisk!" - } - ``` +serverless invoke --function hello --data '{"name": "OpenWhisk"}' +#results +{ + "payload": "Hello, OpenWhisk!" +} +``` 4. **Fetch the Function Logs** - Open up a separate tab in your console and stream all logs for a specific Function using this command. - ```bash - serverless logs -f hello -t - ``` +Open up a separate tab in your console and stream all logs for a specific Function using this command. + +```bash +serverless logs -f hello -t +``` ## Cleanup diff --git a/docs/providers/openwhisk/guide/serverless.yml.md b/docs/providers/openwhisk/guide/serverless.yml.md index 6ce224581..a399c698c 100644 --- a/docs/providers/openwhisk/guide/serverless.yml.md +++ b/docs/providers/openwhisk/guide/serverless.yml.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/openwhisk/guide/serverless.yml) + # OpenWhisk - serverless.yml Reference @@ -19,7 +21,7 @@ Here is a list of all available properties in `serverless.yml` when the provider service: myService -frameworkVersion: ">=1.0.0 <2.0.0" +frameworkVersion: '>=1.0.0 <2.0.0' provider: name: openwhisk @@ -43,11 +45,11 @@ functions: overwrite: false # Can we overwrite deployed function? namespace: 'custom' # use custom namespace, defaults to '_' annotations: - parameter_name: value + parameter_name: value parameters: parameter_name: value events: # The Events that trigger this Function - # This creates an API Gateway HTTP endpoint which can be used to trigger this function. Learn more in "events/apigateway" + # This creates an API Gateway HTTP endpoint which can be used to trigger this function. Learn more in "events/apigateway" - http: METHOD /path/to/url - trigger: my_trigger # bind function to trigger event - trigger: diff --git a/docs/providers/openwhisk/guide/services.md b/docs/providers/openwhisk/guide/services.md index 901933c1b..21dec67a0 100644 --- a/docs/providers/openwhisk/guide/services.md +++ b/docs/providers/openwhisk/guide/services.md @@ -7,25 +7,27 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/openwhisk/guide/services) + # OpenWhisk - Services -A `service` is like a project. It's where you define your Apache OpenWhisk Functions, the `events` that trigger them and any `resources` they require, all in a file called `serverless.yml`. +A `service` is like a project. It's where you define your Apache OpenWhisk Functions, the `events` that trigger them and any `resources` they require, all in a file called `serverless.yml`. To get started building your first Serverless Framework project, create a `service`. ## Organization -In the beginning of an application, many people use a single Service to define all of the Functions, Events and Resources for that project. This is what we recommend in the beginning. +In the beginning of an application, many people use a single Service to define all of the Functions, Events and Resources for that project. This is what we recommend in the beginning. ```bash myService/ serverless.yml # Contains all functions and infrastructure resources ``` -However, as your application grows, you can break it out into multiple services. A lot of people organize their services by workflows or data models, and group the functions related to those workflows and data models together in the service. +However, as your application grows, you can break it out into multiple services. A lot of people organize their services by workflows or data models, and group the functions related to those workflows and data models together in the service. ```bash users/ @@ -35,11 +37,12 @@ posts/ comments/ serverless.yml # Contains 4 functions that do Comments CRUD operations and the Comments database ``` + This makes sense since related functions usually use common infrastructure resources, and you want to keep those functions and resources together as a single unit of deployment, for better organization and separation of concerns. ## Creation -To create a service, use the `create` command. You must also pass in a runtime (e.g., node.js, python etc.) you would like to write the service in. You can also pass in a path to create a directory and auto-name your service: +To create a service, use the `create` command. You must also pass in a runtime (e.g., node.js, python etc.) you would like to write the service in. You can also pass in a path to create a directory and auto-name your service: ```bash # Create service with nodeJS template in the folder ./myService @@ -48,17 +51,18 @@ serverless create --template openwhisk-nodejs --path myService Here are the available runtimes for Apache OpenWhisk: -* openwhisk-nodejs -* openwhisk-php -* openwhisk-python -* openwhisk-ruby -* openwhisk-swift +- openwhisk-nodejs +- openwhisk-php +- openwhisk-python +- openwhisk-ruby +- openwhisk-swift Check out the [create command docs](../cli-reference/create) for all the details and options. ## Contents You'll see the following files in your working directory: + - `serverless.yml` - `handler.js` @@ -68,11 +72,11 @@ Each `service` configuration is managed in the `serverless.yml` file. The main r - Declare a Serverless service - Define one or more functions in the service - - Define the provider the service will be deployed to (and the runtime if provided) - - Define any custom plugins to be used - - Define events that trigger each function to execute (e.g. HTTP requests) - - Allow events listed in the `events` section to automatically create the resources required for the event upon deployment - - Allow flexible configuration using Serverless Variables + - Define the provider the service will be deployed to (and the runtime if provided) + - Define any custom plugins to be used + - Define events that trigger each function to execute (e.g. HTTP requests) + - Allow events listed in the `events` section to automatically create the resources required for the event upon deployment + - Allow flexible configuration using Serverless Variables You can see the name of the service, the provider configuration and the first function inside the `functions` definition which points to the `handler.js` file. Any further service configuration will be done in this file. @@ -93,7 +97,7 @@ functions: - http: post /users/create usersDelete: # A Function handler: users.delete - events: # The Events that trigger this Function + events: # The Events that trigger this Function - http: delete /users/delete ``` @@ -123,7 +127,7 @@ Then use the `deploy` command: serverless deploy ``` -Check out the [deployment guide](https://serverless.com/framework/docs/providers/openwhisk/guide/deploying/) to learn more about deployments and how they work. Or, check out the [deploy command docs](../cli-reference/deploy) for all the details and options. +Check out the [deployment guide](https://serverless.com/framework/docs/providers/openwhisk/guide/deploying/) to learn more about deployments and how they work. Or, check out the [deploy command docs](../cli-reference/deploy) for all the details and options. ## Removal @@ -181,7 +185,6 @@ provider: … ``` - ## Installing Serverless in an existing service If you already have a Serverless service, and would prefer to lock down the framework version using `package.json`, then you can install Serverless as follows: @@ -196,11 +199,13 @@ npm install serverless --save-dev To execute the locally installed Serverless executable you have to reference the binary out of the node modules directory. Example: + ``` node node_modules/.bin/serverless deploy ``` Or with npx (bundled with npm >= 5.2.0) + ``` npx serverless deploy ``` diff --git a/docs/providers/openwhisk/guide/testing.md b/docs/providers/openwhisk/guide/testing.md index 128cb3316..8a676456c 100644 --- a/docs/providers/openwhisk/guide/testing.md +++ b/docs/providers/openwhisk/guide/testing.md @@ -7,22 +7,24 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/openwhisk/guide/testing) + # OpenWhisk - Testing -While the Serverless Architecture introduces a lot of simplicity when it comes to serving business logic, some of its characteristics present challenges for testing. They are: +While the Serverless Architecture introduces a lot of simplicity when it comes to serving business logic, some of its characteristics present challenges for testing. They are: -* The Serverless Architecture is an integration of separate, distributed services, which must be tested both independently, and together. -* The Serverless Architecture is dependent on internet/cloud services, which are hard to emulate locally. -* The Serverless Architecture can feature event-driven, asynchronous workflows, which are hard to emulate entirely. +- The Serverless Architecture is an integration of separate, distributed services, which must be tested both independently, and together. +- The Serverless Architecture is dependent on internet/cloud services, which are hard to emulate locally. +- The Serverless Architecture can feature event-driven, asynchronous workflows, which are hard to emulate entirely. To get through these challenges, and to keep the [test pyramid](http://martinfowler.com/bliki/TestPyramid.html) in mind, keep the following principles in mind: -* Write your business logic so that it is separate from your FaaS provider (e.g., Apache OpenWhisk), to keep it provider-independent, reusable and more easily testable. -* When your business logic is written separately from the FaaS provider, you can write traditional Unit Tests to ensure it is working properly. -* Write Integration Tests to verify integrations with other services are working correctly. +- Write your business logic so that it is separate from your FaaS provider (e.g., Apache OpenWhisk), to keep it provider-independent, reusable and more easily testable. +- When your business logic is written separately from the FaaS provider, you can write traditional Unit Tests to ensure it is working properly. +- Write Integration Tests to verify integrations with other services are working correctly. ## A Poor Example @@ -32,14 +34,14 @@ Here is an example in Node.js of how to follow the practices above. The job this const db = require('db').connect(); const mailer = require('mailer'); -module.exports.saveUser = (params) => { +module.exports.saveUser = params => { return Promise((resolve, reject) => { const user = { email: params.email, - created_at: Date.now() - } + created_at: Date.now(), + }; - db.saveUser(user, function (err) { + db.saveUser(user, function(err) { if (err) { reject(err); } else { @@ -47,14 +49,14 @@ module.exports.saveUser = (params) => { resolve(); } }); - }) + }); }; ``` There are two main problems with this function: -* The business logic is not separate from the FaaS Provider. It's bounded to how Apache OpenWhisk passes incoming data (`params` object). -* Testing this function will rely on separate services. Specifically, running a database instance and a mail server. +- The business logic is not separate from the FaaS Provider. It's bounded to how Apache OpenWhisk passes incoming data (`params` object). +- Testing this function will rely on separate services. Specifically, running a database instance and a mail server. ## Writing Testable Apache OpenWhisk Functions @@ -71,10 +73,10 @@ class Users { return new Promise((resolve, reject) => { const user = { email: email, - created_at: Date.now() - } + created_at: Date.now(), + }; - this.db.saveUser(user, function (err) { + this.db.saveUser(user, function(err) { if (err) { reject(err); } else { @@ -82,7 +84,7 @@ class Users { resolve(); } }); - }) + }); } } @@ -96,13 +98,13 @@ const Users = require('users'); let users = new Users(db, mailer); -module.exports.saveUser = (params) => { +module.exports.saveUser = params => { return users.save(params.email); }; ``` -Now, the above class keeps business logic separate. Further, the code responsible for setting up dependencies, injecting them, calling business logic functions and interacting with Apache OpenWhisk is in its own file, which will be changed less often. This way, the business logic is not provider dependent, easier to re-use, and easier to test. +Now, the above class keeps business logic separate. Further, the code responsible for setting up dependencies, injecting them, calling business logic functions and interacting with Apache OpenWhisk is in its own file, which will be changed less often. This way, the business logic is not provider dependent, easier to re-use, and easier to test. -Further, this code doesn't require running any external services. Instead of a real `db` and `mailer` services, we can pass mocks and assert if `saveUser` and `sendWelcomeEmail` has been called with proper arguments. +Further, this code doesn't require running any external services. Instead of a real `db` and `mailer` services, we can pass mocks and assert if `saveUser` and `sendWelcomeEmail` has been called with proper arguments. -Unit Tests can easily be written to cover the above class. An integration test can be added by invoking the function (`serverless invoke`) with fixture email address, check if user is actually saved to DB and check if email was received to see if everything is working together. +Unit Tests can easily be written to cover the above class. An integration test can be added by invoking the function (`serverless invoke`) with fixture email address, check if user is actually saved to DB and check if email was received to see if everything is working together. diff --git a/docs/providers/openwhisk/guide/variables.md b/docs/providers/openwhisk/guide/variables.md index 0face2d85..a7615e2c5 100644 --- a/docs/providers/openwhisk/guide/variables.md +++ b/docs/providers/openwhisk/guide/variables.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/openwhisk/guide/variables) + # OpenWhisk - Variables @@ -24,6 +26,7 @@ The Serverless framework provides a powerful variable system which allows you to **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. ## Reference Properties In serverless.yml + To self-reference properties in `serverless.yml`, use the `${self:someProperty}` syntax in your `serverless.yml`. This functionality is recursive, so you can go as deep in the object tree as you want. ```yml @@ -34,18 +37,19 @@ custom: functions: hello: - handler: handler.hello - events: - - schedule: ${self:custom.globalSchedule} + handler: handler.hello + events: + - schedule: ${self:custom.globalSchedule} world: - handler: handler.world - events: - - schedule: ${self:custom.globalSchedule} + handler: handler.world + events: + - schedule: ${self:custom.globalSchedule} ``` In the above example you're setting a global schedule for all functions by referencing the `globalSchedule` property in the same `serverless.yml` file. This way, you can easily change the schedule for all functions whenever you like. ## Referencing Environment Variables + To reference environment variables, use the `${env:SOME_VAR}` syntax in your `serverless.yml` configuration file. ```yml @@ -53,16 +57,17 @@ service: new-service provider: openwhisk functions: hello: - name: ${env:FUNC_PREFIX}-hello - handler: handler.hello + name: ${env:FUNC_PREFIX}-hello + handler: handler.hello world: - name: ${env:FUNC_PREFIX}-world - handler: handler.world + name: ${env:FUNC_PREFIX}-world + handler: handler.world ``` In the above example you're dynamically adding a prefix to the function names by referencing the `FUNC_PREFIX` env var. So you can easily change that prefix for all functions by changing the `FUNC_PREFIX` env var. ## Referencing CLI Options + To reference CLI options that you passed, use the `${opt:some_option}` syntax in your `serverless.yml` configuration file. ```yml @@ -70,16 +75,17 @@ service: new-service provider: openwhisk functions: hello: - name: ${opt:stage}-hello - handler: handler.hello + name: ${opt:stage}-hello + handler: handler.hello world: - name: ${opt:stage}-world - handler: handler.world + name: ${opt:stage}-world + handler: handler.world ``` In the above example, you're dynamically adding a prefix to the function names by referencing the `stage` option that you pass in the CLI when you run `serverless deploy --stage dev`. So when you deploy, the function name will always include the stage you're deploying to. ## Reference Variables in other Files + To reference variables in other YAML or JSON files, use the `${file(./myFile.yml):someProperty}` syntax in your `serverless.yml` configuration file. Here's an example: ```yml @@ -94,16 +100,16 @@ provider: openwhisk custom: ${file(./myCustomFile.yml)} # You can reference the entire file functions: hello: - handler: handler.hello - events: - - schedule: ${file(./myCustomFile.yml):globalSchedule} # Or you can reference a specific property + handler: handler.hello + events: + - schedule: ${file(./myCustomFile.yml):globalSchedule} # Or you can reference a specific property world: - handler: handler.world - events: - - schedule: ${self:custom.globalSchedule} # This would also work in this case + handler: handler.world + events: + - schedule: ${self:custom.globalSchedule} # This would also work in this case ``` -In the above example, you're referencing the entire `myCustomFile.yml` file in the `custom` property. You need to pass the path relative to your service directory. You can also request specific properties in that file as shown in the `schedule` property. It's completely recursive and you can go as deep as you want. Additionally you can request properties that contain arrays from either YAML or JSON reference files. Here's a YAML example for an events array: +In the above example, you're referencing the entire `myCustomFile.yml` file in the `custom` property. You need to pass the path relative to your service directory. You can also request specific properties in that file as shown in the `schedule` property. It's completely recursive and you can go as deep as you want. Additionally you can request properties that contain arrays from either YAML or JSON reference files. Here's a YAML example for an events array: ```yml myevents: @@ -111,15 +117,19 @@ myevents: ``` and for JSON: + ```json { - "myevents": [{ - "schedule" : "cron(0 * * * *)" - }] + "myevents": [ + { + "schedule": "cron(0 * * * *)" + } + ] } ``` In your serverless.yml, depending on the type of your source file, either have the following syntax for YAML + ```yml functions: hello: @@ -128,6 +138,7 @@ functions: ``` or for a JSON reference file use this sytax: + ```yml functions: hello: @@ -146,9 +157,9 @@ References can be either named or unnamed exports. To use the exported `someModu ```js // scheduleConfig.js module.exports.cron = () => { - // Code that generates dynamic data - return 'cron(0 * * * *)'; -} + // Code that generates dynamic data + return 'cron(0 * * * *)'; +}; ``` ```js @@ -156,9 +167,9 @@ module.exports.cron = () => { module.exports = () => { return { property1: 'some value', - property2: 'some other value' - } -} + property2: 'some other value', + }; +}; ``` ```yml @@ -170,12 +181,12 @@ custom: ${file(./config.js)} functions: hello: - handler: handler.hello - events: - - schedule: ${file(./scheduleConfig.js):cron} # Reference a specific module + handler: handler.hello + events: + - schedule: ${file(./scheduleConfig.js):cron} # Reference a specific module ``` -You can also return an object and reference a specific property. Just make sure you are returning a valid object and referencing a valid property: +You can also return an object and reference a specific property. Just make sure you are returning a valid object and referencing a valid property: ```yml # serverless.yml @@ -183,19 +194,19 @@ service: new-service provider: openwhisk functions: scheduledFunction: - handler: handler.scheduledFunction - events: - - schedule: ${file(./myCustomFile.js):schedule.hour} + handler: handler.scheduledFunction + events: + - schedule: ${file(./myCustomFile.js):schedule.hour} ``` ```js // myCustomFile.js module.exports.schedule = () => { - // Code that generates dynamic data - return { - hour: 'cron(0 * * * *)' - }; -} + // Code that generates dynamic data + return { + hour: 'cron(0 * * * *)', + }; +}; ``` ## Multiple Configuration Files @@ -210,6 +221,7 @@ resources: The corresponding resources which are defined inside the `openwhisk-resources.json` file will be resolved and loaded into the `Resources` section. ## Nesting Variable References + The Serverless variable system allows you to nest variable references within each other for ultimate flexibility. So you can reference certain variables based on other variables. Here's an example: ```yml @@ -220,12 +232,13 @@ custom: functions: hello: - handler: handler.hello + handler: handler.hello ``` In the above example, if you pass `dev` as a stage option, the framework will look for the `dev_arn` environment variable. If you pass `production`, the framework will look for `production_arn`, and so on. This allows you to creatively use multiple variables by using a certain naming pattern without having to update the values of these variables constantly. You can go as deep as you want in your nesting, and can reference variables at any level of nesting from any source (env, opt, self or file). ## Overwriting Variables + The Serverless framework gives you an intuitive way to reference multiple variables as a fallback strategy in case one of the variables is missing. This way you'll be able to use a default value from a certain source, if the variable from another source is missing. For example, if you want to reference the stage you're deploying to, but you don't want to keep on providing the `stage` option in the CLI. What you can do in `serverless.yml` is: @@ -240,7 +253,7 @@ custom: functions: hello: - handler: handler.hello + handler: handler.hello ``` What this says is to use the `stage` CLI option if it exists, if not, use the default stage (which lives in `provider.stage`). So during development you can safely deploy with `serverless deploy`, but during production you can do `serverless deploy --stage production` and the stage will be picked up for you without having to make any changes to `serverless.yml`. @@ -248,6 +261,7 @@ What this says is to use the `stage` CLI option if it exists, if not, use the de You can have as many variable references as you want, from any source you want, and each of them can be of different type and different name. ## Migrating serverless.env.yml + Previously we used the `serverless.env.yml` file to track Serverless Variables. It was a completely different system with different concepts. To migrate your variables from `serverless.env.yml`, you'll need to decide where you want to store your variables. **Using a config file:** You can still use `serverless.env.yml`, but the difference now is that you can structure the file however you want, and you'll need to reference each variable/property correctly in `serverless.yml`. For more info, you can check the file reference section above. diff --git a/docs/providers/openwhisk/guide/web-actions.md b/docs/providers/openwhisk/guide/web-actions.md index 79b640b83..c318d71bf 100644 --- a/docs/providers/openwhisk/guide/web-actions.md +++ b/docs/providers/openwhisk/guide/web-actions.md @@ -7,12 +7,14 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/openwhisk/guide/web-actions) + # OpenWhisk - Web Actions -Functions can be turned into ["*web actions*"](http://bit.ly/2xSRbOQ) which return HTTP content without use of an API Gateway. This feature is enabled by setting an annotation (`web-export`) in the configuration file. +Functions can be turned into ["_web actions_"](http://bit.ly/2xSRbOQ) which return HTTP content without use of an API Gateway. This feature is enabled by setting an annotation (`web-export`) in the configuration file. ``` functions: @@ -28,11 +30,11 @@ Functions with this annotation can be invoked through a URL template with the fo https://{APIHOST}/api/v1/web/{USER_NAMESPACE}/{PACKAGE}/{ACTION_NAME}.{TYPE} ``` -- *APIHOST* - platform endpoint e.g. *openwhisk.ng.bluemix.net.* -- *USER_NAMESPACE* - this must be an explicit namespace and cannot use the default namespace (_). -- *PACKAGE* - action package or `default`. -- *ACTION_NAME* - default form `${servicename}-${space}-${name}`. -- *TYPE* - `.json`, `.html`, `.text` or `.http`. +- _APIHOST_ - platform endpoint e.g. _openwhisk.ng.bluemix.net._ +- _USER_NAMESPACE_ - this must be an explicit namespace and cannot use the default namespace (\_). +- _PACKAGE_ - action package or `default`. +- _ACTION_NAME_ - default form `${servicename}-${space}-${name}`. +- _TYPE_ - `.json`, `.html`, `.text` or `.http`. Return values from the function are used to construct the HTTP response. The following parameters are supported. @@ -44,12 +46,11 @@ Here is an example of returning HTML content: ```javascript function main(args) { - var msg = "you didn't tell me who you are." - if (args.name) { - msg = `hello ${args.name}!` - } - return {body: - `

    ${msg}

    `} + var msg = 'you didn't tell me who you are.'; + if (args.name) { + msg = `hello ${args.name}!`; + } + return { body: `

    ${msg}

    ` }; } ``` diff --git a/docs/providers/openwhisk/guide/workflow.md b/docs/providers/openwhisk/guide/workflow.md index a4c9dde79..307c26ea0 100644 --- a/docs/providers/openwhisk/guide/workflow.md +++ b/docs/providers/openwhisk/guide/workflow.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/openwhisk/guide/workflow) + # OpenWhisk - Workflow @@ -24,45 +26,59 @@ Intro. Quick recommendations and tips for various processes. 6. Write tests to run locally. ### Larger Projects -* Break your application/project into multiple Serverless Services. -* Model your Serverless Services around Data Models or Workflows. -* Keep the Functions and Resources in your Serverless Services to a minimum. + +- Break your application/project into multiple Serverless Services. +- Model your Serverless Services around Data Models or Workflows. +- Keep the Functions and Resources in your Serverless Services to a minimum. ## Cheat Sheet + A handy list of commands to use when developing with the Serverless Framework. ##### Create A Service: + Creates a new Service + ``` serverless create -p [SERVICE NAME] -t openwhisk-nodejs ``` ##### Install A Service + This is a convenience method to install a pre-made Serverless Service locally by downloading the Github repo and unzipping it. + ``` serverless install -u [GITHUB URL OF SERVICE] ``` ##### Deploy All + Use this when you have made changes to your Functions, Events or Resources in `serverless.yml` or you simply want to deploy all changes within your Service at the same time. + ``` serverless deploy ``` ##### Deploy Function + Use this to quickly overwrite your OpenWhisk Actions, allowing you to develop faster. + ``` serverless deploy function -f [FUNCTION NAME] ``` ##### Invoke Function + Invokes an OpenWhisk Action and returns logs. + ``` serverless invoke function -f [FUNCTION NAME] -l ``` ##### Streaming Logs + Open up a separate tab in your console and stream all logs for a specific Function using this command. + ``` serverless logs -f [FUNCTION NAME] ``` diff --git a/docs/providers/spotinst/README.md b/docs/providers/spotinst/README.md index 945f713c5..2774f3965 100755 --- a/docs/providers/spotinst/README.md +++ b/docs/providers/spotinst/README.md @@ -5,7 +5,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/) + # Spotinst Provider Documentation diff --git a/docs/providers/spotinst/cli-reference/README.md b/docs/providers/spotinst/cli-reference/README.md index 340391623..e704ea7ae 100755 --- a/docs/providers/spotinst/cli-reference/README.md +++ b/docs/providers/spotinst/cli-reference/README.md @@ -6,7 +6,9 @@ menuOrder: 2 --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/spotinst/cli-reference/) + # Serverless Spotinst Functions CLI Reference diff --git a/docs/providers/spotinst/cli-reference/config-credentials.md b/docs/providers/spotinst/cli-reference/config-credentials.md index 2876d5e46..ae464e075 100755 --- a/docs/providers/spotinst/cli-reference/config-credentials.md +++ b/docs/providers/spotinst/cli-reference/config-credentials.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/spotinst/cli-reference/config-credentials) + # Spotinst Functions - Config Credentials @@ -26,4 +28,4 @@ serverless config credentials -p spotinst -a act-92879641 -t eyJ0eXAiOiJKV1QiLCJ - `--provider` or `-p` The provider name. - `--account` or `-a` Spotinst Account ID. -- `--token` or `-t` Spotinst Access Token. \ No newline at end of file +- `--token` or `-t` Spotinst Access Token. diff --git a/docs/providers/spotinst/cli-reference/create.md b/docs/providers/spotinst/cli-reference/create.md index fa4421d7e..64a86b00e 100755 --- a/docs/providers/spotinst/cli-reference/create.md +++ b/docs/providers/spotinst/cli-reference/create.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/spotinst/cli-reference/create) + # Spotinst Functions - Create @@ -27,6 +29,7 @@ serverless create -t spotinst-nodejs -p myService ``` ## Options + - `--template` or `-t` The name of one of the available templates. **Required if --template-url and --template-path are not present**. - `--template-url` or `-u` The name of one of the available templates. **Required if --template and --template-path are not present**. - `--template-path` The local path of your template. **Required if --template and --template-url are not present**. @@ -61,7 +64,6 @@ serverless create -t spotinst-nodejs -n my-special-service This example will generate scaffolding for a service with `Spotinst` as a provider and `nodejs` as runtime. The scaffolding will be generated in the current working directory. - ### Creating a named service in a (new) directory ```bash diff --git a/docs/providers/spotinst/cli-reference/deploy-function.md b/docs/providers/spotinst/cli-reference/deploy-function.md index 97c60bad6..6430bbb83 100644 --- a/docs/providers/spotinst/cli-reference/deploy-function.md +++ b/docs/providers/spotinst/cli-reference/deploy-function.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/spotinst/cli-reference/deploy-function) + # Spotinst Functions - Deploy Function @@ -16,8 +18,8 @@ The `sls deploy` function command deploys an individual function. This command s ## Note: Please update your Environment ID before deploying a function - 1. [Create an Environment](https://console.spotinst.com/functions) - 2. Update your `serverless.yml` file with the Environment ID +1. [Create an Environment](https://console.spotinst.com/functions) +2. Update your `serverless.yml` file with the Environment ID ```yml service: myService @@ -25,7 +27,7 @@ service: myService provider: name: spotinst spotinst: - environment: env-8f451a5f # NOTE: Remember to add the environment ID + environment: env-8f451a5f # NOTE: Remember to add the environment ID functions: hello: @@ -56,8 +58,8 @@ serverless deploy function -f functionName **Note:** Because this command is only deploying the function code, function properties such as environment variables and events will **not** be deployed. - ## Options + - `--function` or `-f` The name of the function which should be deployed -*more options to come soon* +_more options to come soon_ diff --git a/docs/providers/spotinst/cli-reference/deploy.md b/docs/providers/spotinst/cli-reference/deploy.md index b5b909220..8dc0c561d 100755 --- a/docs/providers/spotinst/cli-reference/deploy.md +++ b/docs/providers/spotinst/cli-reference/deploy.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/spotinst/cli-reference/deploy) + # Spotinst Functions - deploy @@ -19,5 +21,7 @@ serverless deploy -v ``` ## Options + +- `--config` or `-c` Path to your conifguration file, if other than `serverless.yml|.yaml|.js|.json`. - `--package` or `-p` path to a pre-packaged directory and skip packaging step. - `--verbose` or `-v` Shows all stack events during deployment, and display any Stack Output. diff --git a/docs/providers/spotinst/cli-reference/info.md b/docs/providers/spotinst/cli-reference/info.md index 0f2fa8ade..807a37949 100755 --- a/docs/providers/spotinst/cli-reference/info.md +++ b/docs/providers/spotinst/cli-reference/info.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/spotinst/cli-reference/info) + # Spotinst Functions - Info @@ -19,5 +21,6 @@ serverless info ``` ## Options + - `--function` or `-f` The name of the function which should be fetched. - `--verbose` or `-v` Shows displays any Stack Output. diff --git a/docs/providers/spotinst/cli-reference/invoke.md b/docs/providers/spotinst/cli-reference/invoke.md index 835dbe025..67cbaf246 100755 --- a/docs/providers/spotinst/cli-reference/invoke.md +++ b/docs/providers/spotinst/cli-reference/invoke.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/spotinst/cli-reference/invoke) + # Spotinst Functions - Invoke diff --git a/docs/providers/spotinst/cli-reference/logs.md b/docs/providers/spotinst/cli-reference/logs.md index 4ec585317..203f4f581 100755 --- a/docs/providers/spotinst/cli-reference/logs.md +++ b/docs/providers/spotinst/cli-reference/logs.md @@ -7,12 +7,14 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/spotinst/cli-reference/logs) + # Spotinst Functions - Logs -Lets you view the logs for of the specified function. +Lets you view the logs for of the specified function. ```bash serverless logs -f hello @@ -20,8 +22,8 @@ serverless logs -f hello ## Options - - `-f` the name of the function that you want to fetch the logs for **Required** - - `--startTime` a unit of time that you want to start searching the logs from. Here is a list of the supported string formats +- `-f` the name of the function that you want to fetch the logs for **Required** +- `--startTime` a unit of time that you want to start searching the logs from. Here is a list of the supported string formats ```bash 30m # since 30 mins ago @@ -37,4 +39,4 @@ serverless logs -f hello serverless logs -f hello --startTime 3h ``` -This will fetch your logs started from 3 hours ago until the current time \ No newline at end of file +This will fetch your logs started from 3 hours ago until the current time diff --git a/docs/providers/spotinst/cli-reference/plugin-install.md b/docs/providers/spotinst/cli-reference/plugin-install.md index 4da71ffdb..65356156b 100644 --- a/docs/providers/spotinst/cli-reference/plugin-install.md +++ b/docs/providers/spotinst/cli-reference/plugin-install.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/spotinst/cli-reference/plugin-install) + # Plugin Install @@ -22,9 +24,11 @@ serverless plugin install --name pluginName ``` ## Options + - `--name` or `-n` The plugins name. **Required**. ## Provided lifecycle events + - `plugin:install:install` ## Examples diff --git a/docs/providers/spotinst/cli-reference/plugin-list.md b/docs/providers/spotinst/cli-reference/plugin-list.md index d706a719f..c686365f4 100644 --- a/docs/providers/spotinst/cli-reference/plugin-list.md +++ b/docs/providers/spotinst/cli-reference/plugin-list.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/spotinst/cli-reference/plugin-list) + # Plugin List @@ -19,7 +21,9 @@ serverless plugin list ``` ## Options -- *None* + +- _None_ ## Provided lifecycle events + - `plugin:list:list` diff --git a/docs/providers/spotinst/cli-reference/plugin-search.md b/docs/providers/spotinst/cli-reference/plugin-search.md index c78e9d53d..f2b8de47f 100644 --- a/docs/providers/spotinst/cli-reference/plugin-search.md +++ b/docs/providers/spotinst/cli-reference/plugin-search.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/spotinst/cli-reference/plugin-search) + # Plugin Search @@ -19,9 +21,11 @@ serverless plugin search --query query ``` ## Options + - `--query` or `-q` The query you want to use for your search. **Required**. ## Provided lifecycle events + - `plugin:search:search` ## Examples diff --git a/docs/providers/spotinst/cli-reference/plugin-uninstall.md b/docs/providers/spotinst/cli-reference/plugin-uninstall.md index d04239f91..f1f686ac1 100644 --- a/docs/providers/spotinst/cli-reference/plugin-uninstall.md +++ b/docs/providers/spotinst/cli-reference/plugin-uninstall.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/spotinst/cli-reference/plugin-uninstall) + # Plugin Uninstall @@ -19,9 +21,11 @@ serverless plugin uninstall --name pluginName ``` ## Options + - `--name` or `-n` The plugins name. **Required**. ## Provided lifecycle events + - `plugin:uninstall:uninstall` ## Examples diff --git a/docs/providers/spotinst/cli-reference/remove.md b/docs/providers/spotinst/cli-reference/remove.md index 87d2ce0d1..525c5d229 100755 --- a/docs/providers/spotinst/cli-reference/remove.md +++ b/docs/providers/spotinst/cli-reference/remove.md @@ -7,16 +7,19 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/spotinst/cli-reference/remove) + # Spotinst Functions - Remove -The `sls remove` command will remove the deployed service, defined in your current working directory, from the provider. +The `sls remove` command will remove the deployed service, defined in your current working directory, from the provider. ```bash serverless remove ``` ## Options + - `--verbose` or `-v` Shows all stack events during deployment. diff --git a/docs/providers/spotinst/cli-reference/stage.md b/docs/providers/spotinst/cli-reference/stage.md index 479708f51..9098828c0 100644 --- a/docs/providers/spotinst/cli-reference/stage.md +++ b/docs/providers/spotinst/cli-reference/stage.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/spotinst/guide/credentials) + # Spotinst Functions - Stage Variables @@ -15,28 +17,31 @@ layout: Doc Serverless allows you to specify different stages to deploy your project to. Changing the stage will change the environment your function is running on, which is helpful when you wish to keep production code partitioned from your development environment. Your function's stage is set to 'dev' by default. You can update the stage when deploying the function, either from the command line using the serverless framework, or by modifying the serverless.yml in your project. When utilizing this feature, remember to include a config file that holds the environment IDs associated with your stages. An example config.json would look something like this: + ```json { - "dev": "env-abcd1234", - "prod": "env-defg5678" + "dev": "env-abcd1234", + "prod": "env-defg5678" } ``` ## Through Serverless Framework + To change the stage through the serverless framework you simply need to enter the command ```bash serverless deploy --stage #{Your Stage Name} ``` + You will also need to update the environment parameter to point to the config.json: + ```yaml  spotinst: - environment: ${file(./config.json):${opt:stage, self:provider.stage, 'dev'}} + environment: ${file(./config.json):${opt:stage, self:provider.stage, 'dev'}} ``` + Note that while I am using 'dev' as the default stage, you may change this parameter to a custom default stage. - - ## Through the .yml File To change the stage in the serverless.yml file you need to add the following into the provider tag then deploy your function as usual @@ -48,5 +53,5 @@ provider: spotinst: environment: #{Your Environment ID} ``` -Be sure to also modify your environment ID when you change the stage if you are not working with a config file. +Be sure to also modify your environment ID when you change the stage if you are not working with a config file. diff --git a/docs/providers/spotinst/events/README.md b/docs/providers/spotinst/events/README.md index 82efeac47..a34f9a665 100755 --- a/docs/providers/spotinst/events/README.md +++ b/docs/providers/spotinst/events/README.md @@ -6,7 +6,9 @@ menuOrder: 3 --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/spotinst/events/) + # Serverless Spotinst Functions Events diff --git a/docs/providers/spotinst/events/http.md b/docs/providers/spotinst/events/http.md index 4ac098b08..7691f2638 100755 --- a/docs/providers/spotinst/events/http.md +++ b/docs/providers/spotinst/events/http.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/spotinst/events/http) + # HTTP @@ -16,4 +18,4 @@ Spotinst Functions are automatically given an HTTP endpoint when they are create `https://{app id}{environment id}.spotinst.io/{function id}` -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 +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) diff --git a/docs/providers/spotinst/events/schedule.md b/docs/providers/spotinst/events/schedule.md index bf8cf48b8..b3c4e5c4d 100755 --- a/docs/providers/spotinst/events/schedule.md +++ b/docs/providers/spotinst/events/schedule.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/spotinst/events/schedule) + # Schedule @@ -22,12 +24,11 @@ The following example is a function configuration in the serverless.yml file tha functions: crawl: handler: handler.crawl - cron: # Setup scheduled trigger with cron expression + 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 @@ -40,10 +41,9 @@ This example will create and attach a schedule event for the function `crawl` wh functions: crawl: handler: handler.crawl - cron: # Setup scheduled trigger with cron expression + 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 index 0284afe3a..9e02775ff 100644 --- a/docs/providers/spotinst/examples/README.md +++ b/docs/providers/spotinst/examples/README.md @@ -7,14 +7,16 @@ menuOrder: 4 --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/examples/hello-world/) + -# Hello World Serverless Example +# Hello World Serverless Example Pick your language of choice: -* [JavaScript](./node) -* [Python](./python) -* [Ruby](./ruby) -* [Java8](./java8) +- [JavaScript](./node) +- [Python](./python) +- [Ruby](./ruby) +- [Java8](./java8) diff --git a/docs/providers/spotinst/examples/java8/README.md b/docs/providers/spotinst/examples/java8/README.md index dd9a422d3..dee01e635 100644 --- a/docs/providers/spotinst/examples/java8/README.md +++ b/docs/providers/spotinst/examples/java8/README.md @@ -6,25 +6,30 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/spotinst/) + # Hello World Java8 Example -Make sure `serverless` is installed. +Make sure `serverless` is installed. ## 1. Create a service -`serverless create --template spotinst-java8 --path serviceName` `serviceName` is going to be a new directory there the Java8 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. You will need to go into the serverless.yml file and add in the environment variable that you want to deploy into. Also you need to copy the Service name in the serverless.yml file and paste it into the pom.xlm file under the finalName tag. Next you will have to package the project to create a .jar file. To do this run the command `mvn package`. + +`serverless create --template spotinst-java8 --path serviceName` `serviceName` is going to be a new directory there the Java8 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. You will need to go into the serverless.yml file and add in the environment variable that you want to deploy into. Also you need to copy the Service name in the serverless.yml file and paste it into the pom.xlm file under the finalName tag. Next you will have to package the project to create a .jar file. To do this run the command `mvn package`. ## 2. Deploy -```bash + +```bash serverless deploy -``` +``` ## 3. Invoke deployed function + ```bash serverless invoke --function hello -``` +``` In your terminal window you should see the response @@ -36,7 +41,7 @@ Congrats you have deployed and ran your Hello World function! ## Short Hand Guide -`sls` is short hand for serverless cli commands +`sls` is short hand for serverless cli commands `-f` is short hand for `--function` diff --git a/docs/providers/spotinst/examples/node/README.md b/docs/providers/spotinst/examples/node/README.md index 2f4c72ea9..1653d2f1b 100644 --- a/docs/providers/spotinst/examples/node/README.md +++ b/docs/providers/spotinst/examples/node/README.md @@ -6,25 +6,30 @@ layout: Doc --> + ### [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. +Make sure `serverless` is installed. ## 1. Create a service -`serverless create --template spotinst-nodejs --path serviceName` `serviceName` is going to be a new directory where the JavaScript 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. You will need to go into the serverless.yml file and add in the environment variable that you want to deploy into. + +`serverless create --template spotinst-nodejs --path serviceName` `serviceName` is going to be a new directory where the JavaScript 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. You will need to go into the serverless.yml file and add in the environment variable that you want to deploy into. ## 2. Deploy -```bash + +```bash serverless deploy -``` +``` ## 3. Invoke deployed function + ```bash serverless invoke --function hello -``` +``` In your terminal window you should see the response @@ -36,7 +41,7 @@ Congrats you have deployed and ran your Hello World function! ## Short Hand Guide -`sls` is short hand for serverless cli commands +`sls` is short hand for serverless cli commands `-f` is short hand for `--function` diff --git a/docs/providers/spotinst/examples/python/README.md b/docs/providers/spotinst/examples/python/README.md index f744ca739..b2777fc9d 100644 --- a/docs/providers/spotinst/examples/python/README.md +++ b/docs/providers/spotinst/examples/python/README.md @@ -6,27 +6,30 @@ layout: Doc --> + ### [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. +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. You will need to go into the serverless.yml file and add in the environment variable that you want to deploy into. +`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. You will need to go into the serverless.yml file and add in the environment variable that you want to deploy into. ## 2. Deploy -```bash + +```bash serverless deploy -``` +``` ## 3. Invoke deployed function + ```bash serverless invoke --function hello -``` - +``` In your terminal window you should see the response @@ -38,7 +41,7 @@ Congrats you have deployed and ran your Hello World function! ## Short Hand Guide -`sls` is short hand for serverless cli commands +`sls` is short hand for serverless cli commands `-f` is short hand for `--function` diff --git a/docs/providers/spotinst/examples/ruby/README.md b/docs/providers/spotinst/examples/ruby/README.md index a4d236bfb..87436ee91 100644 --- a/docs/providers/spotinst/examples/ruby/README.md +++ b/docs/providers/spotinst/examples/ruby/README.md @@ -6,7 +6,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/spotinst/) + # Hello World Ruby Example @@ -14,15 +16,17 @@ layout: Doc 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 Ruby 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. You will need to go into the serverless.yml file and add in the environment variable that you want to deploy into. +`serverless create --template spotinst-ruby --path serviceName` `serviceName` is going to be a new directory there the Ruby 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. You will need to go into the serverless.yml file and add in the environment variable that you want to deploy into. ## 2. Deploy + ```bash serverless deploy -``` +``` ## 3. Invoke deployed function + ```bash serverless invoke --function hello ``` diff --git a/docs/providers/spotinst/guide/IAM-roles.md b/docs/providers/spotinst/guide/IAM-roles.md index ba2ec0516..6f139db98 100644 --- a/docs/providers/spotinst/guide/IAM-roles.md +++ b/docs/providers/spotinst/guide/IAM-roles.md @@ -2,24 +2,29 @@ title: Serverless Framework - Spotinst Functions Guide - IAM Role Configuration menuText: IAM Role menuOrder: 11 -description: How to configure IAM roles for AWS services +description: How to configure IAM roles for AWS services layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/spotinst/guide/iam-roles) + # Spotinst Functions - IAM roles + Functions sometimes rely on outside services from Amazon such as S3, and accessing these resources often requires authorization using IAM. Spotinst Functions can be configured with the relevant permissions with the inclusion of IAM role information in the serverless.yml file. See [Amazon's documentation][amazon-docs-url] for more information on IAM roles. ## Requirements + - You will need to create an IAM role on your AWS account and attach policies with the relevant permissions. - A spotinst role will be generated and linked with your AWS role - Only one Spotinst role per function. - Multiple functions can be associated with the same Spotinst role. -## YML +## YML + ```yaml functions: example: @@ -29,15 +34,14 @@ functions: timeout: 30 access: private iamRoleConfig: - roleId: ${role-id} + roleId: ${role-id} ``` ## Parameters + - roleId: the role created on the console - ex : sfr-5ea76784 - - For more information on how to set up IAM roles, check out our documentation [here][spotinst-help-center] [amazon-docs-url]: https://aws.amazon.com/iam/?sc_channel=PS&sc_campaign=acquisition_US&sc_publisher=google&sc_medium=iam_b&sc_content=amazon_iam_e&sc_detail=amazon%20iam&sc_category=iam&sc_segment=208382128687&sc_matchtype=e&sc_country=US&s_kwcid=AL!4422!3!208382128687!e!!g!!amazon%20iam&ef_id=WoypCQAABVVgCzd0:20180220230233:s diff --git a/docs/providers/spotinst/guide/README.md b/docs/providers/spotinst/guide/README.md index 1b42239e4..f163289f7 100755 --- a/docs/providers/spotinst/guide/README.md +++ b/docs/providers/spotinst/guide/README.md @@ -6,7 +6,9 @@ menuOrder: 1 --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/spotinst/guide/) + # Serverless Spotinst Functions Guide diff --git a/docs/providers/spotinst/guide/active-versions.md b/docs/providers/spotinst/guide/active-versions.md index 29dbb01ae..897d2ec40 100644 --- a/docs/providers/spotinst/guide/active-versions.md +++ b/docs/providers/spotinst/guide/active-versions.md @@ -7,19 +7,23 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/spotinst/guide/active-versions) + # Spotinst Functions - Active Versions -Every time you update your function, a new version is being created by default. Version numbers have a unique ID that starts at 0 and increments by one each update. Each function version is immutable and cannot be changed. +Every time you update your function, a new version is being created by default. Version numbers have a unique ID that starts at 0 and increments by one each update. Each function version is immutable and cannot be changed. ## Latest Version -The 'Latest' version refers to the most recent version created by the last update. Unless otherwise specified, all incoming traffic is routed to the latest version. -*Please note: the 'Latest' tag will point to a different version number each and every time you update your function.* +The 'Latest' version refers to the most recent version created by the last update. Unless otherwise specified, all incoming traffic is routed to the latest version. + +_Please note: the 'Latest' tag will point to a different version number each and every time you update your function._ Default configuration for activeVersions when a new function is created: + ```yaml activeVersions: - version: $LATEST @@ -27,11 +31,13 @@ activeVersions: ``` ## Active Version + The 'Active' version can point to more than one version of your function, including 'Latest'. This allows you to distribute your incoming traffic between multiple versions and dictate what percentage is sent to each version. For example, say you wanted to test a new version of your function to determine if it was production-ready. You could specify that 10% of the traffic be routed to that new version, and route the remaining 90% to the stable version. You can gradually route more traffic to the new version as you become more confident in its performance. ### Examples + ```yaml activeVersions: - version: $LATEST @@ -47,6 +53,7 @@ activeVersions: - version: 2 percentage: 20.0 ``` + 80% of traffic goes to the most recently published update, and 20% goes to version 2. ```yaml @@ -58,16 +65,21 @@ activeVersions: - version: 1 percentage: 25.0 ``` + Traffic is split between versions 1. 3, and 5. Changes made to your latest version will not affect production traffic. ### Configure Active Version + You can configure active versions in the serverless.yml file, but you can also use the Spotinst Console to configure the versions without deploying a new update. In the event you would like to change your 'Active' version configuration without updating your function, you can use the 'Configure Active Version' action from the console and the API + - Console: + 1. Navigate to your function 2. Click 'Actions' 3. Select 'Configure Active Version' - + - API: (update function) + ```yaml activeVersions: - version: $LATEST @@ -77,6 +89,7 @@ activeVersions: ``` ### Requirements + - The sum of all percentages must be 100% - You can set up to two decimal digits in the percentage - Changes made to the ratio using the Spotinst Console will be overwritten by the contents of activeVersions in your serverless.yml file. diff --git a/docs/providers/spotinst/guide/cors.md b/docs/providers/spotinst/guide/cors.md index 1d886d9c1..bfad0215b 100644 --- a/docs/providers/spotinst/guide/cors.md +++ b/docs/providers/spotinst/guide/cors.md @@ -7,36 +7,37 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/spotinst/guide/cors) + # Spotinst Functions - CORS -Cross-Origin Resource Sharing is a mechanism that allows restricted resources on a web page to be requested from a domain outside of the original. CORS defines a way in which a web service and server can interact to determine whether or not it is safe to allow a cross-origin request. Enabling CORS for your function allows you to specify safe domains, and enables out-of-the-box support for preflight HTTP requests (via the OPTIONS method) that will return the needed ‘access-control-*’ headers specified below. The actual HTTP request will return the ‘access-control-allow-origin’ method. + +Cross-Origin Resource Sharing is a mechanism that allows restricted resources on a web page to be requested from a domain outside of the original. CORS defines a way in which a web service and server can interact to determine whether or not it is safe to allow a cross-origin request. Enabling CORS for your function allows you to specify safe domains, and enables out-of-the-box support for preflight HTTP requests (via the OPTIONS method) that will return the needed ‘access-control-\*’ headers specified below. The actual HTTP request will return the ‘access-control-allow-origin’ method. You can enable CORS for cross-domain HTTP requests with Spotinst Functions. Add the required fields to you serverless.yml file. ### Example CORS object: + ```yml - cors: - - enabled: true - - origin: "http://foo.example" - - headers: "Content-Type,X-PINGOTHER" - - methods: "PUT,POST" +cors: + - enabled: true + - origin: 'http://foo.example' + - headers: 'Content-Type,X-PINGOTHER' + - methods: 'PUT,POST' ``` ### Parameters: - - enabled: Boolean - - Specify if CORS is enabled for the function. - - default: false - - origin: String - - Specifies a domain/origin that may access the resource. A wildcard '*' may be used to allow any origin to access the resource. - - default: '*' - - methods: String - - Comma-separated list of HTTP methods that are allowed to access the resource. This is used in response to a preflight request. - - default: 'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT' - - headers: String - - Comma-separated list of allowed headers. - - default: 'Content-Type,Authorization' - - - +- enabled: Boolean + - Specify if CORS is enabled for the function. + - default: false +- origin: String + - Specifies a domain/origin that may access the resource. A wildcard '\*' may be used to allow any origin to access the resource. + - default: '\*' +- methods: String + - Comma-separated list of HTTP methods that are allowed to access the resource. This is used in response to a preflight request. + - default: 'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT' +- headers: String + - Comma-separated list of allowed headers. + - default: 'Content-Type,Authorization' diff --git a/docs/providers/spotinst/guide/create-token.md b/docs/providers/spotinst/guide/create-token.md index 38bdd8c33..e23276bd6 100644 --- a/docs/providers/spotinst/guide/create-token.md +++ b/docs/providers/spotinst/guide/create-token.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/spotinst/guide/create-token) + # Spotinst Functions - Create Token @@ -21,6 +23,7 @@ You can generate a Permanent Token from the [Spotinst Console](https://console.s > `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: @@ -30,12 +33,14 @@ $ curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'username ``` 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": { @@ -62,11 +67,9 @@ The request will return two tokens: } ``` -* *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: - +- _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 b21f18c9b..43a5f851d 100644 --- a/docs/providers/spotinst/guide/credentials.md +++ b/docs/providers/spotinst/guide/credentials.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/spotinst/guide/credentials) + # Spotinst Functions - Credentials @@ -16,7 +18,7 @@ The Serverless Framework needs access to your Spotinst account so that it can cr ## Configure Credentials -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). +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). 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: @@ -31,7 +33,7 @@ cd new-function npm install ``` -Once this has completed you will be able to configure your credentials by running +Once this has completed you will be able to configure your credentials by running ```bash serverless config credentials -p spotinst -a {your account id} -t {your token} @@ -42,7 +44,7 @@ This will create a ~/.spotinst/credentials file the file should look like this w ```bash default: token: {your token} - account: {your account number} + account: {your account number} ``` 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/endpoints.md b/docs/providers/spotinst/guide/endpoints.md index 004a8077e..649a55c4f 100644 --- a/docs/providers/spotinst/guide/endpoints.md +++ b/docs/providers/spotinst/guide/endpoints.md @@ -2,31 +2,32 @@ title: Serverless Framework - Spotinst Functions Guide - Endpoint Setup menuText: Endpoint Set Up menuOrder: 6 -description: How to set up an Endpoint +description: How to set up an Endpoint layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/spotinst/guide/credentials) + # Spotinst Functions - Endpoints -You are able to set your custom endpoint path in the serverless.yml file if you do not want to use the console or API. You will have to set up your environment Alias in the console but here you can set the path and method for your individual functions to be mapped to. +You are able to set your custom endpoint path in the serverless.yml file if you do not want to use the console or API. You will have to set up your environment Alias in the console but here you can set the path and method for your individual functions to be mapped to. Here is a sample function from a yml file. As you can see at the bottom of the file we have listed an endpoint with a path and method. Both of these will need to be set in order to be deployed properly ```yml - hello: - runtime: nodejs8.3 - handler: handler.main - memory: 128 - timeout: 30 - access: public - endpoint: - path: /home - method: get - +hello: + runtime: nodejs8.3 + handler: handler.main + memory: 128 + timeout: 30 + access: public + endpoint: + path: /home + method: get ``` -For more information on how to set up endpoint alias and patterns check out our documentation [here](https://help.spotinst.com/hc/en-us/articles/115005893709) \ No newline at end of file +For more information on how to set up endpoint alias and patterns check out our documentation [here](https://help.spotinst.com/hc/en-us/articles/115005893709) diff --git a/docs/providers/spotinst/guide/intro.md b/docs/providers/spotinst/guide/intro.md index c5b13e15f..3eac3d1b0 100644 --- a/docs/providers/spotinst/guide/intro.md +++ b/docs/providers/spotinst/guide/intro.md @@ -7,7 +7,9 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/spotinst/guide/intro) + # Spotinst - Introduction @@ -21,10 +23,9 @@ Using the Serverless Framework you can develop and deploy your Spotinst Function ## Core Benefits of `Spotinst Functions` - 1. Multi-Cloud Deployments
`(50+ Locations!)` - 2. 50-80% Cost Reduction `(via Cloud Spot Prices)` - 3. Faster Execution time & Advanced Analytics - +1. Multi-Cloud Deployments
`(50+ Locations!)` +2. 50-80% Cost Reduction `(via Cloud Spot Prices)` +3. Faster Execution time & Advanced Analytics ## Core Concepts @@ -41,22 +42,21 @@ The functions underneath the environment share similar characteristics and are m For example, you can create several environments for your application's Dev and Prod functions of the same Application -![](https://s3.amazonaws.com/spotinst-public/assets/IMG/sQ7iaNHCTXnhxSe_S4LlQpQ+(1).png) +![]() ### Functions A Function is an [Spotinst Function](https://help.spotinst.com/hc/en-us/articles/115004143245-Function). 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 specific job. -You can perform multiple jobs in your code, but we don't recommend doing that without good reason. Separation of concerns is best and the Framework is designed to help you easily develop and deploy Functions, as well as manage lots of them. - +You can perform multiple jobs in your code, but we don't recommend doing that without good reason. Separation of concerns is best and the Framework is designed to help you easily develop and deploy Functions, as well as manage lots of them. ### Events -Anything that triggers an [Spotinst Function](https://help.spotinst.com/hc/en-us/articles/115004143245-Function) to execute is regarded by the Framework as an **Event** (in Spotinst Functions they called Triggers). Events platform events such as: +Anything that triggers an [Spotinst Function](https://help.spotinst.com/hc/en-us/articles/115004143245-Function) to execute is regarded by the Framework as an **Event** (in Spotinst Functions they called Triggers). Events platform events such as: -* *An HTTP Trigger (e.g., for a REST API)* -* *A scheduled Cron event (e.g., run every 5 minutes)* -* *And more...* +- _An HTTP Trigger (e.g., for a REST API)_ +- _A scheduled Cron event (e.g., run every 5 minutes)_ +- _And more..._ ### Function Template @@ -64,7 +64,7 @@ Anything that triggers an [Spotinst Function](https://help.spotinst.com/hc/en-us # handler.js module.exports.main = function main (event, context, callback) { callback(null, { - statusCode: 200, + statusCode: 200, body: '{"hello":"from NodeJS8.3 function"}', headers: {"Content-Type": "application/json"} }); @@ -73,10 +73,9 @@ module.exports.main = function main (event, context, callback) { ### Services -A **Service** is the Framework's unit of organization. You can think of it as a project file, though you can have multiple services for a single application. It's where you define your Functions, the Events that trigger them, and the Resources your Functions use, all in one file entitled `serverless.yml` (or `serverless.json` or `serverless.js`). It looks like this: +A **Service** is the Framework's unit of organization. You can think of it as a project file, though you can have multiple services for a single application. It's where you define your Functions, the Events that trigger them, and the Resources your Functions use, all in one file by default entitled `serverless.yml` (or `serverless.json` or `serverless.js`). It looks like this: ```yml - service: spotinst-nodejs # NOTE: update this with your service name provider: @@ -96,17 +95,15 @@ functions: # value: '* * * * *' # environmentVariables: # key: value - ``` -When you deploy with the Framework by running `serverless deploy`, everything in `serverless.yml` is deployed at once. + +When you deploy with the Framework by running `serverless deploy`, everything in `serverless.yml` (or the file specified with the `--config` option) is deployed at once. ### Plugins -You can overwrite or extend the functionality of the Framework using **Plugins**. Every `serverless.yml` can contain a `plugins:` property, which features multiple plugins. Please include the **serverless-spotinst-functions** as part of your plugins in the serverless.yml file. +You can overwrite or extend the functionality of the Framework using **Plugins**. Every `serverless.yml` can contain a `plugins:` property, which features multiple plugins. Please include the **serverless-spotinst-functions** as part of your plugins in the serverless.yml file. ```yml - plugins: - serverless-spotinst-functions - ``` diff --git a/docs/providers/spotinst/guide/quick-start.md b/docs/providers/spotinst/guide/quick-start.md index a23cb539e..044239058 100644 --- a/docs/providers/spotinst/guide/quick-start.md +++ b/docs/providers/spotinst/guide/quick-start.md @@ -7,15 +7,18 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://serverless.com/framework/docs/providers/spotinst/guide/quick-start/) + # Spotinst - Quick Start -Here is a quick guide on how to create a new serverless project using the Spotinst NodeJS template. For more detailed instruction please check out the other reference material provided. +Here is a quick guide on how to create a new serverless project using the Spotinst NodeJS template. For more detailed instruction please check out the other reference material provided. ## Install Serverless Framework - First you will need to have the serverless framework installed. To do this you will have to run the command: + +First you will need to have the serverless framework installed. To do this you will have to run the command: ```bash npm install -g serverless @@ -23,41 +26,43 @@ npm install -g serverless ``` ## Set Up Credentials - To set up your Spotinst Credentials you will have to have the Spotinst Serverless Plugin installed inside a new Serverless project. The first thing you will need to do is create a new template project and enter the new project directory by entering into the terminal: + +To set up your Spotinst Credentials you will have to have the Spotinst Serverless Plugin installed inside a new Serverless project. The first thing you will need to do is create a new template project and enter the new project directory by entering into the terminal: ```bash serverless create --template spotinst-nodejs --path new-service cd new-service ``` - Once you are in the project directory you will have to install the plugin but entering: +Once you are in the project directory you will have to install the plugin but entering: ```bash npm install ``` - After the installation is completed you can then configure your credentials. Before you do this you will want to have your Spotinst account ID number and Spotinst API token ready to go. Those both can be found in the Spotinst Console under settings. Once you have those you will enter: +After the installation is completed you can then configure your credentials. Before you do this you will want to have your Spotinst account ID number and Spotinst API token ready to go. Those both can be found in the Spotinst Console under settings. Once you have those you will enter: ```bash serverless config credentials --provider spotinst --token {Your Spotinst API Token} --account {Your Spotinst Account ID} ``` - To check to see that your credentials have been set up properly you can check the credentials files by entering: +To check to see that your credentials have been set up properly you can check the credentials files by entering: ```bash cat ~/.spotinst/credentials ``` - Here you should see the account ID and Token that are linked to your account. - - **Note:** Once you have set up your Spotinst Credentials you will not need to do this again for each project +Here you should see the account ID and Token that are linked to your account. - For more help please refer to the [Credentials](./credentials.md) link provided +**Note:** Once you have set up your Spotinst Credentials you will not need to do this again for each project + +For more help please refer to the [Credentials](./credentials.md) link provided ## Create a New Project From a Template - *You can skip this step if you have already done this step in configuring your credentials* - - Create a new service using the Node.js template, specifying a unique name and an optional path for your service. + +_You can skip this step if you have already done this step in configuring your credentials_ + +Create a new service using the Node.js template, specifying a unique name and an optional path for your service. ```bash serverless create --template spotinst-nodejs --path new-service @@ -65,30 +70,32 @@ cd new-service ``` ## Install Spotinst Serverless Functions Plugin - *You can skip this step if you have already done this step in configuring your credentials* - You will first need to install the Spotinst Functions plugin before you are able to deploy your function. Once this has been done you do not need to do it again for this project. +_You can skip this step if you have already done this step in configuring your credentials_ + +You will first need to install the Spotinst Functions plugin before you are able to deploy your function. Once this has been done you do not need to do it again for this project. ```bash npm install ``` ## Deploying and Updating the Function - Deploying a project is how you launch the project into production. Once it has been deployed, you will be able to see and edit it in the Spotinst Console. - Before you deploy you will need to add in the environment ID into the `serverless.yml` file. The environment ID can be found on the Spotinst console under Functions. In this menu you will be able to add applications, environments and functions. An application is able to hold many environments and environments can hold many functions. They are mostly used for organization purposes and are at your descretion to manipulate as you like. To deploy your function you will need to select an application and environment and copy/paste the environment ID into the `serverless.yml` file under the environment tag. - +Deploying a project is how you launch the project into production. Once it has been deployed, you will be able to see and edit it in the Spotinst Console. + +Before you deploy you will need to add in the environment ID into the `serverless.yml` file. The environment ID can be found on the Spotinst console under Functions. In this menu you will be able to add applications, environments and functions. An application is able to hold many environments and environments can hold many functions. They are mostly used for organization purposes and are at your descretion to manipulate as you like. To deploy your function you will need to select an application and environment and copy/paste the environment ID into the `serverless.yml` file under the environment tag. + 1. **Deploying the Service** - Use this when you have made changes to your Functions, Events or Resources in `serverless.yml` or you simply want to deploy all changes within your Service at the same time. +Use this when you have made changes to your Functions, Events or Resources in `serverless.yml` or you simply want to deploy all changes within your Service at the same time. ```bash -serverless deploy +serverless deploy ``` 2. **Deploy the Function** - Use this to quickly upload and overwrite your function code, allowing you to develop faster. +Use this to quickly upload and overwrite your function code, allowing you to develop faster. ```bash serverless deploy function -f hello @@ -96,17 +103,17 @@ serverless deploy function -f hello 3. **Updating the Function** - Use this to update your function after you have made updates that you want to push to production. - +Use this to update your function after you have made updates that you want to push to production. + ```bash -serverless deploy +serverless deploy ``` ## Invoke the Function - Invoking a function simply means to run it. There are many event triggers that will invoke a function depending on your needs. All functions are assigned a unique URL that is initially set to private in the `serverless.yml` file but if set to public can trigger an invocation from a web browser. Also you are able to set up a cron function in the `serverless.yml` file to run at regular intervals and invoke the function on a timer. If you simply want to test the function you can invoke from the console. Under the function name there is a Test tab you can select and run a test. Otherwise you are able to test from the terminal as shown below. +Invoking a function simply means to run it. There are many event triggers that will invoke a function depending on your needs. All functions are assigned a unique URL that is initially set to private in the `serverless.yml` file but if set to public can trigger an invocation from a web browser. Also you are able to set up a cron function in the `serverless.yml` file to run at regular intervals and invoke the function on a timer. If you simply want to test the function you can invoke from the console. Under the function name there is a Test tab you can select and run a test. Otherwise you are able to test from the terminal as shown below. - Invokes a Function +Invokes a Function ```bash serverless invoke -f hello diff --git a/docs/providers/spotinst/guide/serverless.yml.md b/docs/providers/spotinst/guide/serverless.yml.md index 8fe0c6e12..dcbf88846 100644 --- a/docs/providers/spotinst/guide/serverless.yml.md +++ b/docs/providers/spotinst/guide/serverless.yml.md @@ -1,5 +1,5 @@ + ### [Read this on the main serverless docs site](https://serverless.com/framework/docs/providers/spotinst/guide/serverless.yml/) + # Serverless.yml Reference @@ -36,13 +38,13 @@ provider: 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 +# 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. The environment variables can be set in here or on the Spotinst console. # Once they are set you can access the variables in your handler file with # process.env['{Your Key}'] diff --git a/docs/providers/spotinst/guide/variables.md b/docs/providers/spotinst/guide/variables.md index e6e222b43..48fcc38c3 100644 --- a/docs/providers/spotinst/guide/variables.md +++ b/docs/providers/spotinst/guide/variables.md @@ -7,16 +7,18 @@ layout: Doc --> + ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/spotinst/guide/variables) + # Spotinst Functions - Variables -There are a few ways to introduce external variables to your serverless functions in order to customize each function call based on your personal needs. +There are a few ways to introduce external variables to your serverless functions in order to customize each function call based on your personal needs. ## Environment Variables -Environment variables allow you to pass static information into your function so you wont have to upload sensitive or protected information in your production code. It also allows you to easily change these variables from the outside so you do not have to upload your code multiple times with different variables. +Environment variables allow you to pass static information into your function so you wont have to upload sensitive or protected information in your production code. It also allows you to easily change these variables from the outside so you do not have to upload your code multiple times with different variables. To enter your environment variables you will need to go into the Spotinst console find the function you want to add environment variables to. Then under the Configuration tab you will find the Environment Variables heading. Here you can enter as many variables you need all with an associated key. @@ -34,7 +36,7 @@ To access your variables in your code you just need to put `process.env['{Your K ## URL Argument Variables -URL parameters can be use when a POST request is made to the endpoint of your function. +URL parameters can be use when a POST request is made to the endpoint of your function. ### 1. Node JS diff --git a/lib/Serverless.js b/lib/Serverless.js index 697c51065..e43241b11 100644 --- a/lib/Serverless.js +++ b/lib/Serverless.js @@ -4,6 +4,7 @@ const path = require('path'); const BbPromise = require('bluebird'); const os = require('os'); const updateNotifier = require('update-notifier'); +const minimist = require('minimist'); const pkg = require('../package.json'); const CLI = require('./classes/CLI'); const Config = require('./classes/Config'); @@ -31,7 +32,9 @@ class Serverless { this.pluginManager = new PluginManager(this); // use the servicePath from the options or try to find it in the CWD - configObject.servicePath = configObject.servicePath || this.utils.findServicePath(); + configObject.servicePath = + configObject.servicePath || + this.utils.findServicePath(minimist(process.argv.slice(2)).config); this.config = new Config(this, configObject); @@ -49,7 +52,7 @@ class Serverless { init() { // create an instanceId (can be e.g. used when a predictable random value is needed) - this.instanceId = (new Date()).getTime().toString(); + this.instanceId = new Date().getTime().toString(); // create a new CLI instance this.cli = new this.classes.CLI(this); @@ -58,26 +61,30 @@ class Serverless { this.processedInput = this.cli.processInput(); // load config file - return this.pluginManager.loadConfigFile().then(() => { - // set the options and commands which were processed by the CLI - this.pluginManager.setCliOptions(this.processedInput.options); - this.pluginManager.setCliCommands(this.processedInput.commands); + return this.pluginManager + .loadConfigFile() + .then(() => { + // set the options and commands which were processed by the CLI + this.pluginManager.setCliOptions(this.processedInput.options); + this.pluginManager.setCliCommands(this.processedInput.commands); - // Check if update is available - updateNotifier({ pkg }).notify(); + // Check if update is available + updateNotifier({ pkg }).notify(); - return this.service.load(this.processedInput.options); - }).then(() => { - // load all plugins - this.pluginManager.loadAllPlugins(this.service.plugins); - return this.pluginManager.asyncPluginInit(); - }).then(() => { - // give the CLI the plugins and commands so that it can print out - // information such as options when the user enters --help - this.cli.setLoadedPlugins(this.pluginManager.getPlugins()); - this.cli.setLoadedCommands(this.pluginManager.getCommands()); - return this.pluginManager.updateAutocompleteCacheFile(); - }); + return this.service.load(this.processedInput.options); + }) + .then(() => { + // load all plugins + this.pluginManager.loadAllPlugins(this.service.plugins); + return this.pluginManager.asyncPluginInit(); + }) + .then(() => { + // give the CLI the plugins and commands so that it can print out + // information such as options when the user enters --help + this.cli.setLoadedPlugins(this.pluginManager.getPlugins()); + this.cli.setLoadedCommands(this.pluginManager.getCommands()); + return this.pluginManager.updateAutocompleteCacheFile(); + }); } run() { @@ -93,8 +100,7 @@ class Serverless { // populate variables after --help, otherwise help may fail to print // (https://github.com/serverless/serverless/issues/2041) - return this.variables.populateService(this.pluginManager.cliOptions) - .then(() => { + return this.variables.populateService(this.pluginManager.cliOptions).then(() => { // merge arrays after variables have been populated // (https://github.com/serverless/serverless/issues/3511) this.service.mergeArrays(); diff --git a/lib/Serverless.test.js b/lib/Serverless.test.js index a1c42707c..83a66606d 100644 --- a/lib/Serverless.test.js +++ b/lib/Serverless.test.js @@ -13,8 +13,8 @@ const PluginManager = require('../lib/classes/PluginManager'); const Utils = require('../lib/classes/Utils'); const Service = require('../lib/classes/Service'); const CLI = require('../lib/classes/CLI'); -const ServerlessError = require('../lib/classes/Error').ServerlessError; -const testUtils = require('../tests/utils'); +const { ServerlessError } = require('../lib/classes/Error'); +const { getTmpDirPath } = require('../tests/utils/fs'); describe('Serverless', () => { let serverless; @@ -115,12 +115,13 @@ describe('Serverless', () => { let updateAutocompleteCacheFileStub; beforeEach(() => { - loadAllPluginsStub = sinon - .stub(serverless.pluginManager, 'loadAllPlugins').returns(); + loadAllPluginsStub = sinon.stub(serverless.pluginManager, 'loadAllPlugins').returns(); asyncPluginInitStub = sinon - .stub(serverless.pluginManager, 'asyncPluginInit').returns(BbPromise.resolve()); + .stub(serverless.pluginManager, 'asyncPluginInit') + .returns(BbPromise.resolve()); updateAutocompleteCacheFileStub = sinon - .stub(serverless.pluginManager, 'updateAutocompleteCacheFile').resolves(); + .stub(serverless.pluginManager, 'updateAutocompleteCacheFile') + .resolves(); }); afterEach(() => { @@ -129,13 +130,15 @@ describe('Serverless', () => { serverless.pluginManager.updateAutocompleteCacheFile.restore(); }); - it('should set an instanceId', () => serverless.init().then(() => { - expect(serverless.instanceId).to.match(/\d/); - })); + it('should set an instanceId', () => + serverless.init().then(() => { + expect(serverless.instanceId).to.match(/\d/); + })); - it('should create a new CLI instance', () => serverless.init().then(() => { - expect(serverless.cli).to.be.instanceof(CLI); - })); + it('should create a new CLI instance', () => + serverless.init().then(() => { + expect(serverless.cli).to.be.instanceof(CLI); + })); it('should allow a custom CLI instance', () => { class CustomCLI extends CLI {} @@ -149,15 +152,14 @@ describe('Serverless', () => { // note: we just test that the processedInput variable is set (not the content of it) // the test for the correct input is done in the CLI class test file - it('should receive the processed input form the CLI instance', () => serverless.init() - .then(() => { + it('should receive the processed input form the CLI instance', () => + serverless.init().then(() => { expect(serverless.processedInput).to.not.deep.equal({}); - }) - ); + })); it('should resolve after loading the service', () => { const SUtils = new Utils(); - const tmpDirPath = testUtils.getTmpDirPath(); + const tmpDirPath = getTmpDirPath(); const serverlessYml = { service: 'new-service', provider: 'aws', @@ -174,8 +176,7 @@ describe('Serverless', () => { }, }; - SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), - YAML.dump(serverlessYml)); + SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), YAML.dump(serverlessYml)); serverless.config.update({ servicePath: tmpDirPath }); serverless.pluginManager.cliOptions = { @@ -201,16 +202,11 @@ describe('Serverless', () => { serverless.cli = new CLI(serverless); serverless.processedInput = { commands: [], options: {} }; // setup default stubs - logStatStub = sinon - .stub(serverless.utils, 'logStat').resolves(); - displayHelpStub = sinon - .stub(serverless.cli, 'displayHelp').returns(false); - validateCommandStub = sinon - .stub(serverless.pluginManager, 'validateCommand').returns(); - populateServiceStub = sinon - .stub(serverless.variables, 'populateService').resolves(); - runStub = sinon - .stub(serverless.pluginManager, 'run').resolves(); + logStatStub = sinon.stub(serverless.utils, 'logStat').resolves(); + displayHelpStub = sinon.stub(serverless.cli, 'displayHelp').returns(false); + validateCommandStub = sinon.stub(serverless.pluginManager, 'validateCommand').returns(); + populateServiceStub = sinon.stub(serverless.variables, 'populateService').resolves(); + runStub = sinon.stub(serverless.pluginManager, 'run').resolves(); }); afterEach(() => { @@ -255,8 +251,7 @@ describe('Serverless', () => { expect(validateCommandStub.calledOnce).to.equal(true); expect(populateServiceStub.calledOnce).to.equal(true); expect(runStub.calledOnce).to.equal(true); - }) - ); + })); }); describe('#setProvider()', () => { diff --git a/lib/classes/CLI.js b/lib/classes/CLI.js index 4b6b58a72..4c1f9ea74 100644 --- a/lib/classes/CLI.js +++ b/lib/classes/CLI.js @@ -1,6 +1,8 @@ 'use strict'; const version = require('../../package.json').version; +const enterpriseVersion = require('@serverless/enterprise-plugin/package.json').version; +const { sdkVersion } = require('@serverless/enterprise-plugin'); const minimist = require('minimist'); const _ = require('lodash'); const os = require('os'); @@ -34,10 +36,9 @@ class CLI { inputArray = process.argv.slice(2); } - const base64Encode = (valueStr) => - new Buffer(valueStr).toString('base64'); + const base64Encode = valueStr => new Buffer(valueStr).toString('base64'); - const toBase64Helper = (value) => { + const toBase64Helper = value => { const valueStr = value.toString(); if (valueStr.startsWith('-')) { if (valueStr.indexOf('=') !== -1) { @@ -55,7 +56,7 @@ class CLI { return base64Encode(valueStr); }; - const decodedArgsHelper = (arg) => { + const decodedArgsHelper = arg => { if (_.isString(arg)) { return new Buffer(arg, 'base64').toString(); } else if (_.isArray(arg)) { @@ -100,7 +101,7 @@ class CLI { // Make "log" no-op to suppress warnings. // But preserve "consoleLog" which "print" command use to print config. - this.log = function () {}; + this.log = function() {}; } displayHelp(processedInput) { @@ -108,16 +109,20 @@ class CLI { const options = processedInput.options; // if only "version" or "v" was entered - if ((commands.length === 0 && (options.version || options.v)) || - (commands.length === 1 && (commands.indexOf('version') > -1))) { + if ( + (commands.length === 0 && (options.version || options.v)) || + (commands.length === 1 && commands.indexOf('version') > -1) + ) { this.getVersionNumber(); return true; } // if only "help" or "h" was entered - if ((commands.length === 0) || - (commands.length === 0 && (options.help || options.h)) || - (commands.length === 1 && (commands.indexOf('help') > -1))) { + if ( + commands.length === 0 || + (commands.length === 0 && (options.help || options.h)) || + (commands.length === 1 && commands.indexOf('help') > -1) + ) { if (options.verbose || options.v) { this.generateVerboseHelp(); } else { @@ -152,7 +157,17 @@ class CLI { displayCommandOptions(commandObject) { const dotsLength = 40; - _.forEach(commandObject.options, (optionsObject, option) => { + + const commandOptions = commandObject.configDependent + ? Object.assign({}, commandObject.options, { + config: { + usage: 'Path to serverless config file', + shortcut: 'c', + }, + }) + : commandObject.options; + + _.forEach(commandOptions, (optionsObject, option) => { let optionsDots = _.repeat('.', dotsLength - option.length); const optionsUsage = optionsObject.usage; @@ -175,8 +190,9 @@ class CLI { requiredInfo = ' (required)'; } - const thingsToLog = `${optionInfo}${shortcutInfo}${requiredInfo} ${ - chalk.dim(optionsDots)} ${optionsUsage}`; + const thingsToLog = `${optionInfo}${shortcutInfo}${requiredInfo} ${chalk.dim( + optionsDots + )} ${optionsUsage}`; this.consoleLog(chalk.yellow(thingsToLog)); }); } @@ -210,8 +226,8 @@ class CLI { this.consoleLog(chalk.yellow.underline('Plugins')); if (this.loadedPlugins.length) { - const sortedPlugins = _.sortBy(this.loadedPlugins, (plugin) => plugin.constructor.name); - this.consoleLog(sortedPlugins.map((plugin) => plugin.constructor.name).join(', ')); + const sortedPlugins = _.sortBy(this.loadedPlugins, plugin => plugin.constructor.name); + this.consoleLog(sortedPlugins.map(plugin => plugin.constructor.name).join(', ')); } else { this.consoleLog('No plugins added yet'); } @@ -225,7 +241,7 @@ class CLI { let pluginCommands = {}; // add commands to pluginCommands based on command's plugin - const addToPluginCommands = (cmd) => { + const addToPluginCommands = cmd => { const pcmd = _.clone(cmd); // remove subcommand from clone @@ -241,24 +257,27 @@ class CLI { // check for subcommands if ('commands' in cmd) { - _.forEach(cmd.commands, (d) => { + _.forEach(cmd.commands, d => { addToPluginCommands(d); }); } }; // fill up pluginCommands with commands in loadedCommands - _.forEach(this.loadedCommands, (details) => { + _.forEach(this.loadedCommands, details => { addToPluginCommands(details); }); // sort plugins alphabetically - pluginCommands = _(pluginCommands).toPairs().sortBy(0).fromPairs() + pluginCommands = _(pluginCommands) + .toPairs() + .sortBy(0) + .fromPairs() .value(); _.forEach(pluginCommands, (details, plugin) => { this.consoleLog(plugin); - _.forEach(details, (cmd) => { + _.forEach(details, cmd => { // display command usage with single(1) indent this.displayCommandUsage(cmd, cmd.key.split(':').join(' '), 1); }); @@ -272,12 +291,16 @@ class CLI { // Get all the commands using getCommands() with filtered entrypoint // commands and reduce to the required command. const allCommands = this.serverless.pluginManager.getCommands(); - const command = _.reduce(commandsArray, (currentCmd, cmd) => { - if (currentCmd.commands && cmd in currentCmd.commands) { - return currentCmd.commands[cmd]; - } - return null; - }, { commands: allCommands }); + const command = _.reduce( + commandsArray, + (currentCmd, cmd) => { + if (currentCmd.commands && cmd in currentCmd.commands) { + return currentCmd.commands[cmd]; + } + return null; + }, + { commands: allCommands } + ); // Throw error if command not found. if (!command) { @@ -300,7 +323,9 @@ class CLI { } getVersionNumber() { - this.consoleLog(version); + this.consoleLog( + `${version} (Enterprise Plugin: ${enterpriseVersion}, Platform SDK: ${sdkVersion})` + ); } asciiGreeting() { diff --git a/lib/classes/CLI.test.js b/lib/classes/CLI.test.js index 7eae2c0a2..29540d41d 100644 --- a/lib/classes/CLI.test.js +++ b/lib/classes/CLI.test.js @@ -1,6 +1,8 @@ 'use strict'; -const expect = require('chai').expect; +/* eslint-disable no-unused-expressions */ + +const chai = require('chai'); const sinon = require('sinon'); const CLI = require('../../lib/classes/CLI'); const os = require('os'); @@ -9,7 +11,10 @@ const exec = require('child_process').exec; const path = require('path'); const stripAnsi = require('strip-ansi'); const Serverless = require('../../lib/Serverless'); -const testUtils = require('../../tests/utils'); +const { getTmpDirPath } = require('../../tests/utils/fs'); + +const { expect } = chai; +chai.use(require('sinon-chai')); describe('CLI', () => { let cli; @@ -31,7 +36,7 @@ describe('CLI', () => { }); it('should set a null inputArray when none is provided', () => - expect(new CLI(serverless).inputArray).to.be.null); + expect(new CLI(serverless).inputArray).to.be.null); it('should set the inputObject when provided', () => { cli = new CLI(serverless, ['foo', 'bar', '--baz', '-qux']); @@ -179,9 +184,7 @@ describe('CLI', () => { this.commands = { test: { usage: 'test', - lifecycleEvents: [ - 'test', - ], + lifecycleEvents: ['test'], options: { name: { usage: 'test', @@ -193,9 +196,7 @@ describe('CLI', () => { commands: { test: { usage: 'test', - lifecycleEvents: [ - 'test', - ], + lifecycleEvents: ['test'], options: { name: { usage: 'test', @@ -263,9 +264,7 @@ describe('CLI', () => { this.commands = { test: { usage: 'test', - lifecycleEvents: [ - 'test', - ], + lifecycleEvents: ['test'], options: { name: { usage: 'test', @@ -297,9 +296,7 @@ describe('CLI', () => { this.commands = { test: { usage: 'test', - lifecycleEvents: [ - 'test', - ], + lifecycleEvents: ['test'], options: { name: { usage: 'test', @@ -311,9 +308,7 @@ describe('CLI', () => { commands: { test: { usage: 'test', - lifecycleEvents: [ - 'test', - ], + lifecycleEvents: ['test'], options: { name: { usage: 'test', @@ -347,9 +342,7 @@ describe('CLI', () => { this.commands = { test: { usage: 'test', - lifecycleEvents: [ - 'test', - ], + lifecycleEvents: ['test'], options: { name: { usage: 'test', @@ -397,8 +390,7 @@ describe('CLI', () => { beforeEach(() => { cli = new CLI(serverless); - getCommandsStub = sinon.stub(cli.serverless.pluginManager, 'getCommands') - .returns(commands); + getCommandsStub = sinon.stub(cli.serverless.pluginManager, 'getCommands').returns(commands); consoleLogStub = sinon.stub(cli, 'consoleLog').returns(); displayCommandUsageStub = sinon.stub(cli, 'displayCommandUsage').returns(); displayCommandOptionsStub = sinon.stub(cli, 'displayCommandOptions').returns(); @@ -420,14 +412,9 @@ describe('CLI', () => { expect(getCommandsStub.calledOnce).to.equal(true); expect(consoleLogStub.called).to.equal(true); expect(displayCommandUsageStub.calledOnce).to.equal(true); - expect(displayCommandUsageStub.calledWithExactly( - commands.package, - 'package' - )).to.equal(true); + expect(displayCommandUsageStub.calledWithExactly(commands.package, 'package')).to.equal(true); expect(displayCommandOptionsStub.calledOnce).to.equal(true); - expect(displayCommandOptionsStub.calledWithExactly( - commands.package - )).to.equal(true); + expect(displayCommandOptionsStub.calledWithExactly(commands.package)).to.equal(true); }); it('should throw an error if the command could not be found', () => { @@ -435,8 +422,9 @@ describe('CLI', () => { cli.inputArray = commandsArray; - expect(() => { cli.generateCommandsHelp(commandsArray); }) - .to.throw(Error, 'not found'); + expect(() => { + cli.generateCommandsHelp(commandsArray); + }).to.throw(Error, 'not found'); expect(getCommandsStub.calledOnce).to.equal(true); expect(consoleLogStub.called).to.equal(false); expect(displayCommandUsageStub.calledOnce).to.equal(false); @@ -444,6 +432,28 @@ describe('CLI', () => { }); }); + describe('#getVersionNumber()', () => { + let consoleLogSpy; + + beforeEach(() => { + cli = new CLI(serverless); + consoleLogSpy = sinon.spy(cli, 'consoleLog'); + }); + + afterEach(() => { + cli.consoleLog.restore(); + }); + + it('should log the version numbers', () => { + cli.getVersionNumber(); + + expect(consoleLogSpy).to.have.been.calledOnce; + expect(consoleLogSpy.args[0][0]).to.match(/[0-9]+\.[0-9]+\.[0-9]+ .+/); + expect(consoleLogSpy.args[0][0]).to.include('Enterprise Plugin'); + expect(consoleLogSpy.args[0][0]).to.include('Platform SDK'); + }); + }); + describe('#processInput()', () => { it('should only return the commands when only commands are given', () => { cli = new CLI(serverless, ['deploy', 'functions']); @@ -608,12 +618,12 @@ describe('CLI', () => { }); }); - describe('Integration tests', function () { + describe('Integration tests', function() { this.timeout(0); const that = this; before(() => { - const tmpDir = testUtils.getTmpDirPath(); + const tmpDir = getTmpDirPath(); that.cwd = process.cwd(); @@ -621,22 +631,24 @@ describe('CLI', () => { process.chdir(tmpDir); serverless = new Serverless(); - serverless.init(); + return serverless.init().then(() => { + // Cannot rely on shebang in severless.js to invoke script using NodeJS on Windows. + const execPrefix = os.platform() === 'win32' ? 'node ' : ''; - // Cannot rely on shebang in severless.js to invoke script using NodeJS on Windows. - const execPrefix = os.platform() === 'win32' ? 'node ' : ''; - - that.serverlessExec = execPrefix + path.join(serverless.config.serverlessPath, - '..', 'bin', 'serverless'); + that.serverlessExec = + execPrefix + path.join(serverless.config.serverlessPath, '..', 'bin', 'serverless'); + }); }); after(() => { process.chdir(that.cwd); }); - it('should print general --help to stdout', (done) => { - exec(`${this.serverlessExec} --help`, (err, stdout) => { + it('should print general --help to stdout', done => { + exec(`${this.serverlessExec} --help`, (err, stdout, stderr) => { if (err) { + process.stdout.write(stdout); + process.stderr.write(stderr); done(err); return; } @@ -646,9 +658,11 @@ describe('CLI', () => { }); }); - it('should print command --help to stdout', (done) => { - exec(`${this.serverlessExec} deploy --help`, (err, stdout) => { + it('should print command --help to stdout', done => { + exec(`${this.serverlessExec} deploy --help`, (err, stdout, stderr) => { if (err) { + process.stdout.write(stdout); + process.stderr.write(stderr); done(err); return; } @@ -659,9 +673,11 @@ describe('CLI', () => { }); }); - it('should print help --verbose to stdout', (done) => { - exec(`${this.serverlessExec} help --verbose`, (err, stdout) => { + it('should print help --verbose to stdout', done => { + exec(`${this.serverlessExec} help --verbose`, (err, stdout, stderr) => { if (err) { + process.stdout.write(stdout); + process.stderr.write(stderr); done(err); return; } diff --git a/lib/classes/Config.js b/lib/classes/Config.js index d563e5aa2..f74f841c1 100644 --- a/lib/classes/Config.js +++ b/lib/classes/Config.js @@ -4,7 +4,6 @@ const _ = require('lodash'); const path = require('path'); class Config { - constructor(serverless, config) { this.serverless = serverless; this.serverlessPath = path.join(__dirname, '..'); @@ -18,4 +17,3 @@ class Config { } module.exports = Config; - diff --git a/lib/classes/Error.js b/lib/classes/Error.js index cd66e850c..ca618ddfe 100644 --- a/lib/classes/Error.js +++ b/lib/classes/Error.js @@ -1,10 +1,14 @@ 'use strict'; + const chalk = require('chalk'); +const { inspect } = require('util'); const version = require('./../../package.json').version; +const sfeVersion = require('@serverless/enterprise-plugin/package.json').version; +const { sdkVersion } = require('@serverless/enterprise-plugin'); // raven implementation examples https://www.npmjs.com/browse/depended/raven const errorReporter = require('../utils/sentry').raven; -const consoleLog = (message) => { +const consoleLog = message => { console.log(message); // eslint-disable-line no-console }; @@ -38,7 +42,7 @@ module.exports.ServerlessError = class ServerlessError extends Error { // Deprecated - use ServerlessError instead module.exports.SError = module.exports.ServerlessError; -module.exports.logError = (e) => { +module.exports.logError = e => { try { const errorType = e.name.replace(/([A-Z])/g, ' $1'); @@ -67,15 +71,18 @@ module.exports.logError = (e) => { consoleLog(chalk.yellow(' Get Support --------------------------------------------')); consoleLog(`${chalk.yellow(' Docs: ')}${'docs.serverless.com'}`); - consoleLog(`${chalk.yellow(' Bugs: ')}${ - 'github.com/serverless/serverless/issues'}`); + consoleLog( + `${chalk.yellow(' Bugs: ')}${'github.com/serverless/serverless/issues'}` + ); consoleLog(`${chalk.yellow(' Issues: ')}${'forum.serverless.com'}`); consoleLog(' '); consoleLog(chalk.yellow(' Your Environment Information ---------------------------')); - consoleLog(chalk.yellow(` OS: ${platform}`)); - consoleLog(chalk.yellow(` Node Version: ${nodeVersion}`)); - consoleLog(chalk.yellow(` Serverless Version: ${slsVersion}`)); + consoleLog(chalk.yellow(` Operating System: ${platform}`)); + consoleLog(chalk.yellow(` Node Version: ${nodeVersion}`)); + consoleLog(chalk.yellow(` Serverless Version: ${slsVersion}`)); + consoleLog(chalk.yellow(` Enterprise Plugin Version: ${sfeVersion}`)); + consoleLog(chalk.yellow(` Platform SDK Version: ${sdkVersion}`)); consoleLog(' '); // Exit early for users who have opted out of tracking @@ -84,19 +91,19 @@ module.exports.logError = (e) => { process.exit(1); } // report error to sentry. - errorReporter.captureException(e, (sendErr, eventId) => { // eslint-disable-line + errorReporter.captureException(e, () => { // process.exit(1) for CI systems to correctly fail process.exit(1); }); } catch (errorHandlingError) { - throw new Error(e); + throw new Error(inspect(e)); } }; -module.exports.logWarning = (message) => { +module.exports.logWarning = message => { writeMessage('Serverless Warning', message); }; -module.exports.logInfo = (message) => { +module.exports.logInfo = message => { writeMessage('Serverless Information', message); }; diff --git a/lib/classes/Error.test.js b/lib/classes/Error.test.js index c1832f123..8998ab695 100644 --- a/lib/classes/Error.test.js +++ b/lib/classes/Error.test.js @@ -1,7 +1,8 @@ 'use strict'; const expect = require('chai').expect; -const sinon = require('sinon'); +const sandbox = require('sinon'); +const overrideEnv = require('process-utils/override-env'); const errorReporter = require('../utils/sentry').raven; const ServerlessError = require('./Error').ServerlessError; const logError = require('./Error').logError; @@ -49,16 +50,17 @@ describe('ServerlessError', () => { }); describe('Error', () => { - let sandbox; let consoleLogSpy; let processExitStub; let captureExceptionStub; beforeEach(() => { - sandbox = sinon.sandbox.create(); consoleLogSpy = sandbox.spy(console, 'log'); // the following is used so that the process exiting never interrupts tests - processExitStub = sandbox.stub(process, 'exit').withArgs(1).returns(); + processExitStub = sandbox + .stub(process, 'exit') + .withArgs(1) + .returns(); captureExceptionStub = sandbox.stub(errorReporter, 'captureException').yields(); }); @@ -67,9 +69,11 @@ describe('Error', () => { }); describe('#logError()', () => { - beforeEach(() => { - delete process.env.SLS_DEBUG; - }); + let restoreEnv; + + beforeEach(() => ({ restoreEnv } = overrideEnv())); + + afterEach(() => restoreEnv()); it('should log error and exit', () => { const error = new ServerlessError('a message', 'a status code'); @@ -87,6 +91,24 @@ describe('Error', () => { expect(message).to.have.string('a message'); }); + it('should log environment information', () => { + const error = new ServerlessError('a message', 'a status code'); + logError(error); + + const message = consoleLogSpy.args.join('\n'); + + expect(consoleLogSpy.called).to.equal(true); + + expect(message).to.have.string('Serverless Error'); + expect(message).to.have.string('a message'); + expect(message).to.have.string('Your Environment Information'); + expect(message).to.have.string('Operating System:'); + expect(message).to.have.string('Node Version:'); + expect(message).to.have.string('Serverless Version:'); + expect(message).to.have.string('Enterprise Plugin Version:'); + expect(message).to.have.string('Platform SDK Version:'); + }); + it('should capture the exception and exit the process with 1 if errorReporter is setup', () => { const error = new Error('an unexpected error'); logError(error); @@ -107,7 +129,7 @@ describe('Error', () => { }); it('should print stack trace with SLS_DEBUG', () => { - process.env.SLS_DEBUG = 1; + process.env.SLS_DEBUG = '1'; const error = new ServerlessError('a message'); logError(error); @@ -130,8 +152,6 @@ describe('Error', () => { }); it('should re-throw error when handling raises an exception itself', () => { - const handlingError = new Error('an unexpected error'); - let thrownError = null; try { logError('INVALID INPUT'); @@ -139,7 +159,7 @@ describe('Error', () => { thrownError = e; } - expect(thrownError).to.deep.equal(handlingError); + expect(thrownError.message).to.equal("'INVALID INPUT'"); }); }); diff --git a/lib/classes/PluginManager.js b/lib/classes/PluginManager.js index 65ee301a9..0853ebdad 100644 --- a/lib/classes/PluginManager.js +++ b/lib/classes/PluginManager.js @@ -1,13 +1,15 @@ 'use strict'; const path = require('path'); +const config = require('../utils/config'); const Module = require('module'); const BbPromise = require('bluebird'); const _ = require('lodash'); +const crypto = require('crypto'); +const updateNotifier = require('update-notifier'); const writeFile = require('../utils/fs/writeFile'); const getCacheFilePath = require('../utils/getCacheFilePath'); const serverlessConfigFileUtils = require('../utils/getServerlessConfigFile'); -const crypto = require('crypto'); const getCommandSuggestion = require('../utils/getCommandSuggestion'); /** @@ -43,8 +45,8 @@ class PluginManager { loadConfigFile() { return serverlessConfigFileUtils - .getServerlessConfigFile(this.serverless.config.servicePath) - .then((serverlessConfigFile) => { + .getServerlessConfigFile(this.serverless) + .then(serverlessConfigFile => { this.serverlessConfigFile = serverlessConfigFile; return; }); @@ -72,8 +74,7 @@ class PluginManager { } // ignore plugins that specify a different provider than the current one - if (pluginProvider - && (pluginProvider !== this.serverless.service.provider.name)) { + if (pluginProvider && pluginProvider !== this.serverless.service.provider.name) { return; } @@ -92,12 +93,15 @@ class PluginManager { loadAllPlugins(servicePlugins) { this.loadCorePlugins(); this.loadServicePlugins(servicePlugins); + this.loadEnterprisePlugin(); } loadPlugins(plugins) { - plugins.forEach((plugin) => { + plugins.forEach(plugin => { try { - const Plugin = require(plugin); // eslint-disable-line global-require + const servicePath = this.serverless.config.servicePath; + const pluginPath = plugin.startsWith('./') ? path.join(servicePath, plugin) : plugin; + const Plugin = require(pluginPath); // eslint-disable-line global-require this.addPlugin(Plugin); } catch (error) { @@ -118,8 +122,7 @@ class PluginManager { if (process.env.SLS_DEBUG) { throw error; } - errorMessage = - `Serverless plugin "${plugin}" initialization errored: ${error.message}`; + errorMessage = `Serverless plugin "${plugin}" initialization errored: ${error.message}`; } if (!this.cliOptions.help) { throw new this.serverless.classes.Error(errorMessage); @@ -133,8 +136,8 @@ class PluginManager { loadCorePlugins() { const pluginsDirectoryPath = path.join(__dirname, '../plugins'); const corePlugins = this.serverless.utils - .readFileSync(path.join(pluginsDirectoryPath, 'Plugins.json')).plugins - .map((corePluginPath) => path.join(pluginsDirectoryPath, corePluginPath)); + .readFileSync(path.join(pluginsDirectoryPath, 'Plugins.json')) + .plugins.map(corePluginPath => path.join(pluginsDirectoryPath, corePluginPath)); this.loadPlugins(corePlugins); } @@ -148,20 +151,48 @@ class PluginManager { if (pluginsObject.localPath) { module.paths.unshift(pluginsObject.localPath); } - this.loadPlugins(pluginsObject.modules); + this.loadPlugins( + pluginsObject.modules.filter(name => name !== '@serverless/enterprise-plugin') + ); + } + + loadEnterprisePlugin() { + if (config.getGlobalConfig().enterpriseDisabled) { + return; + } + // `yarn global add` support, deps of deps are installed as deps + module.paths.unshift(path.join(__dirname, '../../../../node_modules')); + // `npm -i g` support, deps of deps are installed inside projects + module.paths.unshift(path.join(__dirname, '../../node_modules')); + this.loadPlugins(['@serverless/enterprise-plugin']); + const sfePkgJson = require('@serverless/enterprise-plugin/package.json'); + + if (this.serverless.enterpriseEnabled) { + const updates = updateNotifier({ pkg: sfePkgJson, interval: 1 }); + if (updates.update) { + this.serverless.cli.log( + 'An updated version of Serverless Enterprise is available. ' + + 'Please upgrade by running `npm i -g serverless`' + ); + } + } } parsePluginsObject(servicePlugs) { - let localPath = (this.serverless && this.serverless.config && - this.serverless.config.servicePath) && + let localPath = + this.serverless && + this.serverless.config && + this.serverless.config.servicePath && path.join(this.serverless.config.servicePath, '.serverless_plugins'); let modules = []; if (_.isArray(servicePlugs)) { modules = servicePlugs; } else if (servicePlugs) { - localPath = servicePlugs.localPath && - _.isString(servicePlugs.localPath) ? servicePlugs.localPath : localPath; + localPath = + servicePlugs.localPath && _.isString(servicePlugs.localPath) + ? servicePlugs.localPath + : localPath; if (_.isArray(servicePlugs.modules)) { modules = servicePlugs.modules; } @@ -173,32 +204,41 @@ class PluginManager { createCommandAlias(alias, command) { // Deny self overrides if (_.startsWith(command, alias)) { - throw new this.serverless.classes - .Error(`Command "${alias}" cannot be overriden by an alias`); + throw new this.serverless.classes.Error(`Command "${alias}" cannot be overriden by an alias`); } const splitAlias = _.split(alias, ':'); - const aliasTarget = _.reduce(splitAlias, (__, aliasPath) => { - const currentAlias = __; - if (!currentAlias[aliasPath]) { - currentAlias[aliasPath] = {}; - } - return currentAlias[aliasPath]; - }, this.aliases); + const aliasTarget = _.reduce( + splitAlias, + (__, aliasPath) => { + const currentAlias = __; + if (!currentAlias[aliasPath]) { + currentAlias[aliasPath] = {}; + } + return currentAlias[aliasPath]; + }, + this.aliases + ); // Check if the alias is already defined if (aliasTarget.command) { - throw new this.serverless.classes - .Error(`Alias "${alias}" is already defined for command ${aliasTarget.command}`); + throw new this.serverless.classes.Error( + `Alias "${alias}" is already defined for command ${aliasTarget.command}` + ); } // Check if the alias would overwrite an exiting command - if (_.reduce(splitAlias, (__, aliasPath) => { - if (!__ || !__.commands || !__.commands[aliasPath]) { - return null; - } - return __.commands[aliasPath]; - }, this)) { - throw new this.serverless.classes - .Error(`Command "${alias}" cannot be overriden by an alias`); + if ( + _.reduce( + splitAlias, + (__, aliasPath) => { + if (!__ || !__.commands || !__.commands[aliasPath]) { + return null; + } + return __.commands[aliasPath]; + }, + this + ) + ) { + throw new this.serverless.classes.Error(`Command "${alias}" cannot be overriden by an alias`); } aliasTarget.command = command; } @@ -211,17 +251,11 @@ class PluginManager { // Check if there is already an alias for the same path as the command const aliasCommand = this.getAliasCommandTarget(_.split(key, ':')); if (aliasCommand) { - throw new this.serverless.classes - .Error(`Command "${key}" cannot override an existing alias`); + throw new this.serverless.classes.Error(`Command "${key}" cannot override an existing alias`); } // Load the command const commands = _.mapValues(details.commands, (subDetails, subKey) => - this.loadCommand( - pluginName, - subDetails, - `${key}:${subKey}`, - commandIsEntryPoint - ) + this.loadCommand(pluginName, subDetails, `${key}:${subKey}`, commandIsEntryPoint) ); // Handle command aliases _.forEach(details.aliases, alias => { @@ -348,44 +382,52 @@ class PluginManager { const aliasCommandTarget = this.getAliasCommandTarget(commandsArray); const commandOrAlias = aliasCommandTarget ? _.split(aliasCommandTarget, ':') : commandsArray; - return _.reduce(commandOrAlias, (current, name, index) => { - const commandExists = name in current.commands; - const isNotContainer = commandExists && current.commands[name].type !== 'container'; - const isNotEntrypoint = commandExists && current.commands[name].type !== 'entrypoint'; - const remainingIterationsLeft = index < commandOrAlias.length - 1; + return _.reduce( + commandOrAlias, + (current, name, index) => { + const commandExists = name in current.commands; + const isNotContainer = commandExists && current.commands[name].type !== 'container'; + const isNotEntrypoint = commandExists && current.commands[name].type !== 'entrypoint'; + const remainingIterationsLeft = index < commandOrAlias.length - 1; - if (commandExists - && (isNotContainer || remainingIterationsLeft) - && (isNotEntrypoint || allowEntryPoints)) { - return current.commands[name]; - } - // if user is using a top level command properly, but sub commands are not - if (this.serverless.cli.loadedCommands[commandOrAlias[0]]) { - const errorMessage = [`"${name}" is not a valid sub command. Run "serverless `]; - for (let i = 0; commandOrAlias[i] !== name; i++) { - errorMessage.push(`${commandOrAlias[i]}`); - if (commandOrAlias[i + 1] !== name) { - errorMessage.push(' '); - } + if ( + commandExists && + (isNotContainer || remainingIterationsLeft) && + (isNotEntrypoint || allowEntryPoints) + ) { + return current.commands[name]; + } + // if user is using a top level command properly, but sub commands are not + if (this.serverless.cli.loadedCommands[commandOrAlias[0]]) { + const errorMessage = [`"${name}" is not a valid sub command. Run "serverless `]; + for (let i = 0; commandOrAlias[i] !== name; i++) { + errorMessage.push(`${commandOrAlias[i]}`); + if (commandOrAlias[i + 1] !== name) { + errorMessage.push(' '); + } + } + errorMessage.push('" to see a more helpful error message for this command.'); + throw new this.serverless.classes.Error(errorMessage.join('')); } - errorMessage.push('" to see a more helpful error message for this command.'); - throw new this.serverless.classes.Error(errorMessage.join('')); - } - // top level command isn't valid. give a suggestion - const commandName = commandOrAlias.slice(0, index + 1).join(' '); - const suggestedCommand = getCommandSuggestion(commandName, - this.serverless.cli.loadedCommands); - const errorMessage = [ - `Serverless command "${commandName}" not found. Did you mean "${suggestedCommand}"?`, - ' Run "serverless help" for a list of all available commands.', - ].join(''); - throw new this.serverless.classes.Error(errorMessage); - }, { commands: this.commands }); + // top level command isn't valid. give a suggestion + const commandName = commandOrAlias.slice(0, index + 1).join(' '); + const suggestedCommand = getCommandSuggestion( + commandName, + this.serverless.cli.loadedCommands + ); + const errorMessage = [ + `Serverless command "${commandName}" not found. Did you mean "${suggestedCommand}"?`, + ' Run "serverless help" for a list of all available commands.', + ].join(''); + throw new this.serverless.classes.Error(errorMessage); + }, + { commands: this.commands } + ); } getEvents(command) { - return _.flatMap(command.lifecycleEvents, (event) => [ + return _.flatMap(command.lifecycleEvents, event => [ `before:${command.key}:${event}`, `${command.key}:${event}`, `after:${command.key}:${event}`, @@ -397,7 +439,7 @@ class PluginManager { } getHooks(events) { - return _.flatMap([].concat(events), (event) => this.hooks[event] || []); + return _.flatMap([].concat(events), event => this.hooks[event] || []); } invoke(commandsArray, allowEntryPoints) { @@ -419,13 +461,15 @@ class PluginManager { } } - return BbPromise.reduce(hooks, (__, hook) => hook.hook(), null) - .catch(TerminateHookChain, () => { - if (process.env.SLS_DEBUG) { - this.serverless.cli.log(`Terminate ${_.join(commandsArray, ':')}`); + return BbPromise.reduce(hooks, (__, hook) => hook.hook(), null).catch( + TerminateHookChain, + () => { + if (process.env.SLS_DEBUG) { + this.serverless.cli.log(`Terminate ${_.join(commandsArray, ':')}`); + } + return BbPromise.resolve(); } - return BbPromise.resolve(); - }); + ); } /** @@ -437,8 +481,7 @@ class PluginManager { if (_.isString(commandsArray)) { commands = _.split(commandsArray, ':'); } - return this.invoke(commands, true) - .then(() => { + return this.invoke(commands, true).then(() => { if (_.get(options, 'terminateLifecycleAfterExecution', false)) { return BbPromise.reject(new TerminateHookChain(commands)); } @@ -477,7 +520,7 @@ class PluginManager { validateOptions(command) { _.forEach(command.options, (value, key) => { - if (value.required && (this.cliOptions[key] === true || !(this.cliOptions[key]))) { + if (value.required && (this.cliOptions[key] === true || !this.cliOptions[key])) { let requiredThings = `the --${key} option`; if (value.shortcut) { @@ -492,10 +535,12 @@ class PluginManager { throw new this.serverless.classes.Error(errorMessage); } - if (_.isPlainObject(value.customValidation) && + if ( + _.isPlainObject(value.customValidation) && value.customValidation.regularExpression instanceof RegExp && _.isString(value.customValidation.errorMessage) && - !value.customValidation.regularExpression.test(this.cliOptions[key])) { + !value.customValidation.regularExpression.test(this.cliOptions[key]) + ) { throw new this.serverless.classes.Error(value.customValidation.errorMessage); } }); @@ -521,8 +566,10 @@ class PluginManager { .concat(Object.keys(command.commands)); }); - const serverlessConfigFileHash = crypto.createHash('sha256') - .update(JSON.stringify(this.serverlessConfigFile)).digest('hex'); + const serverlessConfigFileHash = crypto + .createHash('sha256') + .update(JSON.stringify(this.serverlessConfigFile)) + .digest('hex'); cacheFile.validationHash = serverlessConfigFileHash; const cacheFilePath = getCacheFilePath(this.serverless.config.servicePath); @@ -531,9 +578,11 @@ class PluginManager { convertShortcutsIntoOptions(command) { _.forEach(command.options, (optionObject, optionKey) => { - if (optionObject.shortcut && _.includes(Object.keys(this.cliOptions), - optionObject.shortcut)) { - Object.keys(this.cliOptions).forEach((option) => { + if ( + optionObject.shortcut && + _.includes(Object.keys(this.cliOptions), optionObject.shortcut) + ) { + Object.keys(this.cliOptions).forEach(option => { if (option === optionObject.shortcut) { this.cliOptions[optionKey] = this.cliOptions[option]; } @@ -551,7 +600,7 @@ class PluginManager { } asyncPluginInit() { - return BbPromise.map(this.plugins, plugin => (plugin.asyncInit && plugin.asyncInit())); + return BbPromise.map(this.plugins, plugin => plugin.asyncInit && plugin.asyncInit()); } } diff --git a/lib/classes/PluginManager.test.js b/lib/classes/PluginManager.test.js index f97787555..8c3ae8d15 100644 --- a/lib/classes/PluginManager.test.js +++ b/lib/classes/PluginManager.test.js @@ -3,6 +3,7 @@ /* eslint-disable no-unused-expressions */ const chai = require('chai'); +const overrideEnv = require('process-utils/override-env'); let PluginManager = require('../../lib/classes/PluginManager'); const Serverless = require('../../lib/Serverless'); const CLI = require('../../lib/classes/CLI'); @@ -12,16 +13,18 @@ const _ = require('lodash'); const path = require('path'); const fs = require('fs'); const fse = require('fs-extra'); -const execSync = require('child_process').execSync; const mockRequire = require('mock-require'); -const testUtils = require('../../tests/utils'); const os = require('os'); const sinon = require('sinon'); const proxyquire = require('proxyquire'); const BbPromise = require('bluebird'); const getCacheFilePath = require('../utils/getCacheFilePath'); +const { execSync } = require('child_process'); +const { installPlugin } = require('../../tests/utils/plugins'); +const { getTmpDirPath } = require('../../tests/utils/fs'); chai.use(require('chai-as-promised')); +chai.use(require('sinon-chai')); const expect = chai.expect; @@ -29,11 +32,12 @@ describe('PluginManager', () => { let pluginManager; let serverless; - class ServicePluginMock1 { - } + class ServicePluginMock1 {} class ServicePluginMock2 {} + class EnterprisePluginMock {} + class BrokenPluginMock { constructor() { throw new Error('Broken plugin'); @@ -46,9 +50,7 @@ describe('PluginManager', () => { this.commands = { deploy: { - lifecycleEvents: [ - 'resources', - ], + lifecycleEvents: ['resources'], }, }; @@ -71,9 +73,7 @@ describe('PluginManager', () => { this.commands = { deploy: { - lifecycleEvents: [ - 'resources', - ], + lifecycleEvents: ['resources'], }, }; @@ -95,10 +95,7 @@ describe('PluginManager', () => { this.commands = { deploy: { usage: 'Deploy to the default infrastructure', - lifecycleEvents: [ - 'resources', - 'functions', - ], + lifecycleEvents: ['resources', 'functions'], options: { resource: { usage: 'The resource you want to deploy (e.g. --resource db)', @@ -110,10 +107,7 @@ describe('PluginManager', () => { commands: { onpremises: { usage: 'Deploy to your On-Premises infrastructure', - lifecycleEvents: [ - 'resources', - 'functions', - ], + lifecycleEvents: ['resources', 'functions'], options: { resource: { usage: 'The resource you want to deploy (e.g. --resource db)', @@ -138,14 +132,14 @@ describe('PluginManager', () => { } functions() { - return new BbPromise((resolve) => { + return new BbPromise(resolve => { this.deployedFunctions = this.deployedFunctions + 1; return resolve(); }); } resources() { - return new BbPromise((resolve) => { + return new BbPromise(resolve => { this.deployedResources = this.deployedResources + 1; return resolve(); }); @@ -157,10 +151,7 @@ describe('PluginManager', () => { this.commands = { deploy: { usage: 'Deploy to the default infrastructure', - lifecycleEvents: [ - 'resources', - 'functions', - ], + lifecycleEvents: ['resources', 'functions'], options: { resource: { usage: 'The resource you want to deploy (e.g. --resource db)', @@ -172,10 +163,7 @@ describe('PluginManager', () => { commands: { onpremises: { usage: 'Deploy to your On-Premises infrastructure', - lifecycleEvents: [ - 'resources', - 'functions', - ], + lifecycleEvents: ['resources', 'functions'], options: { resource: { usage: 'The resource you want to deploy (e.g. --resource db)', @@ -213,10 +201,7 @@ describe('PluginManager', () => { this.commands = { deploy: { usage: 'Deploy to the default infrastructure', - lifecycleEvents: [ - 'resources', - 'functions', - ], + lifecycleEvents: ['resources', 'functions'], options: { resource: { usage: 'The resource you want to deploy (e.g. --resource db)', @@ -228,14 +213,8 @@ describe('PluginManager', () => { commands: { onpremises: { usage: 'Deploy to your On-Premises infrastructure', - lifecycleEvents: [ - 'resources', - 'functions', - ], - aliases: [ - 'on:premise', - 'premise', - ], + lifecycleEvents: ['resources', 'functions'], + aliases: ['on:premise', 'premise'], options: { resource: { usage: 'The resource you want to deploy (e.g. --resource db)', @@ -273,57 +252,36 @@ describe('PluginManager', () => { this.commands = { myep: { type: 'entrypoint', - lifecycleEvents: [ - 'initialize', - 'finalize', - ], + lifecycleEvents: ['initialize', 'finalize'], commands: { // EP, not public command because its parent is decalred as EP mysubep: { - lifecycleEvents: [ - 'initialize', - 'finalize', - ], + lifecycleEvents: ['initialize', 'finalize'], }, // EP that will spawn sub lifecycles spawnep: { - lifecycleEvents: [ - 'event1', - 'event2', - ], + lifecycleEvents: ['event1', 'event2'], }, }, }, // public command mycmd: { - lifecycleEvents: [ - 'run', - ], + lifecycleEvents: ['run'], commands: { // public subcommand mysubcmd: { - lifecycleEvents: [ - 'initialize', - 'finalize', - ], + lifecycleEvents: ['initialize', 'finalize'], }, // command that will spawn sub lifecycles spawncmd: { - lifecycleEvents: [ - 'event1', - 'event2', - ], + lifecycleEvents: ['event1', 'event2'], }, spawnep: { type: 'entrypoint', - lifecycleEvents: [ - 'event1', - 'event2', - ], + lifecycleEvents: ['event1', 'event2'], }, }, }, - }; this.hooks = { @@ -336,14 +294,14 @@ describe('PluginManager', () => { 'mycmd:run': this.run.bind(this), // Event1 spawns mysubcmd, then myep // Event2 spawns mycmd, then mysubep - 'myep:spawnep:event1': () => pluginManager.spawn(['mycmd', 'mysubcmd']) - .then(() => pluginManager.spawn(['myep'])), - 'myep:spawnep:event2': () => pluginManager.spawn(['mycmd']) - .then(() => pluginManager.spawn(['myep', 'mysubep'])), - 'mycmd:spawncmd:event1': () => pluginManager.spawn(['mycmd', 'mysubcmd']) - .then(() => pluginManager.spawn(['myep'])), - 'mycmd:spawncmd:event2': () => pluginManager.spawn(['mycmd']) - .then(() => pluginManager.spawn(['myep', 'mysubep'])), + 'myep:spawnep:event1': () => + pluginManager.spawn(['mycmd', 'mysubcmd']).then(() => pluginManager.spawn(['myep'])), + 'myep:spawnep:event2': () => + pluginManager.spawn(['mycmd']).then(() => pluginManager.spawn(['myep', 'mysubep'])), + 'mycmd:spawncmd:event1': () => + pluginManager.spawn(['mycmd', 'mysubcmd']).then(() => pluginManager.spawn(['myep'])), + 'mycmd:spawncmd:event2': () => + pluginManager.spawn(['mycmd']).then(() => pluginManager.spawn(['myep', 'mysubep'])), }; this.callResult = ''; @@ -387,10 +345,7 @@ describe('PluginManager', () => { commands: { // public command because its children of a container mysubcmd: { - lifecycleEvents: [ - 'event1', - 'event2', - ], + lifecycleEvents: ['event1', 'event2'], }, }, }, @@ -421,18 +376,28 @@ describe('PluginManager', () => { }; } - deprecated() { return; } + deprecated() { + return; + } - untouched() { return; } + untouched() { + return; + } } - beforeEach(function () { // eslint-disable-line prefer-arrow-callback + let restoreEnv; + + beforeEach(() => { + ({ restoreEnv } = overrideEnv({ whitelist: ['APPDATA', 'PATH'] })); serverless = new Serverless(); serverless.cli = new CLI(); + serverless.processedInput = { commands: [], options: {} }; pluginManager = new PluginManager(serverless); pluginManager.serverless.config.servicePath = 'foo'; }); + afterEach(() => restoreEnv()); + describe('#constructor()', () => { it('should set the serverless instance', () => { expect(pluginManager.serverless).to.deep.equal(serverless); @@ -584,13 +549,11 @@ describe('PluginManager', () => { it('should load two plugins that happen to have the same class name', () => { function getFirst() { - return class PluginMock { - }; + return class PluginMock {}; } function getSecond() { - return class PluginMock { - }; + return class PluginMock {}; } const first = getFirst(); @@ -665,27 +628,26 @@ describe('PluginManager', () => { it('should throw an error when trying to load unknown plugin', () => { const servicePlugins = ['ServicePluginMock3', 'ServicePluginMock1']; - expect(() => pluginManager.loadPlugins(servicePlugins)) - .to.throw(serverless.classes.Error); + expect(() => pluginManager.loadPlugins(servicePlugins)).to.throw(serverless.classes.Error); }); it('should log a warning when trying to load unknown plugin with help flag', () => { const consoleLogStub = sinon.stub(pluginManager.serverless.cli, 'log').returns(); const servicePlugins = ['ServicePluginMock3', 'ServicePluginMock1']; - const servicePluginMock1 = new ServicePluginMock1(); pluginManager.setCliOptions({ help: true }); pluginManager.loadPlugins(servicePlugins); - expect(pluginManager.plugins).to.contain(servicePluginMock1); + expect(pluginManager.plugins.some(plugin => plugin instanceof ServicePluginMock1)).to.equal( + true + ); expect(consoleLogStub.calledOnce).to.equal(true); }); it('should throw an error when trying to load a broken plugin (without SLS_DEBUG)', () => { const servicePlugins = ['BrokenPluginMock']; - expect(() => pluginManager.loadPlugins(servicePlugins)) - .to.throw(serverless.classes.Error); + expect(() => pluginManager.loadPlugins(servicePlugins)).to.throw(serverless.classes.Error); }); it('should forward any errors when trying to load a broken plugin (with SLS_DEBUG)', () => { @@ -693,31 +655,25 @@ describe('PluginManager', () => { return BbPromise.try(() => { _.set(process.env, 'SLS_DEBUG', '*'); - expect(() => pluginManager.loadPlugins(servicePlugins)) - .to.throw(/Broken plugin/); - }) - .finally(() => { - _.unset(process.env, 'SLS_DEBUG'); + expect(() => pluginManager.loadPlugins(servicePlugins)).to.throw(/Broken plugin/); }); }); - it('should not throw error when running the plugin commands and given plugins does not exist', - () => { + it('should not throw error when running the plugin commands and given plugins does not exist', () => { const servicePlugins = ['ServicePluginMock3']; const cliCommandsMock = ['plugin']; pluginManager.setCliCommands(cliCommandsMock); - expect(() => pluginManager.loadPlugins(servicePlugins)) - .to.not.throw(serverless.classes.Error); + expect(() => pluginManager.loadPlugins(servicePlugins)).to.not.throw( + serverless.classes.Error + ); }); afterEach(() => { mockRequire.stop('ServicePluginMock1'); }); }); - describe('#asyncPluginInit()', function () { - this.timeout(5000); - + describe('#asyncPluginInit()', () => { it('should call async init on plugins that have it', () => { const plugin1 = new ServicePluginMock1(); plugin1.asyncInit = sinon.stub().returns(BbPromise.resolve()); @@ -728,12 +684,12 @@ describe('PluginManager', () => { }); }); - describe('#loadAllPlugins()', function () { - this.timeout(5000); - - beforeEach(function () { // eslint-disable-line prefer-arrow-callback + describe('#loadAllPlugins()', () => { + beforeEach(() => { + // eslint-disable-line prefer-arrow-callback mockRequire('ServicePluginMock1', ServicePluginMock1); mockRequire('ServicePluginMock2', ServicePluginMock2); + mockRequire('@serverless/enterprise-plugin', EnterprisePluginMock); }); it('should load only core plugins when no service plugins are given', () => { @@ -749,11 +705,15 @@ describe('PluginManager', () => { const servicePlugins = ['ServicePluginMock1', 'ServicePluginMock2']; pluginManager.loadAllPlugins(servicePlugins); - const servicePluginMock1 = new ServicePluginMock1(); - const servicePluginMock2 = new ServicePluginMock2(); - - expect(pluginManager.plugins).to.contain(servicePluginMock1); - expect(pluginManager.plugins).to.contain(servicePluginMock2); + expect(pluginManager.plugins.some(plugin => plugin instanceof ServicePluginMock1)).to.equal( + true + ); + expect(pluginManager.plugins.some(plugin => plugin instanceof ServicePluginMock2)).to.equal( + true + ); + expect(pluginManager.plugins.some(plugin => plugin instanceof EnterprisePluginMock)).to.equal( + true + ); // note: this test will be refactored as the Create plugin will be moved // to another directory expect(pluginManager.plugins.length).to.be.above(2); @@ -772,15 +732,19 @@ describe('PluginManager', () => { // This is the exact same functionality like loadCorePlugins() loadCorePluginsMock(); pluginManager.loadServicePlugins(servicePlugins); + pluginManager.loadEnterprisePlugin(); expect(pluginManager.plugins[0]).to.be.instanceof(Create); expect(pluginManager.plugins[1]).to.be.instanceof(ServicePluginMock1); expect(pluginManager.plugins[2]).to.be.instanceof(ServicePluginMock2); + expect(pluginManager.plugins[3]).to.be.instanceof(EnterprisePluginMock); }); - afterEach(function () { // eslint-disable-line prefer-arrow-callback + afterEach(() => { + // eslint-disable-line prefer-arrow-callback mockRequire.stop('ServicePluginMock1'); mockRequire.stop('ServicePluginMock2'); + mockRequire.stop('@serverless/enterprise-plugin'); }); }); @@ -793,20 +757,24 @@ describe('PluginManager', () => { }); describe('#loadServicePlugins()', () => { - beforeEach(function () { // eslint-disable-line prefer-arrow-callback + beforeEach(() => { + // eslint-disable-line prefer-arrow-callback mockRequire('ServicePluginMock1', ServicePluginMock1); - mockRequire('ServicePluginMock2', ServicePluginMock2); + // Plugins loaded via a relative path should be required relative to the service path + const servicePath = pluginManager.serverless.config.servicePath; + mockRequire(`${servicePath}/RelativePath/ServicePluginMock2`, ServicePluginMock2); }); it('should load the service plugins', () => { - const servicePlugins = ['ServicePluginMock1', 'ServicePluginMock2']; + const servicePlugins = ['ServicePluginMock1', './RelativePath/ServicePluginMock2']; pluginManager.loadServicePlugins(servicePlugins); - const servicePluginMock1 = new ServicePluginMock1(); - const servicePluginMock2 = new ServicePluginMock2(); - - expect(pluginManager.plugins).to.contain(servicePluginMock1); - expect(pluginManager.plugins).to.contain(servicePluginMock2); + expect(pluginManager.plugins.some(plugin => plugin instanceof ServicePluginMock1)).to.equal( + true + ); + expect(pluginManager.plugins.some(plugin => plugin instanceof ServicePluginMock2)).to.equal( + true + ); }); it('should not error if plugins = null', () => { @@ -821,7 +789,8 @@ describe('PluginManager', () => { pluginManager.loadServicePlugins(servicePlugins); }); - afterEach(function () { // eslint-disable-line prefer-arrow-callback + afterEach(() => { + // eslint-disable-line prefer-arrow-callback mockRequire.stop('ServicePluginMock1'); mockRequire.stop('ServicePluginMock2'); }); @@ -900,10 +869,8 @@ describe('PluginManager', () => { }, }, }; - expect(pluginManager.getAliasCommandTarget(['cmd1', 'cmd2'])) - .to.equal('command1'); - expect(pluginManager.getAliasCommandTarget(['cmd1', 'cmd3', 'cmd4'])) - .to.equal('command2'); + expect(pluginManager.getAliasCommandTarget(['cmd1', 'cmd2'])).to.equal('command1'); + expect(pluginManager.getAliasCommandTarget(['cmd1', 'cmd3', 'cmd4'])).to.equal('command2'); }); it('should return undefined if alias does not exist', () => { @@ -919,31 +886,27 @@ describe('PluginManager', () => { }, }, }; - expect(pluginManager.getAliasCommandTarget(['cmd1'])) - .to.be.undefined; - expect(pluginManager.getAliasCommandTarget(['cmd1', 'cmd3'])) - .to.be.undefined; + expect(pluginManager.getAliasCommandTarget(['cmd1'])).to.be.undefined; + expect(pluginManager.getAliasCommandTarget(['cmd1', 'cmd3'])).to.be.undefined; }); }); describe('#createCommandAlias', () => { it('should create an alias for a command', () => { pluginManager.aliases = {}; - expect(pluginManager.createCommandAlias('cmd1:alias2', 'cmd2:cmd3:cmd4')) - .to.not.throw; - expect(pluginManager.createCommandAlias('cmd1:alias2:alias3', 'cmd2:cmd3:cmd5')) - .to.not.throw; - expect(pluginManager.aliases) - .to.deep.equal({ - cmd1: { - alias2: { - command: 'cmd2:cmd3:cmd4', - alias3: { - command: 'cmd2:cmd3:cmd5', - }, + expect(pluginManager.createCommandAlias('cmd1:alias2', 'cmd2:cmd3:cmd4')).to.not.throw; + expect(pluginManager.createCommandAlias('cmd1:alias2:alias3', 'cmd2:cmd3:cmd5')).to.not + .throw; + expect(pluginManager.aliases).to.deep.equal({ + cmd1: { + alias2: { + command: 'cmd2:cmd3:cmd4', + alias3: { + command: 'cmd2:cmd3:cmd5', }, }, - }); + }, + }); }); it('should fail if the alias already exists', () => { @@ -957,22 +920,25 @@ describe('PluginManager', () => { }, }, }; - expect(() => pluginManager.createCommandAlias('cmd1:alias2', 'mycmd')) - .to.throw(/Alias "cmd1:alias2" is already defined/); + expect(() => pluginManager.createCommandAlias('cmd1:alias2', 'mycmd')).to.throw( + /Alias "cmd1:alias2" is already defined/ + ); }); it('should fail if the alias overwrites a command', () => { const synchronousPluginMockInstance = new SynchronousPluginMock(); pluginManager.loadCommands(synchronousPluginMockInstance); - expect(() => pluginManager.createCommandAlias('deploy', 'mycmd')) - .to.throw(/Command "deploy" cannot be overriden/); + expect(() => pluginManager.createCommandAlias('deploy', 'mycmd')).to.throw( + /Command "deploy" cannot be overriden/ + ); }); it('should fail if the alias overwrites the very own command', () => { const synchronousPluginMockInstance = new SynchronousPluginMock(); synchronousPluginMockInstance.commands.deploy.commands.onpremises.aliases = ['deploy']; - expect(() => pluginManager.loadCommands(synchronousPluginMockInstance)) - .to.throw(/Command "deploy" cannot be overriden/); + expect(() => pluginManager.loadCommands(synchronousPluginMockInstance)).to.throw( + /Command "deploy" cannot be overriden/ + ); }); }); }); @@ -989,9 +955,7 @@ describe('PluginManager', () => { pluginManager.loadCommands({ commands: { deploy: { - lifecycleEvents: [ - 'one', - ], + lifecycleEvents: ['one'], options: { foo: {}, }, @@ -1002,24 +966,22 @@ describe('PluginManager', () => { pluginManager.loadCommands({ commands: { deploy: { - lifecycleEvents: [ - 'one', - 'two', - ], + lifecycleEvents: ['one', 'two'], options: { bar: {}, }, commands: { - fn: { - }, + fn: {}, }, }, }, }); - expect(pluginManager.commands.deploy).to.have.property('options') + expect(pluginManager.commands.deploy) + .to.have.property('options') .that.has.all.keys('foo', 'bar'); - expect(pluginManager.commands.deploy).to.have.property('lifecycleEvents') + expect(pluginManager.commands.deploy) + .to.have.property('lifecycleEvents') .that.is.an('array') .that.deep.equals(['one', 'two']); expect(pluginManager.commands.deploy.commands).to.have.property('fn'); @@ -1033,8 +995,9 @@ describe('PluginManager', () => { }; const synchronousPluginMockInstance = new SynchronousPluginMock(); - expect(() => pluginManager.loadCommands(synchronousPluginMockInstance)) - .to.throw(/Command "deploy" cannot override an existing alias/); + expect(() => pluginManager.loadCommands(synchronousPluginMockInstance)).to.throw( + /Command "deploy" cannot override an existing alias/ + ); }); it('should log the alias when SLS_DEBUG is set', () => { @@ -1043,7 +1006,6 @@ describe('PluginManager', () => { synchronousPluginMockInstance.commands.deploy.aliases = ['info']; _.set(process.env, 'SLS_DEBUG', '*'); pluginManager.loadCommands(synchronousPluginMockInstance); - _.unset(process.env, 'SLS_DEBUG'); expect(consoleLogStub).to.have.been.calledWith(' -> @info'); }); }); @@ -1063,24 +1025,23 @@ describe('PluginManager', () => { afterEach(() => { pluginManager.deprecatedEvents = {}; pluginManager.serverless.cli.log.restore(); - delete process.env.SLS_DEBUG; }); it('should replace deprecated events with the new ones', () => { - delete process.env.SLS_DEBUG; pluginManager.loadHooks(deprecatedPluginInstance); - expect(pluginManager.hooks['deprecated:deprecated']) - .to.equal(undefined); - expect(pluginManager.hooks['new:new'][0].pluginName) - .to.equal('DeprecatedLifecycleEventsPluginMock'); - expect(pluginManager.hooks['untouched:untouched'][0].pluginName) - .to.equal('DeprecatedLifecycleEventsPluginMock'); + expect(pluginManager.hooks['deprecated:deprecated']).to.equal(undefined); + expect(pluginManager.hooks['new:new'][0].pluginName).to.equal( + 'DeprecatedLifecycleEventsPluginMock' + ); + expect(pluginManager.hooks['untouched:untouched'][0].pluginName).to.equal( + 'DeprecatedLifecycleEventsPluginMock' + ); expect(consoleLogStub.calledOnce).to.equal(false); }); it('should log a debug message about deprecated when using SLS_DEBUG', () => { - process.env.SLS_DEBUG = true; + process.env.SLS_DEBUG = '1'; pluginManager.loadHooks(deprecatedPluginInstance); expect(consoleLogStub.calledOnce).to.equal(true); @@ -1088,7 +1049,8 @@ describe('PluginManager', () => { }); describe('#getEvents()', () => { - beforeEach(function () { // eslint-disable-line prefer-arrow-callback + beforeEach(() => { + // eslint-disable-line prefer-arrow-callback pluginManager.addPlugin(SynchronousPluginMock); }); @@ -1118,12 +1080,15 @@ describe('PluginManager', () => { }); describe('#getHooks()', () => { - beforeEach(function () { // eslint-disable-line prefer-arrow-callback + beforeEach(() => { + // eslint-disable-line prefer-arrow-callback pluginManager.addPlugin(SynchronousPluginMock); }); it('should get hooks for an event with some registered', () => { - expect(pluginManager.getHooks(['deploy:functions'])).to.be.an('Array').with.length(1); + expect(pluginManager.getHooks(['deploy:functions'])) + .to.be.an('Array') + .with.length(1); }); it('should have the plugin name and function on the hook', () => { @@ -1133,16 +1098,21 @@ describe('PluginManager', () => { }); it('should not get hooks for an event that does not have any', () => { - expect(pluginManager.getHooks(['deploy:resources'])).to.be.an('Array').with.length(0); + expect(pluginManager.getHooks(['deploy:resources'])) + .to.be.an('Array') + .with.length(0); }); it('should accept a single event in place of an array', () => { - expect(pluginManager.getHooks('deploy:functions')).to.be.an('Array').with.length(1); + expect(pluginManager.getHooks('deploy:functions')) + .to.be.an('Array') + .with.length(1); }); }); describe('#getPlugins()', () => { - beforeEach(function () { // eslint-disable-line prefer-arrow-callback + beforeEach(() => { + // eslint-disable-line prefer-arrow-callback mockRequire('ServicePluginMock1', ServicePluginMock1); mockRequire('ServicePluginMock2', ServicePluginMock2); }); @@ -1155,7 +1125,8 @@ describe('PluginManager', () => { expect(pluginManager.getPlugins()[1]).to.be.instanceof(ServicePluginMock2); }); - afterEach(function () { // eslint-disable-line prefer-arrow-callback + afterEach(() => { + // eslint-disable-line prefer-arrow-callback mockRequire.stop('ServicePluginMock1'); mockRequire.stop('ServicePluginMock2'); }); @@ -1165,29 +1136,33 @@ describe('PluginManager', () => { it('should find commands', () => { pluginManager.addPlugin(EntrypointPluginMock); - expect(() => pluginManager.validateCommand(['mycmd', 'mysubcmd'])) - .to.not.throw(serverless.classes.Error); + expect(() => pluginManager.validateCommand(['mycmd', 'mysubcmd'])).to.not.throw( + serverless.classes.Error + ); }); it('should find container children commands', () => { pluginManager.addPlugin(ContainerPluginMock); - expect(() => pluginManager.validateCommand(['mycontainer', 'mysubcmd'])) - .to.not.throw(serverless.classes.Error); + expect(() => pluginManager.validateCommand(['mycontainer', 'mysubcmd'])).to.not.throw( + serverless.classes.Error + ); }); it('should throw on entrypoints', () => { pluginManager.addPlugin(EntrypointPluginMock); - expect(() => pluginManager.validateCommand(['myep', 'mysubep'])) - .to.throw(/command ".*" not found/); + expect(() => pluginManager.validateCommand(['myep', 'mysubep'])).to.throw( + /command ".*" not found/ + ); }); it('should throw on container', () => { pluginManager.addPlugin(ContainerPluginMock); - expect(() => pluginManager.validateCommand(['mycontainer'])) - .to.throw(/command ".*" not found/); + expect(() => pluginManager.validateCommand(['mycontainer'])).to.throw( + /command ".*" not found/ + ); }); }); @@ -1277,7 +1252,9 @@ describe('PluginManager', () => { const foo = pluginManagerInstance.commands.foo; - expect(() => { pluginManager.validateServerlessConfigDependency(foo); }).to.throw(Error); + expect(() => { + pluginManager.validateServerlessConfigDependency(foo); + }).to.throw(Error); }); it('should throw an error if configDependent is true and config is an empty string', () => { @@ -1291,7 +1268,9 @@ describe('PluginManager', () => { const foo = pluginManagerInstance.commands.foo; - expect(() => { pluginManager.validateServerlessConfigDependency(foo); }).to.throw(Error); + expect(() => { + pluginManager.validateServerlessConfigDependency(foo); + }).to.throw(Error); }); it('should load if the configDependent property is true and config exists', () => { @@ -1334,8 +1313,12 @@ describe('PluginManager', () => { const foo = pluginManager.commands.foo; const bar = pluginManager.commands.bar; - expect(() => { pluginManager.validateOptions(foo); }).to.throw(Error); - expect(() => { pluginManager.validateOptions(bar); }).to.throw(Error); + expect(() => { + pluginManager.validateOptions(foo); + }).to.throw(Error); + expect(() => { + pluginManager.validateOptions(bar); + }).to.throw(Error); }); it('should throw an error if a customValidation is not met', () => { @@ -1355,7 +1338,9 @@ describe('PluginManager', () => { }; const command = pluginManager.commands.foo; - expect(() => { pluginManager.validateOptions(command); }).to.throw(Error); + expect(() => { + pluginManager.validateOptions(command); + }).to.throw(Error); }); it('should succeeds if a custom regex matches in a plain commands object', () => { @@ -1375,7 +1360,9 @@ describe('PluginManager', () => { }; const commandsArray = ['foo']; - expect(() => { pluginManager.validateOptions(commandsArray); }).to.not.throw(Error); + expect(() => { + pluginManager.validateOptions(commandsArray); + }).to.not.throw(Error); }); }); @@ -1385,7 +1372,9 @@ describe('PluginManager', () => { const commandsArray = ['foo']; - expect(() => { pluginManager.run(commandsArray); }).to.throw(Error); + expect(() => { + pluginManager.run(commandsArray); + }).to.throw(Error); }); it('should throw an error when the given command is an entrypoint', () => { @@ -1393,7 +1382,9 @@ describe('PluginManager', () => { const commandsArray = ['myep']; - expect(() => { pluginManager.run(commandsArray); }).to.throw(Error); + expect(() => { + pluginManager.run(commandsArray); + }).to.throw(Error); }); it('should throw an error when the given command is a container', () => { @@ -1401,7 +1392,9 @@ describe('PluginManager', () => { const commandsArray = ['mycontainer']; - expect(() => { pluginManager.run(commandsArray); }).to.throw(Error); + expect(() => { + pluginManager.run(commandsArray); + }).to.throw(Error); }); it('should NOT throw an error when the given command is a child of a container', () => { @@ -1409,7 +1402,9 @@ describe('PluginManager', () => { const commandsArray = ['mycontainer', 'mysubcmd']; - expect(() => { pluginManager.run(commandsArray); }).to.not.throw(Error); + expect(() => { + pluginManager.run(commandsArray); + }).to.not.throw(Error); }); it('should throw an error when the given command is a child of an entrypoint', () => { @@ -1417,7 +1412,9 @@ describe('PluginManager', () => { const commandsArray = ['mysubcmd']; - expect(() => { pluginManager.run(commandsArray); }).to.throw(Error); + expect(() => { + pluginManager.run(commandsArray); + }).to.throw(Error); }); it('should show warning if in debug mode and the given command has no hooks', () => { @@ -1438,7 +1435,6 @@ describe('PluginManager', () => { return pluginManager.run(commandsArray).then(() => { expect(consoleLogStub.called).is.equal(true); pluginManager.serverless.cli.log.restore(); - process.env.SLS_DEBUG = undefined; }); }); @@ -1448,11 +1444,7 @@ describe('PluginManager', () => { this.commands = { run: { usage: 'Pushes the current hook status on the hookStatus array', - lifecycleEvents: [ - 'beforeHookStatus', - 'midHookStatus', - 'afterHookStatus', - ], + lifecycleEvents: ['beforeHookStatus', 'midHookStatus', 'afterHookStatus'], }, }; @@ -1481,64 +1473,66 @@ describe('PluginManager', () => { pluginManager.addPlugin(CorrectHookOrderPluginMock); const commandsArray = ['run']; - return pluginManager.run(commandsArray) - .then(() => { - expect(pluginManager.plugins[0].hookStatus[0]).to.equal('before'); - expect(pluginManager.plugins[0].hookStatus[1]).to.equal('mid'); - expect(pluginManager.plugins[0].hookStatus[2]).to.equal('after'); - }); + return pluginManager.run(commandsArray).then(() => { + expect(pluginManager.plugins[0].hookStatus[0]).to.equal('before'); + expect(pluginManager.plugins[0].hookStatus[1]).to.equal('mid'); + expect(pluginManager.plugins[0].hookStatus[2]).to.equal('after'); + }); }); describe('when using a synchronous hook function', () => { - beforeEach(function () { // eslint-disable-line prefer-arrow-callback + beforeEach(() => { + // eslint-disable-line prefer-arrow-callback pluginManager.addPlugin(SynchronousPluginMock); }); describe('when running a simple command', () => { it('should run a simple command', () => { const commandsArray = ['deploy']; - return pluginManager.run(commandsArray) - .then(() => expect(pluginManager.plugins[0].deployedFunctions) - .to.equal(1)); + return pluginManager + .run(commandsArray) + .then(() => expect(pluginManager.plugins[0].deployedFunctions).to.equal(1)); }); }); describe('when running a nested command', () => { it('should run the nested command', () => { const commandsArray = ['deploy', 'onpremises']; - return pluginManager.run(commandsArray) - .then(() => expect(pluginManager.plugins[0].deployedResources) - .to.equal(1)); + return pluginManager + .run(commandsArray) + .then(() => expect(pluginManager.plugins[0].deployedResources).to.equal(1)); }); }); }); describe('when using a promise based hook function', () => { - beforeEach(function () { // eslint-disable-line prefer-arrow-callback + beforeEach(() => { + // eslint-disable-line prefer-arrow-callback pluginManager.addPlugin(PromisePluginMock); }); describe('when running a simple command', () => { it('should run the simple command', () => { const commandsArray = ['deploy']; - return pluginManager.run(commandsArray) - .then(() => expect(pluginManager.plugins[0].deployedFunctions) - .to.equal(1)); + return pluginManager + .run(commandsArray) + .then(() => expect(pluginManager.plugins[0].deployedFunctions).to.equal(1)); }); }); describe('when running a nested command', () => { it('should run the nested command', () => { const commandsArray = ['deploy', 'onpremises']; - return pluginManager.run(commandsArray) - .then(() => expect(pluginManager.plugins[0].deployedResources) - .to.equal(1)); + return pluginManager + .run(commandsArray) + .then(() => expect(pluginManager.plugins[0].deployedResources).to.equal(1)); }); }); }); describe('when using provider specific plugins', () => { - beforeEach(function () { // eslint-disable-line prefer-arrow-callback + beforeEach(() => { + // eslint-disable-line prefer-arrow-callback pluginManager.serverless.service.provider.name = 'provider1'; pluginManager.addPlugin(Provider1PluginMock); @@ -1565,13 +1559,11 @@ describe('PluginManager', () => { const commandsArray = ['mycmd', 'spawncmd']; - return pluginManager.run(commandsArray) - .then(() => { - expect(pluginManager.plugins[0].callResult) - .to.equal( - '>subInitialize>subFinalize>initialize>finalize>run>subEPInitialize>subEPFinalize' - ); - }); + return pluginManager.run(commandsArray).then(() => { + expect(pluginManager.plugins[0].callResult).to.equal( + '>subInitialize>subFinalize>initialize>finalize>run>subEPInitialize>subEPFinalize' + ); + }); }); }); @@ -1581,20 +1573,21 @@ describe('PluginManager', () => { const commands = pluginManager.getCommands(); expect(commands).to.have.a.property('mycmd'); - expect(commands).to.have.a.deep.property('mycmd.commands.mysubcmd'); - expect(commands).to.have.a.deep.property('mycmd.commands.spawncmd'); + expect(commands).to.have.a.nested.property('mycmd.commands.mysubcmd'); + expect(commands).to.have.a.nested.property('mycmd.commands.spawncmd'); // Check for omitted entrypoints expect(commands).to.not.have.a.property('myep'); - expect(commands).to.not.have.a.deep.property('myep.commands.mysubep'); - expect(commands).to.not.have.a.deep.property('mycmd.commands.spawnep'); + expect(commands).to.not.have.a.nested.property('myep.commands.mysubep'); + expect(commands).to.not.have.a.nested.property('mycmd.commands.spawnep'); }); it('should return aliases', () => { pluginManager.addPlugin(AliasPluginMock); const commands = pluginManager.getCommands(); - expect(commands).to.have.a.property('on') - .that.has.a.deep.property('commands.premise'); + expect(commands) + .to.have.a.property('on') + .that.has.a.nested.property('commands.premise'); expect(commands).to.have.a.property('premise'); }); }); @@ -1605,9 +1598,7 @@ describe('PluginManager', () => { pluginManager.serverless.cli.loadedCommands = { create: { usage: 'Create new Serverless service', - lifecycleEvents: [ - 'create', - ], + lifecycleEvents: ['create'], options: { template: { usage: 'Template for the service. Available templates: ", "aws-nodejs", "..."', @@ -1620,10 +1611,7 @@ describe('PluginManager', () => { deploy: { usage: 'Deploy a Serverless service', configDependent: true, - lifecycleEvents: [ - 'cleanup', - 'initialize', - ], + lifecycleEvents: ['cleanup', 'initialize'], options: { conceal: { usage: 'Hide secrets from the output (e.g. API Gateway key values)', @@ -1638,11 +1626,7 @@ describe('PluginManager', () => { commands: { function: { usage: 'Deploy a single function from the service', - lifecycleEvents: [ - 'initialize', - 'packageFunction', - 'deploy', - ], + lifecycleEvents: ['initialize', 'packageFunction', 'deploy'], options: { function: { usage: 'Name of the function', @@ -1655,17 +1639,13 @@ describe('PluginManager', () => { }, list: { usage: 'List deployed version of your Serverless Service', - lifecycleEvents: [ - 'log', - ], + lifecycleEvents: ['log'], key: 'deploy:list', pluginName: 'Deploy', commands: { functions: { usage: 'List all the deployed functions and their versions', - lifecycleEvents: [ - 'log', - ], + lifecycleEvents: ['log'], key: 'deploy:list:functions', pluginName: 'Deploy', }, @@ -1675,26 +1655,30 @@ describe('PluginManager', () => { }, }; }); - it('should give a suggestion for an unknown command', (done) => { + it('should give a suggestion for an unknown command', done => { try { pluginManager.getCommand(['creet']); done('Test failed. Expected an error to be thrown'); } catch (error) { expect(error.name).to.eql('ServerlessError'); - expect(error.message).to.eql('Serverless command "creet" not found. ' + - 'Did you mean "create"? Run "serverless help" for a list of all available commands.'); + expect(error.message).to.eql( + 'Serverless command "creet" not found. ' + + 'Did you mean "create"? Run "serverless help" for a list of all available commands.' + ); done(); } }); - it('should not give a suggestion for valid top level command', (done) => { + it('should not give a suggestion for valid top level command', done => { try { pluginManager.getCommand(['deploy', 'function-misspelled']); done('Test failed. Expected an error to be thrown'); } catch (error) { expect(error.name).to.eql('ServerlessError'); - expect(error.message).to.eql('"function-misspelled" is not a valid sub command. ' - + 'Run "serverless deploy" to see a more helpful error message for this command.'); + expect(error.message).to.eql( + '"function-misspelled" is not a valid sub command. ' + + 'Run "serverless deploy" to see a more helpful error message for this command.' + ); done(); } }); @@ -1706,12 +1690,16 @@ describe('PluginManager', () => { const commandsArray = ['foo']; - expect(() => { pluginManager.spawn(commandsArray); }).to.throw(Error); + expect(() => { + pluginManager.spawn(commandsArray); + }).to.throw(Error); }); it('should show warning in debug mode and when the given command has no hooks', () => { const consoleLogStub = sinon.stub(pluginManager.serverless.cli, 'log').returns(); + process.env.SLS_DEBUG = '*'; + class HooklessPlugin { constructor() { this.commands = { @@ -1727,7 +1715,6 @@ describe('PluginManager', () => { return pluginManager.run(commandsArray).then(() => { expect(consoleLogStub.called).is.equal(true); pluginManager.serverless.cli.log.restore(); - process.env.SLS_DEBUG = undefined; }); }); @@ -1737,10 +1724,9 @@ describe('PluginManager', () => { const commandsArray = ['mycmd']; - return pluginManager.spawn(commandsArray) - .then(() => { - expect(pluginManager.plugins[0].callResult).to.equal('>run'); - }); + return pluginManager.spawn(commandsArray).then(() => { + expect(pluginManager.plugins[0].callResult).to.equal('>run'); + }); }); it('should spawn nested commands', () => { @@ -1748,10 +1734,9 @@ describe('PluginManager', () => { const commandsArray = ['mycmd', 'mysubcmd']; - return pluginManager.spawn(commandsArray) - .then(() => { - expect(pluginManager.plugins[0].callResult).to.equal('>subInitialize>subFinalize'); - }); + return pluginManager.spawn(commandsArray).then(() => { + expect(pluginManager.plugins[0].callResult).to.equal('>subInitialize>subFinalize'); + }); }); it('should terminate the hook chain if requested', () => { @@ -1775,9 +1760,7 @@ describe('PluginManager', () => { const commandsArray = ['mycontainer']; - return expect( - () => pluginManager.spawn(commandsArray) - ).to.throw(/command ".*" not found/); + return expect(() => pluginManager.spawn(commandsArray)).to.throw(/command ".*" not found/); }); it('should spawn nested commands', () => { @@ -1785,10 +1768,9 @@ describe('PluginManager', () => { const commandsArray = ['mycontainer', 'mysubcmd']; - return pluginManager.spawn(commandsArray) - .then(() => { - expect(pluginManager.plugins[0].callResult).to.equal('>mysubcmdEvent1>mysubcmdEvent2'); - }); + return pluginManager.spawn(commandsArray).then(() => { + expect(pluginManager.plugins[0].callResult).to.equal('>mysubcmdEvent1>mysubcmdEvent2'); + }); }); }); @@ -1798,10 +1780,9 @@ describe('PluginManager', () => { const commandsArray = ['myep']; - return pluginManager.spawn(commandsArray) - .then(() => { - expect(pluginManager.plugins[0].callResult).to.equal('>initialize>finalize'); - }); + return pluginManager.spawn(commandsArray).then(() => { + expect(pluginManager.plugins[0].callResult).to.equal('>initialize>finalize'); + }); }); it('should spawn nested entrypoints', () => { @@ -1809,30 +1790,26 @@ describe('PluginManager', () => { const commandsArray = ['myep', 'mysubep']; - return pluginManager.spawn(commandsArray) - .then(() => { - expect(pluginManager.plugins[0].callResult).to.equal('>subEPInitialize>subEPFinalize'); - }); + return pluginManager.spawn(commandsArray).then(() => { + expect(pluginManager.plugins[0].callResult).to.equal('>subEPInitialize>subEPFinalize'); + }); }); describe('with string formatted syntax', () => { it('should succeed', () => { pluginManager.addPlugin(EntrypointPluginMock); - return pluginManager.spawn('myep') - .then(() => { - expect(pluginManager.plugins[0].callResult).to.equal('>initialize>finalize'); - }); + return pluginManager.spawn('myep').then(() => { + expect(pluginManager.plugins[0].callResult).to.equal('>initialize>finalize'); + }); }); it('should spawn nested entrypoints', () => { pluginManager.addPlugin(EntrypointPluginMock); - return pluginManager.spawn('myep:mysubep') - .then(() => { - expect(pluginManager.plugins[0].callResult) - .to.equal('>subEPInitialize>subEPFinalize'); - }); + return pluginManager.spawn('myep:mysubep').then(() => { + expect(pluginManager.plugins[0].callResult).to.equal('>subEPInitialize>subEPFinalize'); + }); }); }); }); @@ -1842,13 +1819,11 @@ describe('PluginManager', () => { const commandsArray = ['myep', 'spawnep']; - return pluginManager.spawn(commandsArray) - .then(() => { - expect(pluginManager.plugins[0].callResult) - .to.equal( - '>subInitialize>subFinalize>initialize>finalize>run>subEPInitialize>subEPFinalize' - ); - }); + return pluginManager.spawn(commandsArray).then(() => { + expect(pluginManager.plugins[0].callResult).to.equal( + '>subInitialize>subFinalize>initialize>finalize>run>subEPInitialize>subEPFinalize' + ); + }); }); }); @@ -1856,8 +1831,9 @@ describe('PluginManager', () => { const cwd = process.cwd(); let serviceDir; let tmpDir; - beforeEach(function () { // eslint-disable-line prefer-arrow-callback - tmpDir = testUtils.getTmpDirPath(); + beforeEach(() => { + // eslint-disable-line prefer-arrow-callback + tmpDir = getTmpDirPath(); serviceDir = path.join(tmpDir, 'service'); fse.mkdirsSync(serviceDir); process.chdir(serviceDir); @@ -1866,16 +1842,17 @@ describe('PluginManager', () => { it('should load plugins from .serverless_plugins', () => { const localPluginDir = path.join(serviceDir, '.serverless_plugins', 'local-plugin'); - testUtils.installPlugin(localPluginDir, SynchronousPluginMock); + installPlugin(localPluginDir, SynchronousPluginMock); pluginManager.loadServicePlugins(['local-plugin']); expect(pluginManager.plugins).to.satisfy(plugins => - plugins.some(plugin => plugin.constructor.name === 'SynchronousPluginMock')); + plugins.some(plugin => plugin.constructor.name === 'SynchronousPluginMock') + ); }); it('should load plugins from custom folder', () => { const localPluginDir = path.join(serviceDir, 'serverless-plugins-custom', 'local-plugin'); - testUtils.installPlugin(localPluginDir, SynchronousPluginMock); + installPlugin(localPluginDir, SynchronousPluginMock); pluginManager.loadServicePlugins({ localPath: path.join(serviceDir, 'serverless-plugins-custom'), @@ -1884,13 +1861,14 @@ describe('PluginManager', () => { // Had to use contructor.name because the class will be loaded via // require and the reference will not match with SynchronousPluginMock expect(pluginManager.plugins).to.satisfy(plugins => - plugins.some(plugin => plugin.constructor.name === 'SynchronousPluginMock')); + plugins.some(plugin => plugin.constructor.name === 'SynchronousPluginMock') + ); }); it('should load plugins from custom folder outside of serviceDir', () => { serviceDir = path.join(tmpDir, 'serverless-plugins-custom'); const localPluginDir = path.join(serviceDir, 'local-plugin'); - testUtils.installPlugin(localPluginDir, SynchronousPluginMock); + installPlugin(localPluginDir, SynchronousPluginMock); pluginManager.loadServicePlugins({ localPath: serviceDir, @@ -1898,11 +1876,13 @@ describe('PluginManager', () => { }); // Had to use contructor.name because the class will be loaded via // require and the reference will not match with SynchronousPluginMock - expect(pluginManager.plugins).to.satisfy(plugins => plugins.some(plugin => - plugin.constructor.name === 'SynchronousPluginMock')); + expect(pluginManager.plugins).to.satisfy(plugins => + plugins.some(plugin => plugin.constructor.name === 'SynchronousPluginMock') + ); }); - afterEach(function () { // eslint-disable-line prefer-arrow-callback + afterEach(() => { + // eslint-disable-line prefer-arrow-callback process.chdir(cwd); try { fse.removeSync(tmpDir); @@ -1912,7 +1892,7 @@ describe('PluginManager', () => { }); }); - describe('Plugin / CLI integration', function () { + describe('Plugin / CLI integration', function() { this.timeout(0); const cwd = process.cwd(); @@ -1920,45 +1900,59 @@ describe('PluginManager', () => { let serviceDir; let serverlessExec; - beforeEach(function () { // eslint-disable-line prefer-arrow-callback + beforeEach(() => { + // eslint-disable-line prefer-arrow-callback serverlessInstance = new Serverless(); - serverlessInstance.init(); + return serverlessInstance.init().then(() => { + // Cannot rely on shebang in severless.js to invoke script using NodeJS on Windows. + const execPrefix = os.platform() === 'win32' ? 'node ' : ''; + serverlessExec = + execPrefix + + path.join(serverlessInstance.config.serverlessPath, '..', 'bin', 'serverless'); + const tmpDir = getTmpDirPath(); + serviceDir = path.join(tmpDir, 'service'); + fse.mkdirsSync(serviceDir); + process.chdir(serviceDir); - // Cannot rely on shebang in severless.js to invoke script using NodeJS on Windows. - const execPrefix = os.platform() === 'win32' ? 'node ' : ''; - serverlessExec = execPrefix + path.join(serverlessInstance.config.serverlessPath, - '..', 'bin', 'serverless'); - const tmpDir = testUtils.getTmpDirPath(); - serviceDir = path.join(tmpDir, 'service'); - fse.mkdirsSync(serviceDir); - process.chdir(serviceDir); - - execSync(`${serverlessExec} create --template aws-nodejs`); + try { + execSync(`${serverlessExec} create --template aws-nodejs`); + } catch (error) { + // Expose process output in case of crash + process.stdout.write(error.stdout); + process.stderr.write(error.stderr); + throw error; + } + }); }); it('should expose a working integration between the CLI and the plugin system', () => { - expect(serverlessInstance.utils - .fileExistsSync(path.join(serviceDir, 'serverless.yml'))).to.equal(true); - expect(serverlessInstance.utils - .fileExistsSync(path.join(serviceDir, 'handler.js'))).to.equal(true); + expect( + serverlessInstance.utils.fileExistsSync(path.join(serviceDir, 'serverless.yml')) + ).to.equal(true); + expect(serverlessInstance.utils.fileExistsSync(path.join(serviceDir, 'handler.js'))).to.equal( + true + ); }); it('should load plugins relatively to the working directory', () => { const localPluginDir = path.join(serviceDir, 'node_modules', 'local-plugin'); const parentPluginDir = path.join(serviceDir, '..', 'node_modules', 'parent-plugin'); - testUtils.installPlugin(localPluginDir, SynchronousPluginMock); - testUtils.installPlugin(parentPluginDir, PromisePluginMock); + installPlugin(localPluginDir, SynchronousPluginMock); + installPlugin(parentPluginDir, PromisePluginMock); - fs.appendFileSync(path.join(serviceDir, 'serverless.yml'), - 'plugins:\n - local-plugin\n - parent-plugin'); + fs.appendFileSync( + path.join(serviceDir, 'serverless.yml'), + 'plugins:\n - local-plugin\n - parent-plugin' + ); const output = execSync(serverlessExec); - const stringifiedOutput = (new Buffer(output, 'base64').toString()); + const stringifiedOutput = new Buffer(output, 'base64').toString(); expect(stringifiedOutput).to.contain('SynchronousPluginMock'); expect(stringifiedOutput).to.contain('PromisePluginMock'); }); - afterEach(function () { // eslint-disable-line prefer-arrow-callback + afterEach(() => { + // eslint-disable-line prefer-arrow-callback process.chdir(cwd); try { fse.removeSync(serviceDir); diff --git a/lib/classes/PromiseTracker.js b/lib/classes/PromiseTracker.js index 9520d705a..0abaa46bb 100644 --- a/lib/classes/PromiseTracker.js +++ b/lib/classes/PromiseTracker.js @@ -18,16 +18,19 @@ class PromiseTracker { report() { const delta = Date.now() - this.startTime; const pending = this.getPending(); - logInfo([ - '##########################################################################################', - `# ${delta}: ${this.getSettled().length} of ${this.getAll().length} promises have settled`, - `# ${delta}: ${pending.length} unsettled promises:`, - ].concat( - pending.map((promise) => `# ${delta}: ${promise.waitList}`) - ).concat([ - '# This can result from latent connections but may represent a cyclic variable dependency', - '##########################################################################################', - ]).join('\n ')); + logInfo( + [ + '##########################################################################################', + `# ${delta}: ${this.getSettled().length} of ${this.getAll().length} promises have settled`, + `# ${delta}: ${pending.length} unsettled promises:`, + ] + .concat(pending.map(promise => `# ${delta}: ${promise.waitList}`)) + .concat([ + '# This can result from latent connections but may represent a cyclic variable dependency', + '##########################################################################################', + ]) + .join('\n ') + ); } stop() { clearInterval(this.interval); @@ -37,9 +40,15 @@ class PromiseTracker { const promise = prms; promise.waitList = `${variable} waited on by: ${specifier}`; promise.state = 'pending'; - promise.then( // creates a promise with the following effects but that we otherwise ignore - () => { promise.state = 'resolved'; }, - () => { promise.state = 'rejected'; }); + promise.then( + // creates a promise with the following effects but that we otherwise ignore + () => { + promise.state = 'resolved'; + }, + () => { + promise.state = 'rejected'; + } + ); this.promiseList.push(promise); this.promiseMap[variable] = promise; return promise; @@ -52,9 +61,15 @@ class PromiseTracker { promise.waitList += ` ${specifier}`; return promise; } - getPending() { return this.promiseList.filter(p => (p.state === 'pending')); } - getSettled() { return this.promiseList.filter(p => (p.state !== 'pending')); } - getAll() { return this.promiseList; } + getPending() { + return this.promiseList.filter(p => p.state === 'pending'); + } + getSettled() { + return this.promiseList.filter(p => p.state !== 'pending'); + } + getAll() { + return this.promiseList; + } } module.exports = PromiseTracker; diff --git a/lib/classes/PromiseTracker.test.js b/lib/classes/PromiseTracker.test.js index 0202770d8..5635a6a86 100644 --- a/lib/classes/PromiseTracker.test.js +++ b/lib/classes/PromiseTracker.test.js @@ -26,6 +26,7 @@ describe('PromiseTracker', () => { promiseTracker.add('foo', BbPromise.resolve(), '${foo:}'); promiseTracker.add('foo', BbPromise.delay(10), '${foo:}'); promiseTracker.report(); // shouldn't throw + return Promise.all(promiseTracker.getAll()); }); it('reports no pending promises when none have been added', () => { const promises = promiseTracker.getPending(); @@ -34,14 +35,20 @@ describe('PromiseTracker', () => { }); it('reports one pending promise when one has been added', () => { let resolve; - const promise = new BbPromise((rslv) => { resolve = rslv; }); + const promise = new BbPromise(rslv => { + resolve = rslv; + }); promiseTracker.add('foo', promise, '${foo:}'); - return BbPromise.delay(1).then(() => { - const promises = promiseTracker.getPending(); - expect(promises).to.be.an.instanceof(Array); - expect(promises.length).to.equal(1); - expect(promises[0]).to.equal(promise); - }).then(() => { resolve(); }); + return BbPromise.delay(1) + .then(() => { + const promises = promiseTracker.getPending(); + expect(promises).to.be.an.instanceof(Array); + expect(promises.length).to.equal(1); + expect(promises[0]).to.equal(promise); + }) + .then(() => { + resolve(); + }); }); it('reports no settled promises when none have been added', () => { const promises = promiseTracker.getSettled(); @@ -56,6 +63,7 @@ describe('PromiseTracker', () => { expect(promises).to.be.an.instanceof(Array); expect(promises.length).to.equal(1); expect(promises[0]).to.equal(promise); + return Promise.all(promiseTracker.getAll()); }); it('reports no promises when none have been added', () => { const promises = promiseTracker.getAll(); diff --git a/lib/classes/Service.js b/lib/classes/Service.js index 7bd1a2ed9..86ff9115a 100644 --- a/lib/classes/Service.js +++ b/lib/classes/Service.js @@ -5,6 +5,7 @@ const path = require('path'); const _ = require('lodash'); const BbPromise = require('bluebird'); const semver = require('semver'); +const serverlessConfigFileUtils = require('../utils/getServerlessConfigFile'); const validAPIGatewayStageNamePattern = /^[-_a-zA-Z0-9]+$/; @@ -20,7 +21,6 @@ class Service { this.serviceObject = null; this.provider = { stage: 'dev', - region: 'us-east-1', variableSyntax: '\\${([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*]+?)}', }; this.custom = {}; @@ -36,8 +36,8 @@ class Service { load(rawOptions) { const that = this; const options = rawOptions || {}; - options.stage = options.stage || options.s; - options.region = options.region || options.r; + if (!options.stage && options.s) options.stage = options.s; + if (!options.region && options.r) options.region = options.r; const servicePath = this.serverless.config.servicePath; // skip if the service path is not found @@ -46,54 +46,17 @@ class Service { return BbPromise.resolve(); } - // List of supported service filename variants. - // The order defines the precedence. - const serviceFilenames = [ - 'serverless.yaml', - 'serverless.yml', - 'serverless.json', - 'serverless.js', - ]; - - const serviceFilePaths = _.map(serviceFilenames, filename => path.join(servicePath, filename)); - const serviceFileIndex = _.findIndex(serviceFilePaths, - filename => this.serverless.utils.fileExistsSync(filename) - ); - - // Set the filename if found, otherwise set the preferred variant. - const serviceFilePath = serviceFileIndex !== -1 ? - serviceFilePaths[serviceFileIndex] : - _.first(serviceFilePaths); - const serviceFilename = serviceFileIndex !== -1 ? - serviceFilenames[serviceFileIndex] : - _.first(serviceFilenames); - - if (serviceFilename === 'serverless.js') { - return BbPromise.try(() => { - // use require to load serverless.js file - // eslint-disable-next-line global-require - const configExport = require(serviceFilePath); - // In case of a promise result, first resolve it. - return configExport; - }).then(config => { - if (!_.isPlainObject(config)) { - throw new Error('serverless.js must export plain object'); - } - - return that.loadServiceFileParam(serviceFilename, config); - }); - } - - return that.serverless.yamlParser - .parse(serviceFilePath) - .then((serverlessFileParam) => - that.loadServiceFileParam(serviceFilename, serverlessFileParam) - ); + return BbPromise.all([ + serverlessConfigFileUtils.getServerlessConfigFilePath(this.serverless), + serverlessConfigFileUtils.getServerlessConfigFile(this.serverless), + ]).then(args => that.loadServiceFileParam(...args)); } loadServiceFileParam(serviceFilename, serverlessFileParam) { const that = this; + that.serviceFilename = path.basename(serviceFilename); + const serverlessFile = serverlessFileParam; // basic service level validation const version = this.serverless.utils.getVersion(); @@ -101,18 +64,20 @@ class Service { if (ymlVersion && !semver.satisfies(version, ymlVersion)) { const errorMessage = [ `The Serverless version (${version}) does not satisfy the`, - ` "frameworkVersion" (${ymlVersion}) in ${serviceFilename}`, + ` "frameworkVersion" (${ymlVersion}) in ${this.serviceFilename}`, ].join(''); throw new ServerlessError(errorMessage); } if (!serverlessFile.service) { - throw new ServerlessError(`"service" property is missing in ${serviceFilename}`); + throw new ServerlessError(`"service" property is missing in ${this.serviceFilename}`); } if (_.isObject(serverlessFile.service) && !serverlessFile.service.name) { - throw new ServerlessError(`"service" is missing the "name" property in ${serviceFilename}`); // eslint-disable-line max-len + throw new ServerlessError( + `"service" is missing the "name" property in ${this.serviceFilename}` + ); // eslint-disable-line max-len } if (!serverlessFile.provider) { - throw new ServerlessError(`"provider" property is missing in ${serviceFilename}`); + throw new ServerlessError(`"provider" property is missing in ${this.serviceFilename}`); } // ####################################################################### @@ -162,6 +127,8 @@ class Service { that.layers = serverlessFile.layers || {}; } + that.outputs = serverlessFile.outputs; + return this; } @@ -179,8 +146,9 @@ class Service { } if (!functionObj.name) { - that.functions[functionName].name = - `${that.service}-${stageNameForFunction}-${functionName}`; + that.functions[ + functionName + ].name = `${that.service}-${stageNameForFunction}-${functionName}`; } }); } @@ -205,8 +173,9 @@ class Service { validate() { _.forEach(this.functions, (functionObj, functionName) => { if (!_.isArray(functionObj.events)) { - throw new ServerlessError(`Events for "${functionName}" must be an array,` + - ` not an ${typeof functionObj.events}`); + throw new ServerlessError( + `Events for "${functionName}" must be an array, not an ${typeof functionObj.events}` + ); } }); @@ -216,11 +185,13 @@ class Service { this.getAllFunctions().forEach(funcName => { _.forEach(this.getAllEventsInFunction(funcName), event => { if (_.has(event, 'http') && !validAPIGatewayStageNamePattern.test(stage)) { - throw new this.serverless.classes.Error([ - `Invalid stage name ${stage}:`, - 'it should contains only [-_a-zA-Z0-9] for AWS provider if http event are present', - 'according to API Gateway limitation.', - ].join(' ')); + throw new this.serverless.classes.Error( + [ + `Invalid stage name ${stage}:`, + 'it should contains only [-_a-zA-Z0-9] for AWS provider if http event are present', + 'according to API Gateway limitation.', + ].join(' ') + ); } }); }); @@ -250,7 +221,7 @@ class Service { } getAllFunctionsNames() { - return this.getAllFunctions().map((func) => this.getFunction(func).name); + return this.getAllFunctions().map(func => this.getFunction(func).name); } getFunction(functionName) { @@ -268,8 +239,7 @@ class Service { } getEventInFunction(eventName, functionName) { - const event = this.getFunction(functionName).events - .find(e => Object.keys(e)[0] === eventName); + const event = this.getFunction(functionName).events.find(e => Object.keys(e)[0] === eventName); if (event) { return event; } diff --git a/lib/classes/Service.test.js b/lib/classes/Service.test.js index 3c57f91d6..8d599578a 100644 --- a/lib/classes/Service.test.js +++ b/lib/classes/Service.test.js @@ -8,7 +8,7 @@ const sinon = require('sinon'); const Service = require('../../lib/classes/Service'); const Utils = require('../../lib/classes/Utils'); const Serverless = require('../../lib/Serverless'); -const testUtils = require('../../tests/utils'); +const { getTmpDirPath } = require('../../tests/utils/fs'); // Configure chai chai.use(require('chai-as-promised')); @@ -17,6 +17,7 @@ const expect = require('chai').expect; describe('Service', () => { describe('#constructor()', () => { const serverless = new Serverless(); + serverless.processedInput = { options: {} }; it('should attach serverless instance', () => { const serviceInstance = new Service(serverless); @@ -30,7 +31,6 @@ describe('Service', () => { expect(serviceInstance.serviceObject).to.be.equal(null); expect(serviceInstance.provider).to.deep.equal({ stage: 'dev', - region: 'us-east-1', variableSyntax: '\\${([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*]+?)}', }); expect(serviceInstance.custom).to.deep.equal({}); @@ -97,14 +97,14 @@ describe('Service', () => { const data = { provider: { name: 'testProvider', - runtime: 'nodejs6.10', + runtime: 'nodejs10.x', }, }; const serviceInstance = new Service(serverless, data); expect(serviceInstance.provider.name).to.be.equal('testProvider'); - expect(serviceInstance.provider.runtime).to.be.equal('nodejs6.10'); + expect(serviceInstance.provider.runtime).to.be.equal('nodejs10.x'); }); }); @@ -113,14 +113,15 @@ describe('Service', () => { let tmpDirPath; beforeEach(() => { - tmpDirPath = testUtils.getTmpDirPath(); + tmpDirPath = getTmpDirPath(); }); it('should resolve if no servicePath is found', () => { const serverless = new Serverless(); + serverless.processedInput = { options: {} }; const noService = new Service(serverless); - return expect(noService.load()).to.eventually.resolve; + return expect(noService.load()).to.be.fulfilled; }); it('should load serverless.yml from filesystem', () => { @@ -152,10 +153,10 @@ describe('Service', () => { }, }; - SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), - YAML.dump(serverlessYml)); + SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), YAML.dump(serverlessYml)); const serverless = new Serverless(); + serverless.processedInput = { options: {} }; serverless.config.update({ servicePath: tmpDirPath }); serviceInstance = new Service(serverless); @@ -206,10 +207,10 @@ describe('Service', () => { }, }; - SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yaml'), - YAML.dump(serverlessYml)); + SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yaml'), YAML.dump(serverlessYml)); const serverless = new Serverless(); + serverless.processedInput = { options: {} }; serverless.config.update({ servicePath: tmpDirPath }); serviceInstance = new Service(serverless); @@ -260,15 +261,17 @@ describe('Service', () => { }, }; - SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.json'), - JSON.stringify(serverlessJSON)); + SUtils.writeFileSync( + path.join(tmpDirPath, 'serverless.json'), + JSON.stringify(serverlessJSON) + ); const serverless = new Serverless(); + serverless.processedInput = { options: {} }; serverless.config.update({ servicePath: tmpDirPath }); serviceInstance = new Service(serverless); - return expect(serviceInstance.load()).to.eventually.be.fulfilled - .then(() => { + return expect(serviceInstance.load()).to.eventually.be.fulfilled.then(() => { expect(serviceInstance.service).to.be.equal('new-service'); expect(serviceInstance.provider.name).to.deep.equal('aws'); expect(serviceInstance.provider.variableSyntax).to.equal( @@ -315,15 +318,17 @@ describe('Service', () => { }, }; - SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.js'), - `module.exports = ${JSON.stringify(serverlessJSON)};`); + SUtils.writeFileSync( + path.join(tmpDirPath, 'serverless.js'), + `module.exports = ${JSON.stringify(serverlessJSON)};` + ); const serverless = new Serverless(); + serverless.processedInput = { options: {} }; serverless.config.update({ servicePath: tmpDirPath }); serviceInstance = new Service(serverless); - return expect(serviceInstance.load()).to.eventually.be.fulfilled - .then(() => { + return expect(serviceInstance.load()).to.eventually.be.fulfilled.then(() => { expect(serviceInstance.service).to.be.equal('new-service'); expect(serviceInstance.provider.name).to.deep.equal('aws'); expect(serviceInstance.provider.variableSyntax).to.equal( @@ -370,15 +375,17 @@ describe('Service', () => { }, }; - SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.js'), - `module.exports = new Promise(resolve => { resolve(${JSON.stringify(serverlessJSON)}) });`); + SUtils.writeFileSync( + path.join(tmpDirPath, 'serverless.js'), + `module.exports = new Promise(resolve => { resolve(${JSON.stringify(serverlessJSON)}) });` + ); const serverless = new Serverless(); + serverless.processedInput = { options: {} }; serverless.config.update({ servicePath: tmpDirPath }); serviceInstance = new Service(serverless); - return expect(serviceInstance.load()).to.eventually.be.fulfilled - .then(() => { + return expect(serviceInstance.load()).to.eventually.be.fulfilled.then(() => { expect(serviceInstance.service).to.be.equal('new-service'); expect(serviceInstance.provider.name).to.deep.equal('aws'); expect(serviceInstance.provider.variableSyntax).to.equal( @@ -400,15 +407,19 @@ describe('Service', () => { it('should throw error if serverless.js exports invalid config', () => { const SUtils = new Utils(); - SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.js'), - 'module.exports = function config() {};'); + SUtils.writeFileSync( + path.join(tmpDirPath, 'serverless.js'), + 'module.exports = function config() {};' + ); const serverless = new Serverless(); + serverless.processedInput = { options: {} }; serverless.config.update({ servicePath: tmpDirPath }); serviceInstance = new Service(serverless); - return expect(serviceInstance.load()) - .to.be.rejectedWith('serverless.js must export plain object'); + return expect(serviceInstance.load()).to.be.rejectedWith( + 'serverless.js must export plain object' + ); }); it('should load YAML in favor of JSON', () => { @@ -439,19 +450,20 @@ describe('Service', () => { }; serverlessJSON.service = 'JSON service'; - SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.json'), - JSON.stringify(serverlessJSON)); + SUtils.writeFileSync( + path.join(tmpDirPath, 'serverless.json'), + JSON.stringify(serverlessJSON) + ); serverlessJSON.service = 'YAML service'; - SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yaml'), - YAML.dump(serverlessJSON)); + SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yaml'), YAML.dump(serverlessJSON)); const serverless = new Serverless(); + serverless.processedInput = { options: {} }; serverless.config.update({ servicePath: tmpDirPath }); serviceInstance = new Service(serverless); - return expect(serviceInstance.load()).to.eventually.be.fulfilled - .then(() => { + return expect(serviceInstance.load()).to.eventually.be.fulfilled.then(() => { // YAML should have been loaded instead of JSON expect(serviceInstance.service).to.be.equal('YAML service'); }); @@ -464,14 +476,15 @@ describe('Service', () => { provider: 'aws', }; - SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yaml'), - YAML.dump(serverlessYaml)); + SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yaml'), YAML.dump(serverlessYaml)); const serverless = new Serverless({ servicePath: tmpDirPath }); + serverless.processedInput = { options: {} }; serviceInstance = new Service(serverless); - return expect(serviceInstance.load()).to.eventually.be - .rejectedWith('"service" is missing the "name" property in'); + return expect(serviceInstance.load()).to.eventually.be.rejectedWith( + '"service" is missing the "name" property in' + ); }); it('should support service objects', () => { @@ -484,14 +497,13 @@ describe('Service', () => { provider: 'aws', }; - SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yaml'), - YAML.dump(serverlessYaml)); + SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yaml'), YAML.dump(serverlessYaml)); const serverless = new Serverless({ servicePath: tmpDirPath }); + serverless.processedInput = { options: {} }; serviceInstance = new Service(serverless); - return expect(serviceInstance.load()).to.eventually.be.fulfilled - .then(() => { + return expect(serviceInstance.load()).to.eventually.be.fulfilled.then(() => { expect(serviceInstance.service).to.equal('my-service'); expect(serviceInstance.serviceObject).to.deep.equal(serverlessYaml.service); }); @@ -509,14 +521,13 @@ describe('Service', () => { }, }; - SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yaml'), - YAML.dump(serverlessYaml)); + SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yaml'), YAML.dump(serverlessYaml)); const serverless = new Serverless({ servicePath: tmpDirPath }); + serverless.processedInput = { options: {} }; serviceInstance = new Service(serverless); - return expect(serviceInstance.load()).to.eventually.be.fulfilled - .then(() => { + return expect(serviceInstance.load()).to.eventually.be.fulfilled.then(() => { serviceInstance.setFunctionNames(); const expectedFunc = { functionA: { @@ -542,14 +553,13 @@ describe('Service', () => { }, }; - SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yaml'), - YAML.dump(serverlessYaml)); + SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yaml'), YAML.dump(serverlessYaml)); const serverless = new Serverless({ servicePath: tmpDirPath }); + serverless.processedInput = { options: {} }; serviceInstance = new Service(serverless); - return expect(serviceInstance.load()).to.eventually.be.fulfilled - .then(() => { + return expect(serviceInstance.load()).to.eventually.be.fulfilled.then(() => { serviceInstance.setFunctionNames(); const expectedFunc = { functionA: { @@ -573,14 +583,13 @@ describe('Service', () => { }, }; - SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), - YAML.dump(serverlessYml)); + SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), YAML.dump(serverlessYml)); const serverless = new Serverless({ servicePath: tmpDirPath }); + serverless.processedInput = { options: {} }; serviceInstance = new Service(serverless); - return expect(serviceInstance.load({ stage: 'dev' })).to.eventually.be.fulfilled - .then(() => { + return expect(serviceInstance.load({ stage: 'dev' })).to.eventually.be.fulfilled.then(() => { serviceInstance.setFunctionNames(); const expectedFunc = { functionA: { @@ -600,14 +609,15 @@ describe('Service', () => { provider: 'aws', functions: {}, }; - SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), - YAML.dump(serverlessYml)); + SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), YAML.dump(serverlessYml)); const serverless = new Serverless({ servicePath: tmpDirPath }); + serverless.processedInput = { options: {} }; serviceInstance = new Service(serverless); - return expect(serviceInstance.load()).to.eventually.be - .rejectedWith('"service" property is missing in serverless.yml'); + return expect(serviceInstance.load()).to.eventually.be.rejectedWith( + '"service" property is missing in serverless.yml' + ); }); it('should reject if provider property is missing', () => { @@ -616,14 +626,15 @@ describe('Service', () => { service: 'service-name', functions: {}, }; - SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), - YAML.dump(serverlessYml)); + SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), YAML.dump(serverlessYml)); const serverless = new Serverless({ servicePath: tmpDirPath }); + serverless.processedInput = { options: {} }; serviceInstance = new Service(serverless); - return expect(serviceInstance.load()).to.eventually.be - .rejectedWith('"provider" property is missing in serverless.yml'); + return expect(serviceInstance.load()).to.eventually.be.rejectedWith( + '"provider" property is missing in serverless.yml' + ); }); it('should reject if frameworkVersion is not satisfied', () => { @@ -634,16 +645,17 @@ describe('Service', () => { provider: 'aws', functions: {}, }; - SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), - YAML.dump(serverlessYml)); + SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), YAML.dump(serverlessYml)); const serverless = new Serverless({ servicePath: tmpDirPath }); + serverless.processedInput = { options: {} }; const getVersion = sinon.stub(serverless.utils, 'getVersion'); getVersion.returns('1.0.2'); serviceInstance = new Service(serverless); - return expect(serviceInstance.load()).to.eventually.be - .rejectedWith(/version \(1\.0\.2\).*"frameworkVersion" \(=1\.0\.0\)/); + return expect(serviceInstance.load()).to.eventually.be.rejectedWith( + /version \(1\.0\.2\).*"frameworkVersion" \(=1\.0\.0\)/ + ); }); it('should pass if frameworkVersion is satisfied', () => { @@ -654,10 +666,10 @@ describe('Service', () => { provider: 'aws', functions: {}, }; - SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), - YAML.dump(serverlessYml)); + SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), YAML.dump(serverlessYml)); const serverless = new Serverless({ servicePath: tmpDirPath }); + serverless.processedInput = { options: {} }; const getVersion = sinon.stub(serverless.utils, 'getVersion'); getVersion.returns('1.2.2'); serviceInstance = new Service(serverless); @@ -671,15 +683,14 @@ describe('Service', () => { service: 'service-name', provider: 'aws', }; - SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), - YAML.dump(serverlessYml)); + SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), YAML.dump(serverlessYml)); const serverless = new Serverless({ servicePath: tmpDirPath }); + serverless.processedInput = { options: {} }; serverless.variables.service = serverless.service; serviceInstance = new Service(serverless); - return expect(serviceInstance.load()).to.eventually.be.fulfilled - .then(() => { + return expect(serviceInstance.load()).to.eventually.be.fulfilled.then(() => { // populate variables in service configuration serverless.variables.populateService(); @@ -695,10 +706,10 @@ describe('Service', () => { let tmpDirPath; beforeEach(() => { - tmpDirPath = testUtils.getTmpDirPath(); + tmpDirPath = getTmpDirPath(); }); - it('should throw if a function\'s event is not an array or a variable', () => { + it("should throw if a function's event is not an array or a variable", () => { const SUtils = new Utils(); const serverlessYml = { service: 'service-name', @@ -709,32 +720,33 @@ describe('Service', () => { }, }, }; - SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), - YAML.dump(serverlessYml)); + SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), YAML.dump(serverlessYml)); const serverless = new Serverless({ servicePath: tmpDirPath }); + serverless.processedInput = { options: {} }; serverless.service = new Service(serverless); - return expect(serverless.service.load()).to.eventually.be.fulfilled - .then(() => { + return expect(serverless.service.load()).to.eventually.be.fulfilled.then(() => { // validate the service configuration, now that variables are loaded - expect(() => serverless.service.validate()) - .to.throw('Events for "functionA" must be an array, not an string'); + expect(() => serverless.service.validate()).to.throw( + 'Events for "functionA" must be an array, not an string' + ); }); }); describe('stage name validation', () => { function simulateRun(serverless) { return serverless.init().then(() => - serverless.variables.populateService(serverless.pluginManager.cliOptions) - .then(() => { - serverless.service.mergeArrays(); - serverless.service.setFunctionNames(serverless.processedInput.options); - })); + serverless.variables.populateService(serverless.pluginManager.cliOptions).then(() => { + serverless.service.mergeArrays(); + serverless.service.setFunctionNames(serverless.processedInput.options); + }) + ); } it(`should not throw an error if http event is absent and - stage contains only alphanumeric, underscore and hyphen`, () => { + stage contains only alphanumeric, underscore and hyphen`, function() { + this.timeout(10000); // Occasionally times out with default settings const SUtils = new Utils(); const serverlessYml = { service: 'new-service', @@ -748,17 +760,17 @@ describe('Service', () => { }, }, }; - SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), - YAML.dump(serverlessYml)); + SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), YAML.dump(serverlessYml)); const serverless = new Serverless({ servicePath: tmpDirPath }); + serverless.processedInput = { options: {} }; return expect(simulateRun(serverless)).to.eventually.be.fulfilled.then(() => { expect(() => serverless.service.validate()).to.not.throw(serverless.classes.Error); }); }); it(`should not throw an error after variable population if http event is present and - the populated stage contains only alphanumeric, underscore and hyphen`, () => { + the populated stage contains only alphanumeric, underscore and hyphen`, () => { const SUtils = new Utils(); const serverlessYml = { service: 'new-service', @@ -779,8 +791,7 @@ describe('Service', () => { }, }, }; - SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), - YAML.dump(serverlessYml)); + SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), YAML.dump(serverlessYml)); const serverless = new Serverless({ servicePath: tmpDirPath }); return expect(simulateRun(serverless)).to.eventually.be.fulfilled.then(() => { @@ -809,16 +820,18 @@ describe('Service', () => { }, }, }; - SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), - YAML.dump(serverlessYml)); + SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), YAML.dump(serverlessYml)); const serverless = new Serverless({ servicePath: tmpDirPath }); return expect(simulateRun(serverless)).to.eventually.be.fulfilled.then(() => { - expect(() => serverless.service.validate()).to.throw(serverless.classes.Error, [ - 'Invalid stage name my@stage: it should contains only [-_a-zA-Z0-9]', - 'for AWS provider if http event are present', - 'according to API Gateway limitation.', - ].join(' ')); + expect(() => serverless.service.validate()).to.throw( + serverless.classes.Error, + [ + 'Invalid stage name my@stage: it should contains only [-_a-zA-Z0-9]', + 'for AWS provider if http event are present', + 'according to API Gateway limitation.', + ].join(' ') + ); }); }); @@ -844,16 +857,18 @@ describe('Service', () => { }, }, }; - SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), - YAML.dump(serverlessYml)); + SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), YAML.dump(serverlessYml)); const serverless = new Serverless({ servicePath: tmpDirPath }); return expect(simulateRun(serverless)).to.eventually.be.fulfilled.then(() => { - expect(() => serverless.service.validate()).to.throw(serverless.classes.Error, [ - 'Invalid stage name default:stage: it should contains only [-_a-zA-Z0-9]', - 'for AWS provider if http event are present', - 'according to API Gateway limitation.', - ].join(' ')); + expect(() => serverless.service.validate()).to.throw( + serverless.classes.Error, + [ + 'Invalid stage name default:stage: it should contains only [-_a-zA-Z0-9]', + 'for AWS provider if http event are present', + 'according to API Gateway limitation.', + ].join(' ') + ); }); }); }); @@ -876,7 +891,8 @@ describe('Service', () => { Resources: { azure: {}, }, - }, { + }, + { foo: 'bar', }, ]; @@ -930,9 +946,7 @@ describe('Service', () => { const serverless = new Serverless(); const serviceInstance = new Service(serverless); - serviceInstance.resources = [ - 42, - ]; + serviceInstance.resources = [42]; expect(() => serviceInstance.mergeArrays()).to.throw(Error); }); @@ -941,9 +955,7 @@ describe('Service', () => { const serverless = new Serverless(); const serviceInstance = new Service(serverless); - serviceInstance.resources = [ - 'string', - ]; + serviceInstance.resources = ['string']; expect(() => serviceInstance.mergeArrays()).to.throw(Error); }); @@ -952,11 +964,14 @@ describe('Service', () => { const serverless = new Serverless(); const serviceInstance = new Service(serverless); - serviceInstance.functions = [{ - a: {}, - }, { - b: {}, - }]; + serviceInstance.functions = [ + { + a: {}, + }, + { + b: {}, + }, + ]; serviceInstance.mergeArrays(); @@ -971,7 +986,7 @@ describe('Service', () => { let tmpDirPath; beforeEach(() => { - tmpDirPath = testUtils.getTmpDirPath(); + tmpDirPath = getTmpDirPath(); }); it('should make sure function name contains the default stage', () => { @@ -1002,10 +1017,10 @@ describe('Service', () => { }, }; - SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), - YAML.dump(serverlessYml)); + SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), YAML.dump(serverlessYml)); const serverless = new Serverless(); + serverless.processedInput = { options: {} }; serverless.config.update({ servicePath: tmpDirPath }); serviceInstance = new Service(serverless); @@ -1137,16 +1152,19 @@ describe('Service', () => { serviceInstance = new Service(serverless); serviceInstance.functions = { create: { - events: [{ - schedule: 'rate(5 minutes)', - }], + events: [ + { + schedule: 'rate(5 minutes)', + }, + ], }, }; }); it('should return an event object based on provided function', () => { - expect(serviceInstance.getEventInFunction('schedule', 'create')) - .to.deep.equal({ schedule: 'rate(5 minutes)' }); + expect(serviceInstance.getEventInFunction('schedule', 'create')).to.deep.equal({ + schedule: 'rate(5 minutes)', + }); }); it('should throw error if function does not exist in service', () => { @@ -1168,20 +1186,25 @@ describe('Service', () => { const serviceInstance = new Service(serverless); serviceInstance.functions = { create: { - events: [{ - schedule: 'rate(5 minutes)', - }, { - bucket: 'my_bucket', - }], + events: [ + { + schedule: 'rate(5 minutes)', + }, + { + bucket: 'my_bucket', + }, + ], }, }; - expect(serviceInstance.getAllEventsInFunction('create')) - .to.deep.equal([{ + expect(serviceInstance.getAllEventsInFunction('create')).to.deep.equal([ + { schedule: 'rate(5 minutes)', - }, { + }, + { bucket: 'my_bucket', - }]); + }, + ]); }); }); }); diff --git a/lib/classes/Utils.js b/lib/classes/Utils.js index 4876f6775..73687675e 100644 --- a/lib/classes/Utils.js +++ b/lib/classes/Utils.js @@ -1,5 +1,7 @@ 'use strict'; +const os = require('os'); +const crypto = require('crypto'); const fs = require('fs'); const path = require('path'); const ci = require('ci-info'); @@ -31,6 +33,16 @@ class Utils { return dirExistsSync(dirPath); } + getTmpDirPath() { + const dirPath = path.join( + os.tmpdir(), + 'tmpdirs-serverless', + crypto.randomBytes(8).toString('hex') + ); + fse.ensureDirSync(dirPath); + return dirPath; + } + fileExistsSync(filePath) { return fileExistsSync(filePath); } @@ -94,13 +106,21 @@ class Utils { } generateShortId(length) { - return Math.random().toString(36).substr(2, length); + return Math.random() + .toString(36) + .substr(2, length); } - findServicePath() { + findServicePath(customPath) { let servicePath = null; - if (fileExistsSync(path.join(process.cwd(), 'serverless.yml'))) { + if (customPath) { + if (fileExistsSync(path.join(process.cwd(), customPath))) { + servicePath = process.cwd(); + } else { + throw new Error(`Config file ${customPath} not found`); + } + } else if (fileExistsSync(path.join(process.cwd(), 'serverless.yml'))) { servicePath = process.cwd(); } else if (fileExistsSync(path.join(process.cwd(), 'serverless.yaml'))) { servicePath = process.cwd(); @@ -127,7 +147,7 @@ class Utils { const options = serverless.processedInput.options; const commands = serverless.processedInput.commands; - return new BbPromise((resolve) => { + return new BbPromise(resolve => { const config = configUtils.getConfig(); const userId = config.frameworkId; const trackingDisabled = config.trackingDisabled; @@ -140,7 +160,7 @@ class Utils { let serviceName = ''; if (service && service.service && service.service.name) { serviceName = service.service.name; - } else if (service && (typeof service.service === 'string')) { + } else if (service && typeof service.service === 'string') { serviceName = service.service; } @@ -148,12 +168,12 @@ class Utils { const whitelistedOptionKeys = ['help', 'disable', 'enable']; const optionKeys = Object.keys(options); - const filteredOptionKeys = optionKeys.filter((key) => - whitelistedOptionKeys.indexOf(key) !== -1 + const filteredOptionKeys = optionKeys.filter( + key => whitelistedOptionKeys.indexOf(key) !== -1 ); const filteredOptions = {}; - filteredOptionKeys.forEach((key) => { + filteredOptionKeys.forEach(key => { filteredOptions[key] = options[key]; }); @@ -162,13 +182,11 @@ class Utils { const memorySizeAndTimeoutPerFunction = []; if (numberOfFunctions) { - _.forEach(functions, (func) => { - const memorySize = Number(func.memorySize) - || Number(this.serverless.service.provider.memorySize) - || 1024; - const timeout = Number(func.timeout) - || Number(this.serverless.service.provider.timeout) - || 6; + _.forEach(functions, func => { + const memorySize = + Number(func.memorySize) || Number(this.serverless.service.provider.memorySize) || 1024; + const timeout = + Number(func.timeout) || Number(this.serverless.service.provider.timeout) || 6; const memorySizeAndTimeoutObject = { memorySize, @@ -186,11 +204,11 @@ class Utils { let hasCustomAuthorizer = false; let hasCognitoAuthorizer = false; if (numberOfFunctions) { - _.forEach(functions, (func) => { + _.forEach(functions, func => { if (func.events) { const funcEventsArray = []; - func.events.forEach((event) => { + func.events.forEach(event => { const name = Object.keys(event)[0]; funcEventsArray.push(name); @@ -206,10 +224,12 @@ class Utils { // For HTTP events, see what authorizer types are enabled if (_.has(event, 'http.authorizer')) { - if ((_.isString(event.http.authorizer) - && _.toUpper(event.http.authorizer) === 'AWS_IAM') - || (event.http.authorizer.type - && _.toUpper(event.http.authorizer.type) === 'AWS_IAM')) { + if ( + (_.isString(event.http.authorizer) && + _.toUpper(event.http.authorizer) === 'AWS_IAM') || + (event.http.authorizer.type && + _.toUpper(event.http.authorizer.type) === 'AWS_IAM') + ) { hasIAMAuthorizer = true; } // There are three ways a user can specify a Custom authorizer: @@ -219,18 +239,22 @@ class Utils { // in the authorizer object. // 3) By listing a function's ARN in the arn property of the authorizer object. - if ((_.isString(event.http.authorizer) - && _.toUpper(event.http.authorizer) !== 'AWS_IAM' - && !awsArnRegExs.cognitoIdpArnExpr.test(event.http.authorizer)) - || event.http.authorizer.name - || (event.http.authorizer.arn - && awsArnRegExs.lambdaArnExpr.test(event.http.authorizer.arn))) { + if ( + (_.isString(event.http.authorizer) && + _.toUpper(event.http.authorizer) !== 'AWS_IAM' && + !awsArnRegExs.cognitoIdpArnExpr.test(event.http.authorizer)) || + event.http.authorizer.name || + (event.http.authorizer.arn && + awsArnRegExs.lambdaArnExpr.test(event.http.authorizer.arn)) + ) { hasCustomAuthorizer = true; } - if ((_.isString(event.http.authorizer) - && awsArnRegExs.cognitoIdpArnExpr.test(event.http.authorizer)) - || (event.http.authorizer.arn - && awsArnRegExs.cognitoIdpArnExpr.test(event.http.authorizer.arn))) { + if ( + (_.isString(event.http.authorizer) && + awsArnRegExs.cognitoIdpArnExpr.test(event.http.authorizer)) || + (event.http.authorizer.arn && + awsArnRegExs.cognitoIdpArnExpr.test(event.http.authorizer.arn)) + ) { hasCognitoAuthorizer = true; } } @@ -243,11 +267,11 @@ class Utils { let hasCustomResourcesDefined = false; // check if configuration in resources.Resources is defined - if ((resources && resources.Resources && Object.keys(resources.Resources).length)) { + if (resources && resources.Resources && Object.keys(resources.Resources).length) { hasCustomResourcesDefined = true; } // check if configuration in resources.Outputs is defined - if ((resources && resources.Outputs && Object.keys(resources.Outputs).length)) { + if (resources && resources.Outputs && Object.keys(resources.Outputs).length) { hasCustomResourcesDefined = true; } @@ -255,8 +279,11 @@ class Utils { const defaultVariableSyntax = '\\${([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)]+?)}'; // check if the variableSyntax in the provider section is defined - if (provider && provider.variableSyntax - && provider.variableSyntax !== defaultVariableSyntax) { + if ( + provider && + provider.variableSyntax && + provider.variableSyntax !== defaultVariableSyntax + ) { hasCustomVariableSyntaxDefined = true; } @@ -268,12 +295,12 @@ class Utils { command: { name: commands.join(' '), filteredOptions, - isRunInService: (!!serverless.config.servicePath), + isRunInService: !!serverless.config.servicePath, }, service: { numberOfCustomPlugins: _.size(service.plugins), hasCustomResourcesDefined, - hasVariablesInCustomSectionDefined: (!!service.custom), + hasVariablesInCustomSectionDefined: !!service.custom, hasCustomVariableSyntaxDefined, name: serviceName, }, @@ -296,10 +323,10 @@ class Utils { userId, context, invocationId, - timestamp: (new Date()).getTime(), - timezone: (new Date()).toString().match(/([A-Z]+[+-][0-9]+)/)[1], + timestamp: new Date().getTime(), + timezone: new Date().toString().match(/([A-Z]+[+-][0-9]+)/)[1], operatingSystem: process.platform, - userAgent: (process.env.SERVERLESS_DASHBOARD) ? 'dashboard' : 'cli', + userAgent: process.env.SERVERLESS_DASHBOARD ? 'dashboard' : 'cli', serverlessVersion: serverless.version, nodeJsVersion: process.version, isDockerContainer: isDockerContainer(), @@ -323,7 +350,7 @@ class Utils { } return resolve(data); - }).then((data) => { + }).then(data => { if (data) { segment.track(data); } @@ -335,22 +362,28 @@ class Utils { const currentId = userConfig.userId; const globalConfig = configUtils.getGlobalConfig(); const username = _.get(globalConfig, `users[${currentId}].dashboard.username`); - return _.get(globalConfig, `users[${currentId}].dashboard.accessKey`, false) || - _.get(globalConfig, `users[${currentId}].dashboard.accessKeys[${username}]`, false); + return ( + _.get(globalConfig, `users[${currentId}].dashboard.accessKey`, false) || + _.get(globalConfig, `users[${currentId}].dashboard.accessKeys[${username}]`, false) + ); } isEventUsed(functions, eventName) { - return _.reduce(functions, (accum, func) => { - const events = func.events || []; - if (events.length) { - events.forEach(event => { - if (Object.keys(event)[0] === eventName) { - accum = true; // eslint-disable-line no-param-reassign - } - }); - } - return accum; - }, false); + return _.reduce( + functions, + (accum, func) => { + const events = func.events || []; + if (events.length) { + events.forEach(event => { + if (Object.keys(event)[0] === eventName) { + accum = true; // eslint-disable-line no-param-reassign + } + }); + } + return accum; + }, + false + ); } } diff --git a/lib/classes/Utils.test.js b/lib/classes/Utils.test.js index 1c3fd1a2c..bc0bbae77 100644 --- a/lib/classes/Utils.test.js +++ b/lib/classes/Utils.test.js @@ -5,14 +5,16 @@ const os = require('os'); const uuid = require('uuid'); const chai = require('chai'); const sinon = require('sinon'); +const fse = require('fs-extra'); const Serverless = require('../../lib/Serverless'); -const testUtils = require('../../tests/utils'); const configUtils = require('../utils/config'); -const serverlessVersion = require('../../package.json').version; const segment = require('../utils/segment'); -chai.use(require('chai-as-promised')); -const expect = require('chai').expect; const Utils = require('../../lib/classes/Utils'); +const { expect } = require('chai'); +const { getTmpFilePath, getTmpDirPath } = require('../../tests/utils/fs'); +const serverlessVersion = require('../../package.json').version; + +chai.use(require('chai-as-promised')); describe('Utils', () => { let utils; @@ -23,6 +25,15 @@ describe('Utils', () => { utils = new Utils(serverless); }); + describe('#getTmpDirPath()', () => { + it('should create a scoped tmp directory', () => { + const dirPath = serverless.utils.getTmpDirPath(); + const stats = fse.statSync(dirPath); + expect(dirPath).to.include('tmpdirs-serverless'); + expect(stats.isDirectory()).to.equal(true); + }); + }); + describe('#dirExistsSync()', () => { describe('When reading a directory', () => { it('should detect if a directory exists', () => { @@ -30,7 +41,7 @@ describe('Utils', () => { expect(dir).to.equal(true); }); - it('should detect if a directory doesn\'t exist', () => { + it("should detect if a directory doesn't exist", () => { const noDir = serverless.utils.dirExistsSync(path.join(__dirname, '..', 'XYZ')); expect(noDir).to.equal(false); }); @@ -44,7 +55,7 @@ describe('Utils', () => { expect(file).to.equal(true); }); - it('should detect if a file doesn\'t exist', () => { + it("should detect if a file doesn't exist", () => { const noFile = serverless.utils.fileExistsSync(path.join(__dirname, 'XYZ.json')); expect(noFile).to.equal(false); }); @@ -53,20 +64,22 @@ describe('Utils', () => { describe('#writeFileDir()', () => { it('should create a directory for the path of the given file', () => { - const tmpDirPath = testUtils.getTmpDirPath(); - const rootDir = serverless.utils - .writeFileDir(path.join(tmpDirPath, 'foo', 'bar', 'somefile.js')); + const tmpDirPath = getTmpDirPath(); + const rootDir = serverless.utils.writeFileDir( + path.join(tmpDirPath, 'foo', 'bar', 'somefile.js') + ); expect(serverless.utils.dirExistsSync(path.join(rootDir, 'foo', 'bar'))).to.equal(true); // it should only create the directories and not the file - expect(serverless.utils.fileExistsSync(path.join(rootDir, 'foo', 'bar', 'somefile.js'))) - .to.equal(false); + expect( + serverless.utils.fileExistsSync(path.join(rootDir, 'foo', 'bar', 'somefile.js')) + ).to.equal(false); }); }); describe('#writeFileSync()', () => { it('should write a .json file synchronously', () => { - const tmpFilePath = testUtils.getTmpFilePath('anything.json'); + const tmpFilePath = getTmpFilePath('anything.json'); serverless.utils.writeFileSync(tmpFilePath, { foo: 'bar' }); const obj = serverless.utils.readFileSync(tmpFilePath); @@ -75,47 +88,50 @@ describe('Utils', () => { }); it('should write a .yml file synchronously', () => { - const tmpFilePath = testUtils.getTmpFilePath('anything.yml'); + const tmpFilePath = getTmpFilePath('anything.yml'); serverless.utils.writeFileSync(tmpFilePath, { foo: 'bar' }); - return expect(serverless.yamlParser.parse(tmpFilePath)).to.be.fulfilled.then((obj) => { + return expect(serverless.yamlParser.parse(tmpFilePath)).to.be.fulfilled.then(obj => { expect(obj.foo).to.equal('bar'); }); }); it('should write a .yaml file synchronously', () => { - const tmpFilePath = testUtils.getTmpFilePath('anything.yaml'); + const tmpFilePath = getTmpFilePath('anything.yaml'); serverless.utils.writeFileSync(tmpFilePath, { foo: 'bar' }); - return expect(serverless.yamlParser.parse(tmpFilePath)).to.be.fulfilled.then((obj) => { + return expect(serverless.yamlParser.parse(tmpFilePath)).to.be.fulfilled.then(obj => { expect(obj.foo).to.equal('bar'); }); }); it('should throw error if invalid path is provided', () => { - expect(() => { serverless.utils.writeFileSync(null); }).to.throw(Error); + expect(() => { + serverless.utils.writeFileSync(null); + }).to.throw(Error); }); }); describe('#writeFile()', () => { it('should write a file asynchronously', () => { - const tmpFilePath = testUtils.getTmpFilePath('anything.json'); + const tmpFilePath = getTmpFilePath('anything.json'); // note: use return when testing promises otherwise you'll have unhandled rejection errors - return expect(serverless.utils.writeFile(tmpFilePath, { foo: 'bar' })) - .to.be.fulfilled.then(() => { - const obj = serverless.utils.readFileSync(tmpFilePath); + return expect(serverless.utils.writeFile(tmpFilePath, { foo: 'bar' })).to.be.fulfilled.then( + () => { + const obj = serverless.utils.readFileSync(tmpFilePath); - expect(obj.foo).to.equal('bar'); - }); + expect(obj.foo).to.equal('bar'); + } + ); }); }); describe('#appendFileSync()', () => { it('should append a line to a text file', () => { - const tmpFilePath = testUtils.getTmpFilePath('appendedfile.txt'); + const tmpFilePath = getTmpFilePath('appendedfile.txt'); serverless.utils.writeFileSync(tmpFilePath, `line 1 ${os.EOL}`); serverless.utils.appendFileSync(tmpFilePath, 'line 2'); @@ -125,13 +141,15 @@ describe('Utils', () => { }); it('should throw error if invalid path is provided', () => { - expect(() => { serverless.utils.readFileSync(null); }).to.throw(Error); + expect(() => { + serverless.utils.readFileSync(null); + }).to.throw(Error); }); }); describe('#readFileSync()', () => { it('should read a file synchronously', () => { - const tmpFilePath = testUtils.getTmpFilePath('anything.json'); + const tmpFilePath = getTmpFilePath('anything.json'); serverless.utils.writeFileSync(tmpFilePath, { foo: 'bar' }); const obj = serverless.utils.readFileSync(tmpFilePath); @@ -140,7 +158,7 @@ describe('Utils', () => { }); it('should read a filename extension .yml', () => { - const tmpFilePath = testUtils.getTmpFilePath('anything.yml'); + const tmpFilePath = getTmpFilePath('anything.yml'); serverless.utils.writeFileSync(tmpFilePath, { foo: 'bar' }); const obj = serverless.utils.readFileSync(tmpFilePath); @@ -149,7 +167,7 @@ describe('Utils', () => { }); it('should read a filename extension .yaml', () => { - const tmpFilePath = testUtils.getTmpFilePath('anything.yaml'); + const tmpFilePath = getTmpFilePath('anything.yaml'); serverless.utils.writeFileSync(tmpFilePath, { foo: 'bar' }); const obj = serverless.utils.readFileSync(tmpFilePath); @@ -158,24 +176,24 @@ describe('Utils', () => { }); it('should throw YAMLException with filename if yml file is invalid format', () => { - const tmpFilePath = testUtils.getTmpFilePath('invalid.yml'); + const tmpFilePath = getTmpFilePath('invalid.yml'); serverless.utils.writeFileSync(tmpFilePath, ': a'); expect(() => { serverless.utils.readFileSync(tmpFilePath); - }).to.throw(new RegExp('YAMLException:.*invalid.yml')); + }).to.throw(/.*invalid.yml/); }); }); describe('#readFile()', () => { it('should read a file asynchronously', () => { - const tmpFilePath = testUtils.getTmpFilePath('anything.json'); + const tmpFilePath = getTmpFilePath('anything.json'); serverless.utils.writeFileSync(tmpFilePath, { foo: 'bar' }); // note: use return when testing promises otherwise you'll have unhandled rejection errors - return expect(serverless.utils.readFile(tmpFilePath)).to.be.fulfilled.then((obj) => { + return expect(serverless.utils.readFile(tmpFilePath)).to.be.fulfilled.then(obj => { expect(obj.foo).to.equal('bar'); }); }); @@ -183,7 +201,7 @@ describe('Utils', () => { describe('#walkDirSync()', () => { it('should return an array with corresponding paths to the found files', () => { - const tmpDirPath = testUtils.getTmpDirPath(); + const tmpDirPath = getTmpDirPath(); const nestedDir1 = path.join(tmpDirPath, 'foo'); const nestedDir2 = path.join(tmpDirPath, 'foo', 'bar'); @@ -227,6 +245,8 @@ describe('Utils', () => { expect(serverless.utils.fileExistsSync(destFile1)).to.equal(true); expect(serverless.utils.fileExistsSync(destFile2)).to.equal(true); expect(serverless.utils.fileExistsSync(destFile3)).to.equal(true); + fse.removeSync(tmpSrcDirPath); + fse.removeSync(tmpDestDirPath); }); }); @@ -246,7 +266,7 @@ describe('Utils', () => { const testDir = process.cwd(); it('should detect if the CWD is a service directory when using Serverless .yaml files', () => { - const tmpDirPath = testUtils.getTmpDirPath(); + const tmpDirPath = getTmpDirPath(); const tmpFilePath = path.join(tmpDirPath, 'serverless.yaml'); serverless.utils.writeFileSync(tmpFilePath, 'foo'); @@ -258,7 +278,7 @@ describe('Utils', () => { }); it('should detect if the CWD is a service directory when using Serverless .yml files', () => { - const tmpDirPath = testUtils.getTmpDirPath(); + const tmpDirPath = getTmpDirPath(); const tmpFilePath = path.join(tmpDirPath, 'serverless.yml'); serverless.utils.writeFileSync(tmpFilePath, 'foo'); @@ -270,7 +290,7 @@ describe('Utils', () => { }); it('should detect if the CWD is a service directory when using Serverless .json files', () => { - const tmpDirPath = testUtils.getTmpDirPath(); + const tmpDirPath = getTmpDirPath(); const tmpFilePath = path.join(tmpDirPath, 'serverless.json'); serverless.utils.writeFileSync(tmpFilePath, 'foo'); @@ -282,7 +302,7 @@ describe('Utils', () => { }); it('should detect if the CWD is a service directory when using Serverless .js files', () => { - const tmpDirPath = testUtils.getTmpDirPath(); + const tmpDirPath = getTmpDirPath(); const tmpFilePath = path.join(tmpDirPath, 'serverless.js'); serverless.utils.writeFileSync(tmpFilePath, 'foo'); @@ -437,7 +457,6 @@ describe('Utils', () => { }, }; - return expect(utils.logStat(serverless)).to.be.fulfilled.then(() => { expect(getConfigStub.calledOnce).to.equal(true); expect(trackStub.calledOnce).to.equal(true); @@ -728,7 +747,7 @@ describe('Utils', () => { service: 'new-service', provider: { name: 'aws', - runtime: 'nodejs6.10', + runtime: 'nodejs10.x', stage: 'dev', region: 'us-east-1', variableSyntax: '\\${foo}', @@ -780,12 +799,9 @@ describe('Utils', () => { expect(data.userId).to.equal('1234wasd'); // command property - expect(data.properties.command.name) - .to.equal(''); - expect(data.properties.command - .isRunInService).to.equal(false); // false because CWD is not a service - expect(data.properties.command.filteredOptions) - .to.deep.equal({}); + expect(data.properties.command.name).to.equal(''); + expect(data.properties.command.isRunInService).to.equal(false); // false because CWD is not a service + expect(data.properties.command.filteredOptions).to.deep.equal({}); // service property expect(data.properties.service.numberOfCustomPlugins).to.equal(0); expect(data.properties.service.hasCustomResourcesDefined).to.equal(true); @@ -794,14 +810,14 @@ describe('Utils', () => { expect(data.properties.service.name).to.equal('new-service'); // functions property expect(data.properties.functions.numberOfFunctions).to.equal(2); - expect(data.properties.functions.memorySizeAndTimeoutPerFunction[0] - .memorySize).to.equal(1024); - expect(data.properties.functions.memorySizeAndTimeoutPerFunction[0] - .timeout).to.equal(6); - expect(data.properties.functions.memorySizeAndTimeoutPerFunction[1] - .memorySize).to.equal(16); - expect(data.properties.functions.memorySizeAndTimeoutPerFunction[1] - .timeout).to.equal(200); + expect(data.properties.functions.memorySizeAndTimeoutPerFunction[0].memorySize).to.equal( + 1024 + ); + expect(data.properties.functions.memorySizeAndTimeoutPerFunction[0].timeout).to.equal(6); + expect(data.properties.functions.memorySizeAndTimeoutPerFunction[1].memorySize).to.equal( + 16 + ); + expect(data.properties.functions.memorySizeAndTimeoutPerFunction[1].timeout).to.equal(200); // events property expect(data.properties.events.numberOfEvents).to.equal(3); expect(data.properties.events.numberOfEventsPerType[0].name).to.equal('http'); diff --git a/lib/classes/Variables.js b/lib/classes/Variables.js index 1d35bb717..64c377a9a 100644 --- a/lib/classes/Variables.js +++ b/lib/classes/Variables.js @@ -80,22 +80,22 @@ class Variables { { name: 'SSM', method: 'getValueFromSsm', original: this.getValueFromSsm }, ]; const dependencyMessage = (configValue, serviceName) => - `Variable dependency failure: variable '${configValue}' references service ${ - serviceName} but using that service requires a concrete value to be called.`; + `Variable dependency failure: variable '${configValue}' references service ${serviceName} but using that service requires a concrete value to be called.`; // replace and then restore the methods for obtaining values from dependent services. the // replacement naturally rejects dependencies on these services that occur during prepopulation. // prepopulation is, of course, the process of obtaining the required configuration for using // these services. - dependentServices.forEach((dependentService) => { // knock out - this[dependentService.method] = (variableString) => BbPromise.reject( - dependencyMessage(variableString, dependentService.name)); + dependentServices.forEach(dependentService => { + // knock out + this[dependentService.method] = variableString => + BbPromise.reject(dependencyMessage(variableString, dependentService.name)); }); - return func() - .finally(() => { - dependentServices.forEach((dependentService) => { // restore - this[dependentService.method] = dependentService.original; - }); + return func().finally(() => { + dependentServices.forEach(dependentService => { + // restore + this[dependentService.method] = dependentService.original; }); + }); } prepopulateService() { const provider = this.serverless.getProvider('aws'); @@ -132,7 +132,8 @@ class Variables { return this.disableDepedentServices(() => { const prepopulations = requiredConfigs.map(config => this.populateValue(config.value, true) // populate - .then(populated => _.assign(config, { populated }))); + .then(populated => _.assign(config, { populated })) + ); return this.assignProperties(provider, prepopulations); }); } @@ -158,13 +159,15 @@ class Variables { this.service.serverless = undefined; return this.initialCall(() => this.prepopulateService() - .then(() => this.populateObjectImpl(this.service) - .finally(() => { + .then(() => + this.populateObjectImpl(this.service).finally(() => { // restore this.service.serverless = this.serverless; this.service.provider.variableSyntax = variableSyntaxProperty; - })) - .then(() => this.service)); + }) + ) + .then(() => this.service) + ); } // ############ // ## OBJECT ## @@ -212,9 +215,7 @@ class Variables { } const addContext = (value, key) => this.getProperties(root, false, value, context.concat(key), results); - if ( - _.isArray(current) - ) { + if (_.isArray(current)) { _.map(current, addContext); } else if ( _.isObject(current) && @@ -242,12 +243,14 @@ class Variables { * populated values of the given terminal properties */ populateVariables(properties) { - const variables = properties.filter(property => - _.isString(property.value) && - property.value.match(this.variableSyntax)); - return _.map(variables, - variable => this.populateValue(variable.value, false) - .then(populated => _.assign({}, variable, { populated }))); + const variables = properties.filter( + property => _.isString(property.value) && property.value.match(this.variableSyntax) + ); + return _.map(variables, variable => + this.populateValue(variable.value, false).then(populated => + _.assign({}, variable, { populated }) + ) + ); } /** * Assign the populated values back to the target object @@ -256,13 +259,15 @@ class Variables { * @returns {Promise} resolving with the number of changes that were applied to the given * target */ - assignProperties(target, populations) { // eslint-disable-line class-methods-use-this - return BbPromise.all(populations) - .then((results) => results.forEach((result) => { + assignProperties(target, populations) { + // eslint-disable-line class-methods-use-this + return BbPromise.all(populations).then(results => + results.forEach(result => { if (result.value !== result.populated) { _.set(target, result.path, result.populated); } - })); + }) + ); } /** * Populate the variables in the given object. @@ -278,8 +283,9 @@ class Variables { if (populations.length === 0) { return BbPromise.resolve(objectToPopulate); } - return this.assignProperties(objectToPopulate, populations) - .then(() => this.populateObjectImpl(objectToPopulate)); + return this.assignProperties(objectToPopulate, populations).then(() => + this.populateObjectImpl(objectToPopulate) + ); } // ############## // ## PROPERTY ## @@ -291,10 +297,7 @@ class Variables { * @returns {string} The cleaned variable match */ cleanVariable(match) { - let cleaned = match.replace( - this.variableSyntax, - (context, contents) => contents.trim() - ); + let cleaned = match.replace(this.variableSyntax, (context, contents) => contents.trim()); if (!cleaned.match(/".*"|'.*'/)) { cleaned = cleaned.replace(/\s/g, ''); } @@ -331,7 +334,7 @@ class Variables { * @returns {Promise[]} Promises for the eventual populated values of the given matches */ populateMatches(matches, property) { - return _.map(matches, (match) => this.splitAndGet(match, property)); + return _.map(matches, match => this.splitAndGet(match, property)); } /** * Render the given matches and their associated results to the given value @@ -365,7 +368,7 @@ class Variables { const populations = this.populateMatches(matches, valueToPopulate); return BbPromise.all(populations) .then(results => this.renderMatches(property, matches, results)) - .then((result) => { + .then(result => { if (root && matches.length) { return this.populateValue(result, root); } @@ -407,11 +410,14 @@ class Variables { */ populateVariable(propertyParam, matchedString, valueToPopulate) { let property = propertyParam; - if (property === matchedString) { // total replacement + if (property === matchedString) { + // total replacement property = valueToPopulate; - } else if (_.isString(valueToPopulate)) { // partial replacement, string + } else if (_.isString(valueToPopulate)) { + // partial replacement, string property = replaceall(matchedString, valueToPopulate, property); - } else if (_.isNumber(valueToPopulate)) { // partial replacement, number + } else if (_.isNumber(valueToPopulate)) { + // partial replacement, number property = replaceall(matchedString, String(valueToPopulate), property); } else { const errorMessage = [ @@ -443,15 +449,17 @@ class Variables { match = this.stringRefSyntax.exec(input); } const commaReplacements = []; - const contained = commaMatch => // curry the current commaMatch - stringMatch => // check whether stringMatch containing the commaMatch - stringMatch.start < commaMatch.index && - this.overwriteSyntax.lastIndex < stringMatch.end; + const contained = ( + commaMatch // curry the current commaMatch + ) => ( + stringMatch // check whether stringMatch containing the commaMatch + ) => stringMatch.start < commaMatch.index && this.overwriteSyntax.lastIndex < stringMatch.end; match = this.overwriteSyntax.exec(input); while (match) { const matchContained = contained(match); const containedBy = stringMatches.find(matchContained); - if (!containedBy) { // if uncontained, this comma respresents a splitting location + if (!containedBy) { + // if uncontained, this comma respresents a splitting location commaReplacements.push({ start: match.index, end: this.overwriteSyntax.lastIndex, @@ -461,7 +469,7 @@ class Variables { } let prior = 0; const results = []; - commaReplacements.forEach((replacement) => { + commaReplacements.forEach(replacement => { results.push(input.slice(prior, replacement.start)); prior = replacement.end; }); @@ -480,14 +488,13 @@ class Variables { // A sentinel to rid rejected Promises, so any of resolved value can be used as fallback. const FAIL_TOKEN = {}; const variableValues = variableStrings.map(variableString => - this.getValueFromSource(variableString, propertyString) - .catch(unused => FAIL_TOKEN)); // eslint-disable-line no-unused-vars + this.getValueFromSource(variableString, propertyString).catch(() => FAIL_TOKEN) + ); // eslint-disable-line no-unused-vars - const validValue = value => ( + const validValue = value => value !== null && typeof value !== 'undefined' && - !(typeof value === 'object' && _.isEmpty(value)) - ); + !(typeof value === 'object' && _.isEmpty(value)); return BbPromise.all(variableValues) .then(values => values.filter(v => v !== FAIL_TOKEN)) .then(values => { @@ -499,12 +506,13 @@ class Variables { const deepVariable = this.makeDeepVariable(value); deepPropertyString = deepPropertyString.replace( variableStrings[index], - this.cleanVariable(deepVariable)); + this.cleanVariable(deepVariable) + ); } }); - return deepProperties > 0 ? - BbPromise.resolve(deepPropertyString) : // return deep variable replacement of original - BbPromise.resolve(values.find(validValue));// resolve first valid value, else undefined + return deepProperties > 0 + ? BbPromise.resolve(deepPropertyString) // return deep variable replacement of original + : BbPromise.resolve(values.find(validValue)); // resolve first valid value, else undefined }); } /** @@ -559,7 +567,8 @@ class Variables { return BbPromise.resolve(valueToPopulate); } - getValueFromEnv(variableString) { // eslint-disable-line class-methods-use-this + getValueFromEnv(variableString) { + // eslint-disable-line class-methods-use-this const requestedEnvVar = variableString.split(':')[1]; let valueToPopulate; if (requestedEnvVar !== '' || '' in process.env) { @@ -570,7 +579,8 @@ class Variables { return BbPromise.resolve(valueToPopulate); } - getValueFromString(variableString) { // eslint-disable-line class-methods-use-this + getValueFromString(variableString) { + // eslint-disable-line class-methods-use-this const valueToPopulate = variableString.replace(/^['"]|['"]$/g, ''); return BbPromise.resolve(valueToPopulate); } @@ -603,7 +613,10 @@ class Variables { variable = 'self:provider.name'; } const valueToPopulate = this.service; - const deepProperties = variable.split(':')[1].split('.').filter(property => property); + const deepProperties = variable + .split(':')[1] + .split('.') + .filter(property => property); return this.getDeeperValue(deepProperties, valueToPopulate); } @@ -613,14 +626,14 @@ class Variables { .replace(this.fileRefSyntax, (match, varName) => varName.trim()) .replace('~', os.homedir()); - let referencedFileFullPath = (path.isAbsolute(referencedFileRelativePath) ? - referencedFileRelativePath : - path.join(this.serverless.config.servicePath, referencedFileRelativePath)); + let referencedFileFullPath = path.isAbsolute(referencedFileRelativePath) + ? referencedFileRelativePath + : path.join(this.serverless.config.servicePath, referencedFileRelativePath); // Get real path to handle potential symlinks (but don't fatal error) - referencedFileFullPath = fse.existsSync(referencedFileFullPath) ? - fse.realpathSync(referencedFileFullPath) : - referencedFileFullPath; + referencedFileFullPath = fse.existsSync(referencedFileFullPath) + ? fse.realpathSync(referencedFileFullPath) + : referencedFileFullPath; let fileExtension = referencedFileRelativePath.split('.'); fileExtension = fileExtension[fileExtension.length - 1]; @@ -655,12 +668,12 @@ class Variables { } valueToPopulate = returnValueFunction.call(jsFile, this.serverless); - return BbPromise.resolve(valueToPopulate).then((valueToPopulateResolved) => { + return BbPromise.resolve(valueToPopulate).then(valueToPopulateResolved => { let deepProperties = variableString.replace(matchedFileRefString, ''); deepProperties = deepProperties.slice(1).split('.'); deepProperties.splice(0, 1); - return this.getDeeperValue(deepProperties, valueToPopulateResolved) - .then((deepValueToPopulateResolved) => { + return this.getDeeperValue(deepProperties, valueToPopulateResolved).then( + deepValueToPopulateResolved => { if (typeof deepValueToPopulateResolved === 'undefined') { const errorMessage = [ 'Invalid variable syntax when referencing', @@ -670,7 +683,8 @@ class Variables { return BbPromise.reject(new this.serverless.classes.Error(errorMessage)); } return BbPromise.resolve(deepValueToPopulateResolved); - }); + } + ); }); } @@ -678,8 +692,7 @@ class Variables { if (fileExtension !== 'js') { valueToPopulate = this.serverless.utils.readFileSync(referencedFileFullPath); if (matchedFileRefString !== variableString) { - let deepProperties = variableString - .replace(matchedFileRefString, ''); + let deepProperties = variableString.replace(matchedFileRefString, ''); if (deepProperties.substring(0, 1) !== ':') { const errorMessage = [ 'Invalid variable syntax when referencing', @@ -704,12 +717,10 @@ class Variables { if (!_.isUndefined(regionSuffix)) { options.region = regionSuffix; } - return this.serverless.getProvider('aws') - .request('CloudFormation', - 'describeStacks', - { StackName: stackName }, - options) - .then((result) => { + return this.serverless + .getProvider('aws') + .request('CloudFormation', 'describeStacks', { StackName: stackName }, options) + .then(result => { const outputs = result.Stacks[0].Outputs; const output = outputs.find(x => x.OutputKey === outputLogicalId); @@ -729,16 +740,19 @@ class Variables { const groups = variableString.match(this.s3RefSyntax); const bucket = groups[1]; const key = groups[2]; - return this.serverless.getProvider('aws').request( - 'S3', - 'getObject', - { - Bucket: bucket, - Key: key, - }, - { useCache: true }) // Use request cache + return this.serverless + .getProvider('aws') + .request( + 'S3', + 'getObject', + { + Bucket: bucket, + Key: key, + }, + { useCache: true } + ) // Use request cache .then(response => BbPromise.resolve(response.Body.toString())) - .catch((err) => { + .catch(err => { const errorMessage = `Error getting value for ${variableString}. ${err.message}`; return BbPromise.reject(new this.serverless.classes.Error(errorMessage)); }); @@ -747,15 +761,18 @@ class Variables { getValueFromSsm(variableString) { const groups = variableString.match(this.ssmRefSyntax); const param = groups[1]; - const decrypt = (groups[2] === 'true'); - return this.serverless.getProvider('aws').request( - 'SSM', - 'getParameter', - { - Name: param, - WithDecryption: decrypt, - }, - { useCache: true }) // Use request cache + const decrypt = groups[2] === 'true'; + return this.serverless + .getProvider('aws') + .request( + 'SSM', + 'getParameter', + { + Name: param, + WithDecryption: decrypt, + }, + { useCache: true } + ) // Use request cache .then(response => { const plainText = response.Parameter.Value; // Only if Secrets Manager. Parameter Store does not support JSON. @@ -769,7 +786,7 @@ class Variables { } return BbPromise.resolve(plainText); }) - .catch((err) => { + .catch(err => { if (err.statusCode !== 400) { return BbPromise.reject(new this.serverless.classes.Error(err.message)); } @@ -791,8 +808,9 @@ class Variables { const variable = this.getVariableFromDeep(variableString); const deepRef = variableString.replace(deepPrefixReplace, ''); let ret = this.populateValue(variable); - if (deepRef.length) { // if there is a deep reference remaining - ret = ret.then((result) => { + if (deepRef.length) { + // if there is a deep reference remaining + ret = ret.then(result => { if (_.isString(result) && result.match(this.variableSyntax)) { const deepVariable = this.makeDeepVariable(result); return BbPromise.resolve(this.appendDeepVariable(deepVariable, deepRef)); @@ -804,15 +822,13 @@ class Variables { } makeDeepVariable(variable) { - let index = this.deep.findIndex((item) => variable === item); + let index = this.deep.findIndex(item => variable === item); if (index < 0) { index = this.deep.push(variable) - 1; } const variableContainer = variable.match(this.variableSyntax)[0]; const variableString = this.cleanVariable(variableContainer).replace(/\s/g, ''); - return variableContainer - .replace(/\s/g, '') - .replace(variableString, `deep:${index}`); + return variableContainer.replace(/\s/g, '').replace(variableString, `deep:${index}`); } appendDeepVariable(variable, subProperty) { const variableString = this.cleanVariable(variable); @@ -834,22 +850,28 @@ class Variables { * will later resolve to the deeper value */ getDeeperValue(deepProperties, valueToPopulate) { - return BbPromise.reduce(deepProperties, (reducedValueParam, subProperty) => { - let reducedValue = reducedValueParam; - if (_.isString(reducedValue) && reducedValue.match(this.deepRefSyntax)) { // build mode - reducedValue = this.appendDeepVariable(reducedValue, subProperty); - } else { // get mode - if (typeof reducedValue === 'undefined') { - reducedValue = {}; - } else if (subProperty !== '' || '' in reducedValue) { - reducedValue = reducedValue[subProperty]; + return BbPromise.reduce( + deepProperties, + (reducedValueParam, subProperty) => { + let reducedValue = reducedValueParam; + if (_.isString(reducedValue) && reducedValue.match(this.deepRefSyntax)) { + // build mode + reducedValue = this.appendDeepVariable(reducedValue, subProperty); + } else { + // get mode + if (typeof reducedValue === 'undefined') { + reducedValue = {}; + } else if (subProperty !== '' || '' in reducedValue) { + reducedValue = reducedValue[subProperty]; + } + if (typeof reducedValue === 'string' && reducedValue.match(this.variableSyntax)) { + reducedValue = this.makeDeepVariable(reducedValue); + } } - if (typeof reducedValue === 'string' && reducedValue.match(this.variableSyntax)) { - reducedValue = this.makeDeepVariable(reducedValue); - } - } - return BbPromise.resolve(reducedValue); - }, valueToPopulate); + return BbPromise.resolve(reducedValue); + }, + valueToPopulate + ); } warnIfNotFound(variableString, valueToPopulate) { @@ -871,8 +893,9 @@ class Variables { varType = 'SSM parameter'; } if (!_.isUndefined(varType)) { - logWarning(`A valid ${varType} to satisfy the declaration '${ - variableString}' could not be found.`); + logWarning( + `A valid ${varType} to satisfy the declaration '${variableString}' could not be found.` + ); } } return valueToPopulate; diff --git a/lib/classes/Variables.test.js b/lib/classes/Variables.test.js index 7da83663b..e41352fe0 100644 --- a/lib/classes/Variables.test.js +++ b/lib/classes/Variables.test.js @@ -11,14 +11,16 @@ const proxyquire = require('proxyquire'); const sinon = require('sinon'); const YAML = require('js-yaml'); const _ = require('lodash'); +const overrideEnv = require('process-utils/override-env'); const AwsProvider = require('../plugins/aws/provider/awsProvider'); const fse = require('../utils/fs/fse'); const Serverless = require('../../lib/Serverless'); const slsError = require('./Error'); -const testUtils = require('../../tests/utils'); const Utils = require('../../lib/classes/Utils'); const Variables = require('../../lib/classes/Variables'); +const { getTmpDirPath } = require('../../tests/utils/fs'); +const { skipOnWindowsDisabledSymlinks } = require('../../tests/utils/misc'); BbPromise.longStackTraces(true); @@ -29,12 +31,17 @@ chai.should(); const expect = chai.expect; - describe('Variables', () => { let serverless; + let restoreEnv; + beforeEach(() => { + ({ restoreEnv } = overrideEnv()); serverless = new Serverless(); }); + + afterEach(() => restoreEnv()); + describe('#constructor()', () => { it('should attach serverless instance', () => { const variablesInstance = new Variables(serverless); @@ -56,74 +63,83 @@ describe('Variables', () => { }); describe('#populateService()', () => { - it('should remove problematic attributes bofore calling populateObjectImpl with the service', - () => { - const prepopulateServiceStub = sinon.stub(serverless.variables, 'prepopulateService') - .returns(BbPromise.resolve()); - const populateObjectStub = sinon.stub(serverless.variables, 'populateObjectImpl', (val) => { + it('should remove problematic attributes bofore calling populateObjectImpl with the service', () => { + const prepopulateServiceStub = sinon + .stub(serverless.variables, 'prepopulateService') + .returns(BbPromise.resolve()); + const populateObjectStub = sinon + .stub(serverless.variables, 'populateObjectImpl') + .callsFake(val => { expect(val).to.equal(serverless.service); expect(val.provider.variableSyntax).to.be.undefined; expect(val.serverless).to.be.undefined; return BbPromise.resolve(); }); - return serverless.variables.populateService().should.be.fulfilled - .then().finally(() => { - prepopulateServiceStub.restore(); - populateObjectStub.restore(); - }); - }); - it('should clear caches and remaining state *before* [pre]populating service', - () => { - const prepopulateServiceStub = sinon.stub(serverless.variables, 'prepopulateService', - (val) => { - expect(serverless.variables.deep).to.eql([]); - expect(serverless.variables.tracker.getAll()).to.eql([]); - return BbPromise.resolve(val); - }); - const populateObjectStub = sinon.stub(serverless.variables, 'populateObjectImpl', - (val) => { - expect(serverless.variables.deep).to.eql([]); - expect(serverless.variables.tracker.getAll()).to.eql([]); - return BbPromise.resolve(val); - }); - serverless.variables.deep.push('${foo:}'); - const prms = BbPromise.resolve('foo'); - serverless.variables.tracker.add('foo:', prms, '${foo:}'); - prms.state = 'resolved'; - return serverless.variables.populateService().should.be.fulfilled - .then().finally(() => { - prepopulateServiceStub.restore(); - populateObjectStub.restore(); - }); - }); - it('should clear caches and remaining *after* [pre]populating service', - () => { - const prepopulateServiceStub = sinon.stub(serverless.variables, 'prepopulateService', - (val) => { - serverless.variables.deep.push('${foo:}'); - const promise = BbPromise.resolve(val); - serverless.variables.tracker.add('foo:', promise, '${foo:}'); - promise.state = 'resolved'; - return BbPromise.resolve(); - }); - const populateObjectStub = sinon.stub(serverless.variables, 'populateObjectImpl', - (val) => { - serverless.variables.deep.push('${bar:}'); - const promise = BbPromise.resolve(val); - serverless.variables.tracker.add('bar:', promise, '${bar:}'); - promise.state = 'resolved'; - return BbPromise.resolve(); - }); - return serverless.variables.populateService().should.be.fulfilled - .then(() => { - expect(serverless.variables.deep).to.eql([]); - expect(serverless.variables.tracker.getAll()).to.eql([]); - }) - .finally(() => { - prepopulateServiceStub.restore(); - populateObjectStub.restore(); - }); - }); + return serverless.variables + .populateService() + .should.be.fulfilled.then() + .finally(() => { + prepopulateServiceStub.restore(); + populateObjectStub.restore(); + }); + }); + it('should clear caches and remaining state *before* [pre]populating service', () => { + const prepopulateServiceStub = sinon + .stub(serverless.variables, 'prepopulateService') + .callsFake(val => { + expect(serverless.variables.deep).to.eql([]); + expect(serverless.variables.tracker.getAll()).to.eql([]); + return BbPromise.resolve(val); + }); + const populateObjectStub = sinon + .stub(serverless.variables, 'populateObjectImpl') + .callsFake(val => { + expect(serverless.variables.deep).to.eql([]); + expect(serverless.variables.tracker.getAll()).to.eql([]); + return BbPromise.resolve(val); + }); + serverless.variables.deep.push('${foo:}'); + const prms = BbPromise.resolve('foo'); + serverless.variables.tracker.add('foo:', prms, '${foo:}'); + prms.state = 'resolved'; + return serverless.variables + .populateService() + .should.be.fulfilled.then() + .finally(() => { + prepopulateServiceStub.restore(); + populateObjectStub.restore(); + }); + }); + it('should clear caches and remaining *after* [pre]populating service', () => { + const prepopulateServiceStub = sinon + .stub(serverless.variables, 'prepopulateService') + .callsFake(val => { + serverless.variables.deep.push('${foo:}'); + const promise = BbPromise.resolve(val); + serverless.variables.tracker.add('foo:', promise, '${foo:}'); + promise.state = 'resolved'; + return BbPromise.resolve(); + }); + const populateObjectStub = sinon + .stub(serverless.variables, 'populateObjectImpl') + .callsFake(val => { + serverless.variables.deep.push('${bar:}'); + const promise = BbPromise.resolve(val); + serverless.variables.tracker.add('bar:', promise, '${bar:}'); + promise.state = 'resolved'; + return BbPromise.resolve(); + }); + return serverless.variables + .populateService() + .should.be.fulfilled.then(() => { + expect(serverless.variables.deep).to.eql([]); + expect(serverless.variables.tracker.getAll()).to.eql([]); + }) + .finally(() => { + prepopulateServiceStub.restore(); + populateObjectStub.restore(); + }); + }); }); describe('fallback', () => { @@ -131,7 +147,7 @@ describe('Variables', () => { serverless.variables.service.custom = { settings: '${self:nonExistent, "fallback"}', }; - return serverless.variables.populateService().should.be.fulfilled.then((result) => { + return serverless.variables.populateService().should.be.fulfilled.then(result => { expect(result.custom).to.be.deep.eql({ settings: 'fallback', }); @@ -142,7 +158,7 @@ describe('Variables', () => { serverless.variables.service.custom = { settings: '${opt:nonExistent, "fallback"}', }; - return serverless.variables.populateService().should.be.fulfilled.then((result) => { + return serverless.variables.populateService().should.be.fulfilled.then(result => { expect(result.custom).to.be.deep.eql({ settings: 'fallback', }); @@ -153,7 +169,7 @@ describe('Variables', () => { serverless.variables.service.custom = { settings: '${env:nonExistent, "fallback"}', }; - return serverless.variables.populateService().should.be.fulfilled.then((result) => { + return serverless.variables.populateService().should.be.fulfilled.then(result => { expect(result.custom).to.be.deep.eql({ settings: 'fallback', }); @@ -169,14 +185,17 @@ describe('Variables', () => { const fileExistsStub = sinon.stub(serverless.utils, 'fileExistsSync').returns(false); const realpathSync = sinon.stub(fse, 'realpathSync').returns(`${os.homedir()}/config.yml`); - return serverless.variables.populateService().should.be.fulfilled.then((result) => { - expect(result.custom).to.be.deep.eql({ - settings: 'fallback', + return serverless.variables + .populateService() + .should.be.fulfilled.then(result => { + expect(result.custom).to.be.deep.eql({ + settings: 'fallback', + }); + }) + .finally(() => { + fileExistsStub.restore(); + realpathSync.restore(); }); - }).finally(() => { - fileExistsStub.restore(); - realpathSync.restore(); - }); }); it('should fallback if file exists but given key not found and fallback is provided', () => { @@ -191,15 +210,18 @@ describe('Variables', () => { test2: 'test2', }); - return serverless.variables.populateService().should.be.fulfilled.then((result) => { - expect(result.custom).to.be.deep.eql({ - settings: 'fallback', + return serverless.variables + .populateService() + .should.be.fulfilled.then(result => { + expect(result.custom).to.be.deep.eql({ + settings: 'fallback', + }); + }) + .finally(() => { + fileExistsStub.restore(); + realpathSync.restore(); + readFileSyncStub.restore(); }); - }).finally(() => { - fileExistsStub.restore(); - realpathSync.restore(); - readFileSyncStub.restore(); - }); }); }); @@ -208,8 +230,9 @@ describe('Variables', () => { let requestStub; beforeEach(() => { awsProvider = new AwsProvider(serverless, {}); - requestStub = sinon.stub(awsProvider, 'request', () => - BbPromise.reject(new serverless.classes.Error('Not found.', 400))); + requestStub = sinon + .stub(awsProvider, 'request') + .callsFake(() => BbPromise.reject(new serverless.classes.Error('Not found.', 400))); }); afterEach(() => { requestStub.restore(); @@ -218,7 +241,7 @@ describe('Variables', () => { serverless.variables.service.custom = { settings: '${s3:bucket/key, "fallback"}', }; - return serverless.variables.populateService().should.be.fulfilled.then((result) => { + return serverless.variables.populateService().should.be.fulfilled.then(result => { expect(result.custom).to.be.deep.eql({ settings: 'fallback', }); @@ -229,7 +252,7 @@ describe('Variables', () => { serverless.variables.service.custom = { settings: '${cf:stack.value, "fallback"}', }; - return serverless.variables.populateService().should.be.fulfilled.then((result) => { + return serverless.variables.populateService().should.be.fulfilled.then(result => { expect(result.custom).to.be.deep.eql({ settings: 'fallback', }); @@ -240,7 +263,7 @@ describe('Variables', () => { serverless.variables.service.custom = { settings: '${ssm:/path/param, "fallback"}', }; - return serverless.variables.populateService().should.be.fulfilled.then((result) => { + return serverless.variables.populateService().should.be.fulfilled.then(result => { expect(result.custom).to.be.deep.eql({ settings: 'fallback', }); @@ -269,43 +292,48 @@ describe('Variables', () => { awsProvider = new AwsProvider(serverless, {}); populateObjectImplStub = sinon.stub(serverless.variables, 'populateObjectImpl'); populateObjectImplStub.withArgs(serverless.variables.service).returns(BbPromise.resolve()); - requestStub = sinon.stub(awsProvider, 'request', () => - BbPromise.reject(new Error('unexpected'))); + requestStub = sinon + .stub(awsProvider, 'request') + .callsFake(() => BbPromise.reject(new Error('unexpected'))); }); afterEach(() => { populateObjectImplStub.restore(); requestStub.restore(); }); const prepopulatedProperties = [ - { name: 'region', getter: (provider) => provider.getRegion() }, - { name: 'stage', getter: (provider) => provider.getStage() }, - { name: 'profile', getter: (provider) => provider.getProfile() }, + { name: 'region', getter: provider => provider.getRegion() }, + { name: 'stage', getter: provider => provider.getStage() }, + { name: 'profile', getter: provider => provider.getProfile() }, { name: 'credentials', - getter: (provider) => provider.serverless.service.provider.credentials, + getter: provider => provider.serverless.service.provider.credentials, }, { name: 'credentials.accessKeyId', - getter: (provider) => provider.serverless.service.provider.credentials.accessKeyId, + getter: provider => provider.serverless.service.provider.credentials.accessKeyId, }, { name: 'credentials.secretAccessKey', - getter: (provider) => provider.serverless.service.provider.credentials.secretAccessKey, + getter: provider => provider.serverless.service.provider.credentials.secretAccessKey, }, { name: 'credentials.sessionToken', - getter: (provider) => provider.serverless.service.provider.credentials.sessionToken, + getter: provider => provider.serverless.service.provider.credentials.sessionToken, }, ]; describe('basic population tests', () => { - prepopulatedProperties.forEach((property) => { + prepopulatedProperties.forEach(property => { it(`should populate variables in ${property.name} values`, () => { _.set( awsProvider.serverless.service.provider, property.name, - '${self:foobar, "default"}'); - return serverless.variables.populateService().should.be.fulfilled - .then(() => expect(property.getter(awsProvider)).to.be.eql('default')); + '${self:foobar, "default"}' + ); + return serverless.variables + .populateService() + .should.be.fulfilled.then(() => + expect(property.getter(awsProvider)).to.be.eql('default') + ); }); }); }); @@ -320,7 +348,8 @@ describe('Variables', () => { dependentConfigs.forEach(config => { it(`should reject ${config.name} variables in ${property.name} values`, () => { _.set(awsProvider.serverless.service.provider, property.name, config.value); - return serverless.variables.populateService() + return serverless.variables + .populateService() .should.be.rejectedWith('Variable dependency failure'); }); it(`should reject recursively dependent ${config.name} service dependencies`, () => { @@ -328,7 +357,8 @@ describe('Variables', () => { settings: config.value, }; awsProvider.serverless.service.provider.region = '${self:custom.settings.region}'; - return serverless.variables.populateService() + return serverless.variables + .populateService() .should.be.rejectedWith('Variable dependency failure'); }); }); @@ -341,7 +371,7 @@ describe('Variables', () => { { region: '${self:foo, "foo"}', state: 'bar' }, { region: '${self:foo, "foo"}', state: '${self:bar, "bar"}' }, ]; - stateCombinations.forEach((combination) => { + stateCombinations.forEach(combination => { it('must leave the dependent services in their original state', () => { const dependentMethods = [ { name: 'getValueFromCf', original: serverless.variables.getValueFromCf }, @@ -350,12 +380,11 @@ describe('Variables', () => { ]; awsProvider.serverless.service.provider.region = combination.region; awsProvider.serverless.service.provider.state = combination.state; - return serverless.variables.populateService().should.be.fulfilled - .then(() => { - dependentMethods.forEach((method) => { - expect(serverless.variables[method.name]).to.equal(method.original); - }); + return serverless.variables.populateService().should.be.fulfilled.then(() => { + dependentMethods.forEach(method => { + expect(serverless.variables[method.name]).to.equal(method.original); }); + }); }); }); }); @@ -371,10 +400,7 @@ describe('Variables', () => { bar: 'baz', biz: 'buz', }, - b: [ - { c: 'd' }, - { e: 'f' }, - ], + b: [{ c: 'd' }, { e: 'f' }], g: date, h: regex, i: func, @@ -414,9 +440,11 @@ describe('Variables', () => { sinon.stub(serverless.variables, 'populateValue').resolves('prod'); - return serverless.variables.populateObject(object).then((populatedObject) => { - expect(populatedObject).to.deep.equal(expectedPopulatedObject); - }) + return serverless.variables + .populateObject(object) + .then(populatedObject => { + expect(populatedObject).to.deep.equal(expectedPopulatedObject); + }) .finally(() => serverless.variables.populateValue.restore()); }); @@ -429,12 +457,15 @@ describe('Variables', () => { stage: 'prod', }; expectedPopulatedObject['some.nested.key'] = 'hello'; - const populateValueStub = sinon.stub(serverless.variables, 'populateValue', + const populateValueStub = sinon.stub(serverless.variables, 'populateValue').callsFake( // eslint-disable-next-line no-template-curly-in-string - val => (val === '${opt:stage}' ? BbPromise.resolve('prod') : BbPromise.resolve(val))); - return serverless.variables.populateObject(object) + val => (val === '${opt:stage}' ? BbPromise.resolve('prod') : BbPromise.resolve(val)) + ); + return serverless.variables + .populateObject(object) .should.become(expectedPopulatedObject) - .then().finally(() => populateValueStub.restore()); + .then() + .finally(() => populateValueStub.restore()); }); describe('significant variable usage corner cases', () => { let service; @@ -460,9 +491,11 @@ describe('Variables', () => { expected.custom = { me: expected, }; - return expect(serverless.variables.populateObject(service).then((result) => { - expect(jc.stringify(result)).to.eql(jc.stringify(expected)); - })).to.be.fulfilled; + return expect( + serverless.variables.populateObject(service).then(result => { + expect(jc.stringify(result)).to.eql(jc.stringify(expected)); + }) + ).to.be.fulfilled; }); it('should properly populate embedded variables', () => { service.custom = { @@ -475,9 +508,11 @@ describe('Variables', () => { val1: '0', val2: 'my value 0', }; - return expect(serverless.variables.populateObject(service.custom).then((result) => { - expect(result).to.eql(expected); - })).to.be.fulfilled; + return expect( + serverless.variables.populateObject(service.custom).then(result => { + expect(result).to.eql(expected); + }) + ).to.be.fulfilled; }); it('should properly populate an overwrite with a default value that is a string', () => { service.custom = { @@ -488,9 +523,11 @@ describe('Variables', () => { val0: 'my value', val1: 'string', }; - return expect(serverless.variables.populateObject(service.custom).then((result) => { - expect(result).to.eql(expected); - })).to.be.fulfilled; + return expect( + serverless.variables.populateObject(service.custom).then(result => { + expect(result).to.eql(expected); + }) + ).to.be.fulfilled; }); it('should properly populate an overwrite with a default value that is the string *', () => { service.custom = { @@ -501,9 +538,11 @@ describe('Variables', () => { val0: 'my value', val1: '*', }; - return expect(serverless.variables.populateObject(service.custom).then((result) => { - expect(result).to.eql(expected); - })).to.be.fulfilled; + return expect( + serverless.variables.populateObject(service.custom).then(result => { + expect(result).to.eql(expected); + }) + ).to.be.fulfilled; }); it('should properly populate an overwrite with a default value that is a string w/*', () => { service.custom = { @@ -514,9 +553,11 @@ describe('Variables', () => { val0: 'my value', val1: 'foo*', }; - return expect(serverless.variables.populateObject(service.custom).then((result) => { - expect(result).to.eql(expected); - })).to.be.fulfilled; + return expect( + serverless.variables.populateObject(service.custom).then(result => { + expect(result).to.eql(expected); + }) + ).to.be.fulfilled; }); it('should properly populate overwrites where the first value is valid', () => { service.custom = { @@ -527,18 +568,22 @@ describe('Variables', () => { val0: 'my value', val1: 'my value', }; - return expect(serverless.variables.populateObject(service.custom).then((result) => { - expect(result).to.eql(expected); - })).to.be.fulfilled; + return expect( + serverless.variables.populateObject(service.custom).then(result => { + expect(result).to.eql(expected); + }) + ).to.be.fulfilled; }); it('should do nothing useful on * when not wrapped in quotes', () => { service.custom = { val0: '${self:custom.*}', }; const expected = { val0: undefined }; - return expect(serverless.variables.populateObject(service.custom).then((result) => { - expect(result).to.eql(expected); - })).to.be.fulfilled; + return expect( + serverless.variables.populateObject(service.custom).then(result => { + expect(result).to.eql(expected); + }) + ).to.be.fulfilled; }); it('should properly populate overwrites where the middle value is valid', () => { service.custom = { @@ -549,9 +594,11 @@ describe('Variables', () => { val0: 'my value', val1: 'my value', }; - return expect(serverless.variables.populateObject(service.custom).then((result) => { - expect(result).to.eql(expected); - })).to.be.fulfilled; + return expect( + serverless.variables.populateObject(service.custom).then(result => { + expect(result).to.eql(expected); + }) + ).to.be.fulfilled; }); it('should properly populate overwrites where the last value is valid', () => { service.custom = { @@ -562,9 +609,11 @@ describe('Variables', () => { val0: 'my value', val1: 'my value', }; - return expect(serverless.variables.populateObject(service.custom).then((result) => { - expect(result).to.eql(expected); - })).to.be.fulfilled; + return expect( + serverless.variables.populateObject(service.custom).then(result => { + expect(result).to.eql(expected); + }) + ).to.be.fulfilled; }); it('should properly populate overwrites with nested variables in the first value', () => { service.custom = { @@ -577,9 +626,11 @@ describe('Variables', () => { val1: 0, val2: 'my value', }; - return expect(serverless.variables.populateObject(service.custom).then((result) => { - expect(result).to.eql(expected); - })).to.be.fulfilled; + return expect( + serverless.variables.populateObject(service.custom).then(result => { + expect(result).to.eql(expected); + }) + ).to.be.fulfilled; }); it('should properly populate overwrites with nested variables in the middle value', () => { service.custom = { @@ -592,9 +643,11 @@ describe('Variables', () => { val1: 0, val2: 'my value', }; - return expect(serverless.variables.populateObject(service.custom).then((result) => { - expect(result).to.eql(expected); - })).to.be.fulfilled; + return expect( + serverless.variables.populateObject(service.custom).then(result => { + expect(result).to.eql(expected); + }) + ).to.be.fulfilled; }); it('should properly populate overwrites with nested variables in the last value', () => { service.custom = { @@ -607,9 +660,11 @@ describe('Variables', () => { val1: 0, val2: 'my value', }; - return expect(serverless.variables.populateObject(service.custom).then((result) => { - expect(result).to.eql(expected); - })).to.be.fulfilled; + return expect( + serverless.variables.populateObject(service.custom).then(result => { + expect(result).to.eql(expected); + }) + ).to.be.fulfilled; }); it('should properly replace duplicate variable declarations', () => { service.custom = { @@ -622,9 +677,11 @@ describe('Variables', () => { val1: 'my value', val2: 'my value', }; - return expect(serverless.variables.populateObject(service.custom).then((result) => { - expect(result).to.eql(expected); - })).to.be.fulfilled; + return expect( + serverless.variables.populateObject(service.custom).then(result => { + expect(result).to.eql(expected); + }) + ).to.be.fulfilled; }); it('should recursively populate, regardless of order and duplication', () => { service.custom = { @@ -639,9 +696,11 @@ describe('Variables', () => { val0: 'my value', val2: 'my value', }; - return expect(serverless.variables.populateObject(service.custom).then((result) => { - expect(result).to.eql(expected); - })).to.be.fulfilled; + return expect( + serverless.variables.populateObject(service.custom).then(result => { + expect(result).to.eql(expected); + }) + ).to.be.fulfilled; }); // see https://github.com/serverless/serverless/pull/4713#issuecomment-366975172 it('should handle deep references into deep variables', () => { @@ -668,8 +727,7 @@ describe('Variables', () => { SECRET: 'secret', }, }; - return serverless.variables.populateObject(service.custom) - .should.become(expected); + return serverless.variables.populateObject(service.custom).should.become(expected); }); it('should handle deep variables that reference overrides', () => { service.custom = { @@ -680,8 +738,7 @@ describe('Variables', () => { val1: 'bar', val2: 'bar', }; - return serverless.variables.populateObject(service.custom) - .should.become(expected); + return serverless.variables.populateObject(service.custom).should.become(expected); }); it('should handle deep references into deep variables', () => { service.custom = { @@ -700,8 +757,7 @@ describe('Variables', () => { }, val2: 'bar', }; - return serverless.variables.populateObject(service.custom) - .should.become(expected); + return serverless.variables.populateObject(service.custom).should.become(expected); }); it('should handle deep variables that reference overrides', () => { service.custom = { @@ -712,8 +768,7 @@ describe('Variables', () => { val1: 'bar', val2: 'foobar', }; - return serverless.variables.populateObject(service.custom) - .should.become(expected); + return serverless.variables.populateObject(service.custom).should.become(expected); }); it('should handle referenced deep variables that reference overrides', () => { service.custom = { @@ -726,8 +781,7 @@ describe('Variables', () => { val2: 'bar', val3: 'bar', }; - return serverless.variables.populateObject(service.custom) - .should.become(expected); + return serverless.variables.populateObject(service.custom).should.become(expected); }); it('should handle partial referenced deep variables that reference overrides', () => { service.custom = { @@ -740,8 +794,7 @@ describe('Variables', () => { val2: 'bar', val3: 'foobar', }; - return serverless.variables.populateObject(service.custom) - .should.become(expected); + return serverless.variables.populateObject(service.custom).should.become(expected); }); it('should handle referenced contained deep variables that reference overrides', () => { service.custom = { @@ -754,8 +807,7 @@ describe('Variables', () => { val2: 'foobar', val3: 'foobar', }; - return serverless.variables.populateObject(service.custom) - .should.become(expected); + return serverless.variables.populateObject(service.custom).should.become(expected); }); it('should handle multiple referenced contained deep variables referencing overrides', () => { service.custom = { @@ -770,8 +822,7 @@ describe('Variables', () => { val2: 'foo:bar', val3: 'foo:bar', }; - return serverless.variables.populateObject(service.custom) - .should.become(expected); + return serverless.variables.populateObject(service.custom).should.become(expected); }); it('should handle overrides that are populated by unresolvable deep variables', () => { service.custom = { @@ -784,8 +835,7 @@ describe('Variables', () => { val1: 'foo', val2: 'fallback', }; - return serverless.variables.populateObject(service.custom) - .should.become(expected); + return serverless.variables.populateObject(service.custom).should.become(expected); }); it('should handle embedded deep variable replacements in overrides', () => { service.custom = { @@ -800,8 +850,7 @@ describe('Variables', () => { val1: 'foo', val2: 'bar', }; - return serverless.variables.populateObject(service.custom) - .should.become(expected); + return serverless.variables.populateObject(service.custom).should.become(expected); }); it('should deal with overwites that reference embedded deep references', () => { service.custom = { @@ -818,8 +867,7 @@ describe('Variables', () => { val3: 'val', val4: 'val', }; - return serverless.variables.populateObject(service.custom) - .should.become(expected); + return serverless.variables.populateObject(service.custom).should.become(expected); }); it('should preserve whitespace in double-quote literal fallback', () => { service.custom = { @@ -828,18 +876,16 @@ describe('Variables', () => { const expected = { val0: 'rate(3 hours)', }; - return serverless.variables.populateObject(service.custom) - .should.become(expected); + return serverless.variables.populateObject(service.custom).should.become(expected); }); it('should preserve whitespace in single-quote literal fallback', () => { service.custom = { - val0: '${self:custom.val, \'rate(1 hour)\'}', + val0: "${self:custom.val, 'rate(1 hour)'}", }; const expected = { val0: 'rate(1 hour)', }; - return serverless.variables.populateObject(service.custom) - .should.become(expected); + return serverless.variables.populateObject(service.custom).should.become(expected); }); it('should accept whitespace in variables', () => { service.custom = { @@ -850,8 +896,7 @@ describe('Variables', () => { val: 'foobar', val0: 'foobar', }; - return serverless.variables.populateObject(service.custom) - .should.become(expected); + return serverless.variables.populateObject(service.custom).should.become(expected); }); it('should handle deep variables regardless of custom variableSyntax', () => { service.provider.variableSyntax = '\\${{([ ~:a-zA-Z0-9._@\\\'",\\-\\/\\(\\)*]+?)}}'; @@ -867,8 +912,7 @@ describe('Variables', () => { my1stStage: 'DEV', my2ndStage: 'DEV', }; - return serverless.variables.populateObject(service.custom) - .should.become(expected); + return serverless.variables.populateObject(service.custom).should.become(expected); }); it('should handle deep variable continuations regardless of custom variableSyntax', () => { service.provider.variableSyntax = '\\${{([ ~:a-zA-Z0-9._@\\\'",\\-\\/\\(\\)]+?)}}'; @@ -884,8 +928,7 @@ describe('Variables', () => { my1stStage: { we: 'DEV' }, my2ndStage: 'DEV', }; - return serverless.variables.populateObject(service.custom) - .should.become(expected); + return serverless.variables.populateObject(service.custom).should.become(expected); }); it('should handle deep variables regardless of recursion into custom variableSyntax', () => { service.provider.variableSyntax = '\\${{([ ~:a-zA-Z0-9._@\\\'",\\-\\/\\(\\)*]+?)}}'; @@ -905,8 +948,7 @@ describe('Variables', () => { my1stStage: 'DEV', my2ndStage: 'DEV', }; - return serverless.variables.populateObject(service.custom) - .should.become(expected); + return serverless.variables.populateObject(service.custom).should.become(expected); }); it('should handle deep variables in complex recursions of custom variableSyntax', () => { service.provider.variableSyntax = '\\${{([ ~:a-zA-Z0-9._@\\\'",\\-\\/\\(\\)*]+?)}}'; @@ -926,13 +968,12 @@ describe('Variables', () => { s2: 'I am a DEV! A DEV!', s3: 'DEV!, I am a DEV! DEV!, I am a DEV! A DEV!', }; - return serverless.variables.populateObject(service.custom) - .should.become(expected); + return serverless.variables.populateObject(service.custom).should.become(expected); }); describe('file reading cases', () => { let tmpDirPath; beforeEach(() => { - tmpDirPath = testUtils.getTmpDirPath(); + tmpDirPath = getTmpDirPath(); fse.mkdirsSync(tmpDirPath); serverless.config.update({ servicePath: tmpDirPath }); }); @@ -977,8 +1018,7 @@ module.exports = { val2: 'my-async-value-1', val0: 'my-async-value-1', }; - return serverless.variables.populateObject(service.custom) - .should.become(expected); + return serverless.variables.populateObject(service.custom).should.become(expected); }); it('should still work with a default file name in double or single quotes', () => { makeTempFile(asyncFileName, asyncContent); @@ -994,109 +1034,98 @@ module.exports = { val3: 'my-async-value-1', val0: 'my-async-value-1', }; - return serverless.variables.populateObject(service.custom) - .should.become(expected); + return serverless.variables.populateObject(service.custom).should.become(expected); }); - it('should populate any given variable only once regardless of ordering or reference count', - () => { - makeTempFile(asyncFileName, asyncContent); - service.custom = { - val9: '${self:custom.val7}', // eslint-disable-line no-template-curly-in-string - val7: '${self:custom.val5}', // eslint-disable-line no-template-curly-in-string - val5: '${self:custom.val3}', // eslint-disable-line no-template-curly-in-string - val3: '${self:custom.val1}', // eslint-disable-line no-template-curly-in-string - val1: '${self:custom.val0}', // eslint-disable-line no-template-curly-in-string - val2: '${self:custom.val1}', // eslint-disable-line no-template-curly-in-string - val4: '${self:custom.val3}', // eslint-disable-line no-template-curly-in-string - val6: '${self:custom.val5}', // eslint-disable-line no-template-curly-in-string - val8: '${self:custom.val7}', // eslint-disable-line no-template-curly-in-string - val0: `\${file(${asyncFileName}):str}`, - }; - const expected = { - val9: 'my-async-value-1', - val7: 'my-async-value-1', - val5: 'my-async-value-1', - val3: 'my-async-value-1', - val1: 'my-async-value-1', - val2: 'my-async-value-1', - val4: 'my-async-value-1', - val6: 'my-async-value-1', - val8: 'my-async-value-1', + it('should populate any given variable only once regardless of ordering or reference count', () => { + makeTempFile(asyncFileName, asyncContent); + service.custom = { + val9: '${self:custom.val7}', // eslint-disable-line no-template-curly-in-string + val7: '${self:custom.val5}', // eslint-disable-line no-template-curly-in-string + val5: '${self:custom.val3}', // eslint-disable-line no-template-curly-in-string + val3: '${self:custom.val1}', // eslint-disable-line no-template-curly-in-string + val1: '${self:custom.val0}', // eslint-disable-line no-template-curly-in-string + val2: '${self:custom.val1}', // eslint-disable-line no-template-curly-in-string + val4: '${self:custom.val3}', // eslint-disable-line no-template-curly-in-string + val6: '${self:custom.val5}', // eslint-disable-line no-template-curly-in-string + val8: '${self:custom.val7}', // eslint-disable-line no-template-curly-in-string + val0: `\${file(${asyncFileName}):str}`, + }; + const expected = { + val9: 'my-async-value-1', + val7: 'my-async-value-1', + val5: 'my-async-value-1', + val3: 'my-async-value-1', + val1: 'my-async-value-1', + val2: 'my-async-value-1', + val4: 'my-async-value-1', + val6: 'my-async-value-1', + val8: 'my-async-value-1', + val0: 'my-async-value-1', + }; + return serverless.variables.populateObject(service.custom).should.become(expected); + }); + it('should populate async objects with contained variables', () => { + makeTempFile(asyncFileName, asyncContent); + serverless.variables.options = { + stage: 'dev', + }; + service.custom = { + obj: `\${file(${asyncFileName}):obj}`, + }; + const expected = { + obj: { val0: 'my-async-value-1', - }; - return serverless.variables.populateObject(service.custom) - .should.become(expected); - }); - it('should populate async objects with contained variables', - () => { - makeTempFile(asyncFileName, asyncContent); - serverless.variables.options = { - stage: 'dev', - }; - service.custom = { - obj: `\${file(${asyncFileName}):obj}`, - }; - const expected = { - obj: { - val0: 'my-async-value-1', - val1: 'dev', - }, - }; - return serverless.variables.populateObject(service.custom) - .should.become(expected); - }); - it('should populate variables from filesnames including \'@\', e.g scoped npm packages', - () => { - const fileName = `./node_modules/@scoped-org/${asyncFileName}`; - makeTempFile( - fileName, - asyncContent - ); - service.custom = { - val0: `\${file(${fileName}):str}`, - }; - const expected = { - val0: 'my-async-value-1', - }; - return serverless.variables - .populateObject(service.custom) - .should.become(expected); - }); + val1: 'dev', + }, + }; + return serverless.variables.populateObject(service.custom).should.become(expected); + }); + it("should populate variables from filesnames including '@', e.g scoped npm packages", () => { + const fileName = `./node_modules/@scoped-org/${asyncFileName}`; + makeTempFile(fileName, asyncContent); + service.custom = { + val0: `\${file(${fileName}):str}`, + }; + const expected = { + val0: 'my-async-value-1', + }; + return serverless.variables.populateObject(service.custom).should.become(expected); + }); const selfFileName = 'self.yml'; const selfContent = `foo: baz bar: \${self:custom.self.foo} `; - it('should populate a "cyclic" reference across an unresolved dependency (issue #4687)', - () => { - makeTempFile(selfFileName, selfContent); - service.custom = { - self: `\${file(${selfFileName})}`, - }; - const expected = { - self: { - foo: 'baz', - bar: 'baz', - }, - }; - return serverless.variables.populateObject(service.custom) - .should.become(expected); - }); + it('should populate a "cyclic" reference across an unresolved dependency (issue #4687)', () => { + makeTempFile(selfFileName, selfContent); + service.custom = { + self: `\${file(${selfFileName})}`, + }; + const expected = { + self: { + foo: 'baz', + bar: 'baz', + }, + }; + return serverless.variables.populateObject(service.custom).should.become(expected); + }); const emptyFileName = 'empty.js'; const emptyContent = `'use strict'; module.exports = { func: () => ({ value: 'a value' }), } `; - it('should reject population of an attribute not exported from a file', - () => { - makeTempFile(emptyFileName, emptyContent); - service.custom = { - val: `\${file(${emptyFileName}):func.notAValue}`, - }; - return serverless.variables.populateObject(service.custom) - .should.be.rejectedWith(serverless.classes.Error, - 'Invalid variable syntax when referencing file'); - }); + it('should reject population of an attribute not exported from a file', () => { + makeTempFile(emptyFileName, emptyContent); + service.custom = { + val: `\${file(${emptyFileName}):func.notAValue}`, + }; + return serverless.variables + .populateObject(service.custom) + .should.be.rejectedWith( + serverless.classes.Error, + 'Invalid variable syntax when referencing file' + ); + }); }); }); }); @@ -1111,7 +1140,8 @@ module.exports = { const property = 'my stage is ${opt:stage, self:provider.stage}'; serverless.variables.options = { stage: 'dev' }; serverless.service.provider.stage = 'prod'; - return serverless.variables.populateProperty(property) + return serverless.variables + .populateProperty(property) .should.eventually.eql('my stage is dev'); }); @@ -1119,7 +1149,8 @@ module.exports = { // eslint-disable-next-line no-template-curly-in-string const property = "my stage is ${opt:stage, 'prod'}"; serverless.variables.options = {}; - return serverless.variables.populateProperty(property) + return serverless.variables + .populateProperty(property) .should.eventually.eql('my stage is prod'); }); @@ -1127,7 +1158,8 @@ module.exports = { // eslint-disable-next-line no-template-curly-in-string const property = 'my stage is ${opt:stage, "prod"}'; serverless.variables.options = {}; - return serverless.variables.populateProperty(property) + return serverless.variables + .populateProperty(property) .should.eventually.eql('my stage is prod'); }); @@ -1135,7 +1167,8 @@ module.exports = { // eslint-disable-next-line no-template-curly-in-string const property = 'my stage is ${opt:stage}'; serverless.variables.options = { stage: 'prod' }; - return serverless.variables.populateProperty(property) + return serverless.variables + .populateProperty(property) .should.eventually.eql('my stage is prod'); }); @@ -1149,10 +1182,13 @@ module.exports = { const param = '/some/path/to/invalidparam'; const property = `\${ssm:${param}}`; const error = new serverless.classes.Error(`Parameter ${param} not found.`, 400); - const requestStub = sinon.stub(awsProvider, 'request', () => BbPromise.reject(error)); + const requestStub = sinon + .stub(awsProvider, 'request') + .callsFake(() => BbPromise.reject(error)); const warnIfNotFoundSpy = sinon.spy(serverless.variables, 'warnIfNotFound'); - return serverless.variables.populateProperty(property) + return serverless.variables + .populateProperty(property) .should.become(undefined) .then(() => { expect(requestStub.callCount).to.equal(1); @@ -1174,8 +1210,11 @@ module.exports = { const param = '/some/path/to/invalidparam'; const property = `\${ssm:${param}}`; const error = new serverless.classes.Error('Some random failure.', 123); - const requestStub = sinon.stub(awsProvider, 'request', () => BbPromise.reject(error)); - return serverless.variables.populateProperty(property) + const requestStub = sinon + .stub(awsProvider, 'request') + .callsFake(() => BbPromise.reject(error)); + return serverless.variables + .populateProperty(property) .should.be.rejectedWith(serverless.classes.Error) .then(() => expect(requestStub.callCount).to.equal(1)) .finally(() => requestStub.restore()); @@ -1186,10 +1225,11 @@ module.exports = { const property = 'my stage is ${env:${opt:name}}'; process.env.TEST_VAR = 'dev'; serverless.variables.options = { name: 'TEST_VAR' }; - return serverless.variables.populateProperty(property) - .should.eventually.eql('my stage is dev') - .then().finally(() => { delete process.env.TEST_VAR; }); + return serverless.variables + .populateProperty(property) + .should.eventually.eql('my stage is dev'); }); + it('should run recursively through many nested variables', () => { // eslint-disable-next-line no-template-curly-in-string const property = 'my stage is ${env:${opt:name}}'; @@ -1204,9 +1244,9 @@ module.exports = { lvl5: 'A${opt:lvl6}', lvl6: 'R', }; - return serverless.variables.populateProperty(property) - .should.eventually.eql('my stage is dev') - .then().finally(() => { delete process.env.TEST_VAR; }); + return serverless.variables + .populateProperty(property) + .should.eventually.eql('my stage is dev'); }); }); @@ -1216,7 +1256,8 @@ module.exports = { const matchedString = '${opt:stage}'; // eslint-disable-line no-template-curly-in-string // eslint-disable-next-line no-template-curly-in-string const property = 'my stage is ${opt:stage}'; - serverless.variables.populateVariable(property, matchedString, valueToPopulate) + serverless.variables + .populateVariable(property, matchedString, valueToPopulate) .should.eql('my stage is dev'); }); @@ -1225,7 +1266,8 @@ module.exports = { const matchedString = '${opt:number}'; // eslint-disable-line no-template-curly-in-string // eslint-disable-next-line no-template-curly-in-string const property = 'your account number is ${opt:number}'; - serverless.variables.populateVariable(property, matchedString, valueToPopulate) + serverless.variables + .populateVariable(property, matchedString, valueToPopulate) .should.eql('your account number is 5'); }); @@ -1233,7 +1275,8 @@ module.exports = { const valueToPopulate = 5; const matchedString = '${opt:number}'; // eslint-disable-line no-template-curly-in-string const property = '${opt:number}'; // eslint-disable-line no-template-curly-in-string - return serverless.variables.populateVariable(property, matchedString, valueToPopulate) + return serverless.variables + .populateVariable(property, matchedString, valueToPopulate) .should.equal(5); }); @@ -1243,8 +1286,8 @@ module.exports = { // eslint-disable-next-line no-template-curly-in-string const property = 'your account number is ${opt:object}'; return expect(() => - serverless.variables.populateVariable(property, matchedString, valueToPopulate)) - .to.throw(serverless.classes.Error); + serverless.variables.populateVariable(property, matchedString, valueToPopulate) + ).to.throw(serverless.classes.Error); }); }); @@ -1276,19 +1319,12 @@ module.exports = { }); it('should ignore quoted commas', () => { const input = '",", \',\', ",\', \',\'", "\',\', \',\'", \',", ","\', \'",", ","\''; - const expected = [ - '","', - '\',\'', - '",\', \',\'"', - '"\',\', \',\'"', - '\',", ","\'', - '\'",", ","\'', - ]; + const expected = ['","', "','", "\",', ','\"", "\"',', ','\"", '\',", ","\'', '\'",", ","\'']; expect(serverless.variables.splitByComma(input)).to.eql(expected); }); it('should deal with a combination of these cases', () => { - const input = ' \t\n\'a\'\t\n , \n\t"foo,bar", opt:foo, ",", \',\', "\',\', \',\'", foo\n\t '; - const expected = ['\'a\'', '"foo,bar"', 'opt:foo', '","', '\',\'', '"\',\', \',\'"', 'foo']; + const input = " \t\n'a'\t\n , \n\t\"foo,bar\", opt:foo, \",\", ',', \"',', ','\", foo\n\t "; + const expected = ["'a'", '"foo,bar"', 'opt:foo', '","', "','", "\"',', ','\"", 'foo']; expect(serverless.variables.splitByComma(input)).to.eql(expected); }); }); @@ -1304,9 +1340,9 @@ module.exports = { getValueFromSourceStub.onCall(0).resolves(undefined); getValueFromSourceStub.onCall(1).resolves(null); getValueFromSourceStub.onCall(2).resolves('variableValue'); - return serverless.variables.overwrite(['opt:stage', 'env:stage', 'self:provider.stage']) - .should.be.fulfilled - .then((valueToPopulate) => { + return serverless.variables + .overwrite(['opt:stage', 'env:stage', 'self:provider.stage']) + .should.be.fulfilled.then(valueToPopulate => { expect(valueToPopulate).to.equal('variableValue'); expect(getValueFromSourceStub).to.have.been.calledThrice; }) @@ -1317,8 +1353,9 @@ module.exports = { const getValueFromSourceStub = sinon.stub(serverless.variables, 'getValueFromSource'); getValueFromSourceStub.onCall(0).resolves({}); getValueFromSourceStub.onCall(1).resolves('variableValue'); - return serverless.variables.overwrite(['opt:stage', 'env:stage']).should.be.fulfilled - .then((valueToPopulate) => { + return serverless.variables + .overwrite(['opt:stage', 'env:stage']) + .should.be.fulfilled.then(valueToPopulate => { expect(valueToPopulate).to.equal('variableValue'); expect(getValueFromSourceStub).to.have.been.calledTwice; }) @@ -1329,16 +1366,22 @@ module.exports = { const getValueFromSourceStub = sinon.stub(serverless.variables, 'getValueFromSource'); getValueFromSourceStub.onCall(0).resolves(0); getValueFromSourceStub.onCall(1).resolves('variableValue'); - return serverless.variables.overwrite(['opt:stage', 'env:stage']).should.become(0) - .then().finally(() => getValueFromSourceStub.restore()); + return serverless.variables + .overwrite(['opt:stage', 'env:stage']) + .should.become(0) + .then() + .finally(() => getValueFromSourceStub.restore()); }); it('should not overwrite false values', () => { const getValueFromSourceStub = sinon.stub(serverless.variables, 'getValueFromSource'); getValueFromSourceStub.onCall(0).resolves(false); getValueFromSourceStub.onCall(1).resolves('variableValue'); - return serverless.variables.overwrite(['opt:stage', 'env:stage']).should.become(false) - .then().finally(() => getValueFromSourceStub.restore()); + return serverless.variables + .overwrite(['opt:stage', 'env:stage']) + .should.become(false) + .then() + .finally(() => getValueFromSourceStub.restore()); }); it('should skip getting values once a value has been found', () => { @@ -1346,18 +1389,23 @@ module.exports = { getValueFromSourceStub.onCall(0).resolves(undefined); getValueFromSourceStub.onCall(1).resolves('variableValue'); getValueFromSourceStub.onCall(2).resolves('variableValue2'); - return serverless.variables.overwrite(['opt:stage', 'env:stage', 'self:provider.stage']) - .should.be.fulfilled - .then(valueToPopulate => expect(valueToPopulate).to.equal('variableValue')) + return serverless.variables + .overwrite(['opt:stage', 'env:stage', 'self:provider.stage']) + .should.be.fulfilled.then(valueToPopulate => + expect(valueToPopulate).to.equal('variableValue') + ) .finally(() => getValueFromSourceStub.restore()); }); it('should properly handle string values containing commas', () => { const str = '"foo,bar"'; - const getValueFromSourceStub = sinon.stub(serverless.variables, 'getValueFromSource') + const getValueFromSourceStub = sinon + .stub(serverless.variables, 'getValueFromSource') .resolves(undefined); - return serverless.variables.overwrite(['opt:stage', str]) - .should.be.fulfilled - .then(() => expect(getValueFromSourceStub.getCall(1).args[0]).to.eql(str)) + return serverless.variables + .overwrite(['opt:stage', str]) + .should.be.fulfilled.then(() => + expect(getValueFromSourceStub.getCall(1).args[0]).to.eql(str) + ) .finally(() => getValueFromSourceStub.restore()); }); }); @@ -1374,21 +1422,29 @@ module.exports = { let getValueFromSsmStub; beforeEach(() => { - getValueFromSlsStub = sinon.stub(serverless.variables, 'getValueFromSls') + getValueFromSlsStub = sinon + .stub(serverless.variables, 'getValueFromSls') .resolves('variableValue'); - getValueFromEnvStub = sinon.stub(serverless.variables, 'getValueFromEnv') + getValueFromEnvStub = sinon + .stub(serverless.variables, 'getValueFromEnv') .resolves('variableValue'); - getValueFromOptionsStub = sinon.stub(serverless.variables, 'getValueFromOptions') + getValueFromOptionsStub = sinon + .stub(serverless.variables, 'getValueFromOptions') .resolves('variableValue'); - getValueFromSelfStub = sinon.stub(serverless.variables, 'getValueFromSelf') + getValueFromSelfStub = sinon + .stub(serverless.variables, 'getValueFromSelf') .resolves('variableValue'); - getValueFromFileStub = sinon.stub(serverless.variables, 'getValueFromFile') + getValueFromFileStub = sinon + .stub(serverless.variables, 'getValueFromFile') .resolves('variableValue'); - getValueFromCfStub = sinon.stub(serverless.variables, 'getValueFromCf') + getValueFromCfStub = sinon + .stub(serverless.variables, 'getValueFromCf') .resolves('variableValue'); - getValueFromS3Stub = sinon.stub(serverless.variables, 'getValueFromS3') + getValueFromS3Stub = sinon + .stub(serverless.variables, 'getValueFromS3') .resolves('variableValue'); - getValueFromSsmStub = sinon.stub(serverless.variables, 'getValueFromSsm') + getValueFromSsmStub = sinon + .stub(serverless.variables, 'getValueFromSsm') .resolves('variableValue'); }); @@ -1403,74 +1459,81 @@ module.exports = { serverless.variables.getValueFromSsm.restore(); }); - it('should call getValueFromSls if referencing sls var', () => serverless.variables - .getValueFromSource('sls:instanceId').should.be.fulfilled - .then((valueToPopulate) => { + it('should call getValueFromSls if referencing sls var', () => + serverless.variables + .getValueFromSource('sls:instanceId') + .should.be.fulfilled.then(valueToPopulate => { expect(valueToPopulate).to.equal(variableValue); expect(getValueFromSlsStub).to.have.been.called; expect(getValueFromSlsStub).to.have.been.calledWith('sls:instanceId'); })); - it('should call getValueFromEnv if referencing env var', () => serverless.variables - .getValueFromSource('env:TEST_VAR').should.be.fulfilled - .then((valueToPopulate) => { + it('should call getValueFromEnv if referencing env var', () => + serverless.variables + .getValueFromSource('env:TEST_VAR') + .should.be.fulfilled.then(valueToPopulate => { expect(valueToPopulate).to.equal(variableValue); expect(getValueFromEnvStub).to.have.been.called; expect(getValueFromEnvStub).to.have.been.calledWith('env:TEST_VAR'); })); - it('should call getValueFromOptions if referencing an option', () => serverless.variables - .getValueFromSource('opt:stage').should.be.fulfilled - .then((valueToPopulate) => { + it('should call getValueFromOptions if referencing an option', () => + serverless.variables + .getValueFromSource('opt:stage') + .should.be.fulfilled.then(valueToPopulate => { expect(valueToPopulate).to.equal(variableValue); expect(getValueFromOptionsStub).to.have.been.called; expect(getValueFromOptionsStub).to.have.been.calledWith('opt:stage'); })); - it('should call getValueFromSelf if referencing from self', () => serverless.variables - .getValueFromSource('self:provider').should.be.fulfilled - .then((valueToPopulate) => { + it('should call getValueFromSelf if referencing from self', () => + serverless.variables + .getValueFromSource('self:provider') + .should.be.fulfilled.then(valueToPopulate => { expect(valueToPopulate).to.equal(variableValue); expect(getValueFromSelfStub).to.have.been.called; expect(getValueFromSelfStub).to.have.been.calledWith('self:provider'); })); - it('should call getValueFromFile if referencing from another file', () => serverless.variables - .getValueFromSource('file(./config.yml)').should.be.fulfilled - .then((valueToPopulate) => { + it('should call getValueFromFile if referencing from another file', () => + serverless.variables + .getValueFromSource('file(./config.yml)') + .should.be.fulfilled.then(valueToPopulate => { expect(valueToPopulate).to.equal(variableValue); expect(getValueFromFileStub).to.have.been.called; expect(getValueFromFileStub).to.have.been.calledWith('file(./config.yml)'); })); - it('should call getValueFromCf if referencing CloudFormation Outputs', () => serverless - .variables.getValueFromSource('cf:test-stack.testOutput').should.be.fulfilled - .then((valueToPopulate) => { + it('should call getValueFromCf if referencing CloudFormation Outputs', () => + serverless.variables + .getValueFromSource('cf:test-stack.testOutput') + .should.be.fulfilled.then(valueToPopulate => { expect(valueToPopulate).to.equal(variableValue); expect(getValueFromCfStub).to.have.been.called; expect(getValueFromCfStub).to.have.been.calledWith('cf:test-stack.testOutput'); })); - it('should call getValueFromS3 if referencing variable in S3', () => serverless.variables - .getValueFromSource('s3:test-bucket/path/to/key') - .should.be.fulfilled - .then((valueToPopulate) => { + it('should call getValueFromS3 if referencing variable in S3', () => + serverless.variables + .getValueFromSource('s3:test-bucket/path/to/key') + .should.be.fulfilled.then(valueToPopulate => { expect(valueToPopulate).to.equal(variableValue); expect(getValueFromS3Stub).to.have.been.called; expect(getValueFromS3Stub).to.have.been.calledWith('s3:test-bucket/path/to/key'); })); - it('should call getValueFromSsm if referencing variable in SSM', () => serverless.variables - .getValueFromSource('ssm:/test/path/to/param') - .should.be.fulfilled - .then((valueToPopulate) => { + it('should call getValueFromSsm if referencing variable in SSM', () => + serverless.variables + .getValueFromSource('ssm:/test/path/to/param') + .should.be.fulfilled.then(valueToPopulate => { expect(valueToPopulate).to.equal(variableValue); expect(getValueFromSsmStub).to.have.been.called; expect(getValueFromSsmStub).to.have.been.calledWith('ssm:/test/path/to/param'); })); it('should reject invalid sources', () => - serverless.variables.getValueFromSource('weird:source') + serverless.variables + .getValueFromSource('weird:source') .should.be.rejectedWith(serverless.classes.Error)); describe('caching', () => { @@ -1484,15 +1547,18 @@ module.exports = { { function: 'getValueFromS3', variableString: 's3:test-bucket/path/to/ke' }, { function: 'getValueFromSsm', variableString: 'ssm:/test/path/to/param' }, ]; - sources.forEach((source) => { + sources.forEach(source => { it(`should only call ${source.function} once, returning the cached value otherwise`, () => { const getValueFunctionStub = serverless.variables[source.function]; return BbPromise.all([ - serverless.variables.getValueFromSource(source.variableString) + serverless.variables + .getValueFromSource(source.variableString) .should.become(variableValue), BbPromise.delay(100).then(() => - serverless.variables.getValueFromSource(source.variableString) - .should.become(variableValue)), + serverless.variables + .getValueFromSource(source.variableString) + .should.become(variableValue) + ), ]).then(() => { expect(getValueFunctionStub).to.have.been.calledOnce; expect(getValueFunctionStub).to.have.been.calledWith(source.variableString); @@ -1505,7 +1571,7 @@ module.exports = { describe('#getValueFromSls()', () => { it('should get variable from Serverless Framework provided variables', () => { serverless.instanceId = 12345678; - return serverless.variables.getValueFromSls('sls:instanceId').then((valueToPopulate) => { + return serverless.variables.getValueFromSls('sls:instanceId').then(valueToPopulate => { expect(valueToPopulate).to.equal(12345678); }); }); @@ -1514,17 +1580,14 @@ module.exports = { describe('#getValueFromEnv()', () => { it('should get variable from environment variables', () => { process.env.TEST_VAR = 'someValue'; - return serverless.variables.getValueFromEnv('env:TEST_VAR') - .finally(() => { delete process.env.TEST_VAR; }) - .should.become('someValue'); + return serverless.variables.getValueFromEnv('env:TEST_VAR').should.become('someValue'); }); it('should allow top-level references to the environment variables hive', () => { process.env.TEST_VAR = 'someValue'; - return serverless.variables.getValueFromEnv('env:').then((valueToPopulate) => { + return serverless.variables.getValueFromEnv('env:').then(valueToPopulate => { expect(valueToPopulate.TEST_VAR).to.be.equal('someValue'); - }) - .finally(() => { delete process.env.TEST_VAR; }); + }); }); }); @@ -1536,7 +1599,8 @@ module.exports = { it('should allow top-level references to the options hive', () => { serverless.variables.options = { stage: 'prod' }; - return serverless.variables.getValueFromOptions('opt:') + return serverless.variables + .getValueFromOptions('opt:') .should.become(serverless.variables.options); }); }); @@ -1559,7 +1623,8 @@ module.exports = { service: 'testService', provider: serverless.service.provider, }; - return serverless.variables.getValueFromSelf('self:service.name') + return serverless.variables + .getValueFromSelf('self:service.name') .should.become('testService'); }); it('should redirect ${self:provider} to ${self:provider.name}', () => { @@ -1578,7 +1643,8 @@ module.exports = { awsKmsKeyArn: keyArn, }, }; - return serverless.variables.getValueFromSelf('self:service.awsKmsKeyArn') + return serverless.variables + .getValueFromSelf('self:service.awsKmsKeyArn') .should.become(keyArn); }); it('should handle self-references to the root of the serverless.yml file', () => { @@ -1587,7 +1653,8 @@ module.exports = { provider: 'testProvider', defaults: serverless.service.defaults, }; - return serverless.variables.getValueFromSelf('self:') + return serverless.variables + .getValueFromSelf('self:') .should.eventually.equal(serverless.variables.service); }); }); @@ -1606,8 +1673,9 @@ module.exports = { const fileExistsStub = sinon.stub(serverless.utils, 'fileExistsSync').returns(true); const realpathSync = sinon.stub(fse, 'realpathSync').returns(expectedFileName); const readFileSyncStub = sinon.stub(serverless.utils, 'readFileSync').returns(configYml); - return serverless.variables.getValueFromFile('file(~/somedir/config.yml)').should.be.fulfilled - .then((valueToPopulate) => { + return serverless.variables + .getValueFromFile('file(~/somedir/config.yml)') + .should.be.fulfilled.then(valueToPopulate => { expect(realpathSync).to.not.have.been.called; expect(fileExistsStub).to.have.been.calledWithMatch(expectedFileName); expect(readFileSyncStub).to.have.been.calledWithMatch(expectedFileName); @@ -1622,7 +1690,7 @@ module.exports = { it('should populate an entire variable file', () => { const SUtils = new Utils(); - const tmpDirPath = testUtils.getTmpDirPath(); + const tmpDirPath = getTmpDirPath(); const configYml = { test: 1, test2: 'test2', @@ -1633,17 +1701,19 @@ module.exports = { }; SUtils.writeFileSync(path.join(tmpDirPath, 'config.yml'), YAML.dump(configYml)); serverless.config.update({ servicePath: tmpDirPath }); - return serverless.variables.getValueFromFile('file(./config.yml)') + return serverless.variables + .getValueFromFile('file(./config.yml)') .should.eventually.eql(configYml); }); it('should get undefined if non existing file and the second argument is true', () => { - const tmpDirPath = testUtils.getTmpDirPath(); + const tmpDirPath = getTmpDirPath(); serverless.config.update({ servicePath: tmpDirPath }); const realpathSync = sinon.spy(fse, 'realpathSync'); const existsSync = sinon.spy(fse, 'existsSync'); - return serverless.variables.getValueFromFile('file(./non-existing.yml)').should.be.fulfilled - .then((valueToPopulate) => { + return serverless.variables + .getValueFromFile('file(./non-existing.yml)') + .should.be.fulfilled.then(valueToPopulate => { expect(realpathSync).to.not.have.been.called; expect(existsSync).to.have.been.calledOnce; expect(valueToPopulate).to.be.undefined; @@ -1656,24 +1726,30 @@ module.exports = { it('should populate non json/yml files', () => { const SUtils = new Utils(); - const tmpDirPath = testUtils.getTmpDirPath(); + const tmpDirPath = getTmpDirPath(); SUtils.writeFileSync(path.join(tmpDirPath, 'someFile'), 'hello world'); serverless.config.update({ servicePath: tmpDirPath }); - return serverless.variables.getValueFromFile('file(./someFile)') - .should.become('hello world'); + return serverless.variables.getValueFromFile('file(./someFile)').should.become('hello world'); }); - it('should populate symlinks', () => { + it('should populate symlinks', function() { const SUtils = new Utils(); - const tmpDirPath = testUtils.getTmpDirPath(); + const tmpDirPath = getTmpDirPath(); const realFilePath = path.join(tmpDirPath, 'someFile'); const symlinkPath = path.join(tmpDirPath, 'refSomeFile'); SUtils.writeFileSync(realFilePath, 'hello world'); - fse.ensureSymlinkSync(realFilePath, symlinkPath); + try { + fse.ensureSymlinkSync(realFilePath, symlinkPath); + } catch (error) { + skipOnWindowsDisabledSymlinks(error, this); + throw error; + } serverless.config.update({ servicePath: tmpDirPath }); - return serverless.variables.getValueFromFile('file(./refSomeFile)') + return serverless.variables + .getValueFromFile('file(./refSomeFile)') .should.become('hello world') - .then().finally(() => { + .then() + .finally(() => { fse.removeSync(realFilePath); fse.removeSync(symlinkPath); }); @@ -1681,16 +1757,15 @@ module.exports = { it('should trim trailing whitespace and new line character', () => { const SUtils = new Utils(); - const tmpDirPath = testUtils.getTmpDirPath(); + const tmpDirPath = getTmpDirPath(); SUtils.writeFileSync(path.join(tmpDirPath, 'someFile'), 'hello world \n'); serverless.config.update({ servicePath: tmpDirPath }); - return serverless.variables.getValueFromFile('file(./someFile)') - .should.become('hello world'); + return serverless.variables.getValueFromFile('file(./someFile)').should.become('hello world'); }); it('should populate from another file when variable is of any type', () => { const SUtils = new Utils(); - const tmpDirPath = testUtils.getTmpDirPath(); + const tmpDirPath = getTmpDirPath(); const configYml = { test0: 0, test1: 'test1', @@ -1701,69 +1776,75 @@ module.exports = { }; SUtils.writeFileSync(path.join(tmpDirPath, 'config.yml'), YAML.dump(configYml)); serverless.config.update({ servicePath: tmpDirPath }); - return serverless.variables.getValueFromFile('file(./config.yml):test2.sub') + return serverless.variables + .getValueFromFile('file(./config.yml):test2.sub') .should.become(configYml.test2.sub); }); it('should populate from a javascript file', () => { const SUtils = new Utils(); - const tmpDirPath = testUtils.getTmpDirPath(); + const tmpDirPath = getTmpDirPath(); const jsData = 'module.exports.hello=function(){return "hello world";};'; SUtils.writeFileSync(path.join(tmpDirPath, 'hello.js'), jsData); serverless.config.update({ servicePath: tmpDirPath }); - return serverless.variables.getValueFromFile('file(./hello.js):hello') + return serverless.variables + .getValueFromFile('file(./hello.js):hello') .should.become('hello world'); }); it('should populate an entire variable exported by a javascript file', () => { const SUtils = new Utils(); - const tmpDirPath = testUtils.getTmpDirPath(); + const tmpDirPath = getTmpDirPath(); const jsData = 'module.exports=function(){return { hello: "hello world" };};'; SUtils.writeFileSync(path.join(tmpDirPath, 'hello.js'), jsData); serverless.config.update({ servicePath: tmpDirPath }); - return serverless.variables.getValueFromFile('file(./hello.js)') + return serverless.variables + .getValueFromFile('file(./hello.js)') .should.become({ hello: 'hello world' }); }); it('should throw if property exported by a javascript file is not a function', () => { const SUtils = new Utils(); - const tmpDirPath = testUtils.getTmpDirPath(); + const tmpDirPath = getTmpDirPath(); const jsData = 'module.exports={ hello: "hello world" };'; SUtils.writeFileSync(path.join(tmpDirPath, 'hello.js'), jsData); serverless.config.update({ servicePath: tmpDirPath }); - return serverless.variables.getValueFromFile('file(./hello.js)') + return serverless.variables + .getValueFromFile('file(./hello.js)') .should.be.rejectedWith(serverless.classes.Error); }); it('should populate deep object from a javascript file', () => { const SUtils = new Utils(); - const tmpDirPath = testUtils.getTmpDirPath(); + const tmpDirPath = getTmpDirPath(); const jsData = `module.exports.hello=function(){ return {one:{two:{three: 'hello world'}}} };`; SUtils.writeFileSync(path.join(tmpDirPath, 'hello.js'), jsData); serverless.config.update({ servicePath: tmpDirPath }); serverless.variables.loadVariableSyntax(); - return serverless.variables.getValueFromFile('file(./hello.js):hello.one.two.three') + return serverless.variables + .getValueFromFile('file(./hello.js):hello.one.two.three') .should.become('hello world'); }); it('should preserve the exported function context when executing', () => { const SUtils = new Utils(); - const tmpDirPath = testUtils.getTmpDirPath(); + const tmpDirPath = getTmpDirPath(); const jsData = ` module.exports.one = {two: {three: 'hello world'}} module.exports.hello=function(){ return this; };`; SUtils.writeFileSync(path.join(tmpDirPath, 'hello.js'), jsData); serverless.config.update({ servicePath: tmpDirPath }); serverless.variables.loadVariableSyntax(); - return serverless.variables.getValueFromFile('file(./hello.js):hello.one.two.three') + return serverless.variables + .getValueFromFile('file(./hello.js):hello.one.two.three') .should.become('hello world'); }); it('should file variable not using ":" syntax', () => { const SUtils = new Utils(); - const tmpDirPath = testUtils.getTmpDirPath(); + const tmpDirPath = getTmpDirPath(); const configYml = { test: 1, test2: 'test2', @@ -1774,7 +1855,8 @@ module.exports = { }; SUtils.writeFileSync(path.join(tmpDirPath, 'config.yml'), YAML.dump(configYml)); serverless.config.update({ servicePath: tmpDirPath }); - return serverless.variables.getValueFromFile('file(./config.yml).testObj.sub') + return serverless.variables + .getValueFromFile('file(./config.yml).testObj.sub') .should.be.rejectedWith(serverless.classes.Error); }); }); @@ -1789,16 +1871,22 @@ module.exports = { serverless.setProvider('aws', awsProvider); serverless.variables.options = options; const awsResponseMock = { - Stacks: [{ - Outputs: [{ - OutputKey: 'MockExport', - OutputValue: 'MockValue', - }], - }], + Stacks: [ + { + Outputs: [ + { + OutputKey: 'MockExport', + OutputValue: 'MockValue', + }, + ], + }, + ], }; - const cfStub = sinon.stub(serverless.getProvider('aws'), 'request', - () => BbPromise.resolve(awsResponseMock)); - return serverless.variables.getValueFromCf('cf:some-stack.MockExport') + const cfStub = sinon + .stub(serverless.getProvider('aws'), 'request') + .callsFake(() => BbPromise.resolve(awsResponseMock)); + return serverless.variables + .getValueFromCf('cf:some-stack.MockExport') .should.become('MockValue') .then(() => { expect(cfStub).to.have.been.calledOnce; @@ -1806,7 +1894,8 @@ module.exports = { 'CloudFormation', 'describeStacks', { StackName: 'some-stack' }, - { useCache: true }); + { useCache: true } + ); }) .finally(() => cfStub.restore()); }); @@ -1820,16 +1909,22 @@ module.exports = { serverless.setProvider('aws', awsProvider); serverless.variables.options = options; const awsResponseMock = { - Stacks: [{ - Outputs: [{ - OutputKey: 'MockExport', - OutputValue: 'MockValue', - }], - }], + Stacks: [ + { + Outputs: [ + { + OutputKey: 'MockExport', + OutputValue: 'MockValue', + }, + ], + }, + ], }; - const cfStub = sinon.stub(serverless.getProvider('aws'), 'request', - () => BbPromise.resolve(awsResponseMock)); - return serverless.variables.getValueFromCf('cf.us-east-1:some-stack.MockExport') + const cfStub = sinon + .stub(serverless.getProvider('aws'), 'request') + .callsFake(() => BbPromise.resolve(awsResponseMock)); + return serverless.variables + .getValueFromCf('cf.us-east-1:some-stack.MockExport') .should.become('MockValue') .then(() => { expect(cfStub).to.have.been.calledOnce; @@ -1837,7 +1932,8 @@ module.exports = { 'CloudFormation', 'describeStacks', { StackName: 'some-stack' }, - { region: 'us-east-1', useCache: true }); + { region: 'us-east-1', useCache: true } + ); }) .finally(() => cfStub.restore()); }); @@ -1851,25 +1947,34 @@ module.exports = { serverless.setProvider('aws', awsProvider); serverless.variables.options = options; const awsResponseMock = { - Stacks: [{ - Outputs: [{ - OutputKey: 'MockExport', - OutputValue: 'MockValue', - }], - }], + Stacks: [ + { + Outputs: [ + { + OutputKey: 'MockExport', + OutputValue: 'MockValue', + }, + ], + }, + ], }; - const cfStub = sinon.stub(serverless.getProvider('aws'), 'request', - () => BbPromise.resolve(awsResponseMock)); - return serverless.variables.getValueFromCf('cf:some-stack.DoestNotExist') - .should.be.rejectedWith(serverless.classes.Error, - /to request a non exported variable from CloudFormation/) + const cfStub = sinon + .stub(serverless.getProvider('aws'), 'request') + .callsFake(() => BbPromise.resolve(awsResponseMock)); + return serverless.variables + .getValueFromCf('cf:some-stack.DoestNotExist') + .should.be.rejectedWith( + serverless.classes.Error, + /to request a non exported variable from CloudFormation/ + ) .then(() => { expect(cfStub).to.have.been.calledOnce; expect(cfStub).to.have.been.calledWithExactly( 'CloudFormation', 'describeStacks', { StackName: 'some-stack' }, - { useCache: true }); + { useCache: true } + ); }) .finally(() => cfStub.restore()); }); @@ -1890,8 +1995,11 @@ module.exports = { const awsResponseMock = { Body: 'MockValue', }; - const s3Stub = sinon.stub(awsProvider, 'request', () => BbPromise.resolve(awsResponseMock)); - return serverless.variables.getValueFromS3('s3:some.bucket/path/to/key') + const s3Stub = sinon + .stub(awsProvider, 'request') + .callsFake(() => BbPromise.resolve(awsResponseMock)); + return serverless.variables + .getValueFromS3('s3:some.bucket/path/to/key') .should.become('MockValue') .then(() => { expect(s3Stub).to.have.been.calledOnce; @@ -1902,19 +2010,24 @@ module.exports = { Bucket: 'some.bucket', Key: 'path/to/key', }, - { useCache: true }); + { useCache: true } + ); }) .finally(() => s3Stub.restore()); }); it('should throw error if error getting value from S3', () => { const error = new Error('The specified bucket is not valid'); - const requestStub = sinon.stub(awsProvider, 'request', () => BbPromise.reject(error)); + const requestStub = sinon + .stub(awsProvider, 'request') + .callsFake(() => BbPromise.reject(error)); return expect(serverless.variables.getValueFromS3('s3:some.bucket/path/to/key')) .to.be.rejectedWith( serverless.classes.Error, - 'Error getting value for s3:some.bucket/path/to/key. The specified bucket is not valid') - .then().finally(() => requestStub.restore()); + 'Error getting value for s3:some.bucket/path/to/key. The specified bucket is not valid' + ) + .then() + .finally(() => requestStub.restore()); }); }); @@ -1937,8 +2050,11 @@ module.exports = { serverless.variables.options = options; }); it('should get variable from Ssm using regular-style param', () => { - const ssmStub = sinon.stub(awsProvider, 'request', () => BbPromise.resolve(awsResponseMock)); - return serverless.variables.getValueFromSsm(`ssm:${param}`) + const ssmStub = sinon + .stub(awsProvider, 'request') + .callsFake(() => BbPromise.resolve(awsResponseMock)); + return serverless.variables + .getValueFromSsm(`ssm:${param}`) .should.become(value) .then(() => { expect(ssmStub).to.have.been.calledOnce; @@ -1949,13 +2065,17 @@ module.exports = { Name: param, WithDecryption: false, }, - { useCache: true }); + { useCache: true } + ); }) .finally(() => ssmStub.restore()); }); it('should get variable from Ssm using path-style param', () => { - const ssmStub = sinon.stub(awsProvider, 'request', () => BbPromise.resolve(awsResponseMock)); - return serverless.variables.getValueFromSsm(`ssm:${param}`) + const ssmStub = sinon + .stub(awsProvider, 'request') + .callsFake(() => BbPromise.resolve(awsResponseMock)); + return serverless.variables + .getValueFromSsm(`ssm:${param}`) .should.become(value) .then(() => { expect(ssmStub).to.have.been.calledOnce; @@ -1966,13 +2086,17 @@ module.exports = { Name: param, WithDecryption: false, }, - { useCache: true }); + { useCache: true } + ); }) .finally(() => ssmStub.restore()); }); it('should get encrypted variable from Ssm using extended syntax', () => { - const ssmStub = sinon.stub(awsProvider, 'request', () => BbPromise.resolve(awsResponseMock)); - return serverless.variables.getValueFromSsm(`ssm:${param}~true`) + const ssmStub = sinon + .stub(awsProvider, 'request') + .callsFake(() => BbPromise.resolve(awsResponseMock)); + return serverless.variables + .getValueFromSsm(`ssm:${param}~true`) .should.become(value) .then(() => { expect(ssmStub).to.have.been.calledOnce; @@ -1983,13 +2107,17 @@ module.exports = { Name: param, WithDecryption: true, }, - { useCache: true }); + { useCache: true } + ); }) .finally(() => ssmStub.restore()); }); it('should get unencrypted variable from Ssm using extended syntax', () => { - const ssmStub = sinon.stub(awsProvider, 'request', () => BbPromise.resolve(awsResponseMock)); - return serverless.variables.getValueFromSsm(`ssm:${param}~false`) + const ssmStub = sinon + .stub(awsProvider, 'request') + .callsFake(() => BbPromise.resolve(awsResponseMock)); + return serverless.variables + .getValueFromSsm(`ssm:${param}~false`) .should.become(value) .then(() => { expect(ssmStub).to.have.been.calledOnce; @@ -2000,13 +2128,17 @@ module.exports = { Name: param, WithDecryption: false, }, - { useCache: true }); + { useCache: true } + ); }) .finally(() => ssmStub.restore()); }); it('should ignore bad values for extended syntax', () => { - const ssmStub = sinon.stub(awsProvider, 'request', () => BbPromise.resolve(awsResponseMock)); - return serverless.variables.getValueFromSsm(`ssm:${param}~badvalue`) + const ssmStub = sinon + .stub(awsProvider, 'request') + .callsFake(() => BbPromise.resolve(awsResponseMock)); + return serverless.variables + .getValueFromSsm(`ssm:${param}~badvalue`) .should.become(value) .then(() => { expect(ssmStub).to.have.been.calledOnce; @@ -2017,7 +2149,8 @@ module.exports = { Name: param, WithDecryption: false, }, - { useCache: true }); + { useCache: true } + ); }) .finally(() => ssmStub.restore()); }); @@ -2030,10 +2163,14 @@ module.exports = { Value: jsonLikeText, }, }; - const ssmStub = sinon.stub(awsProvider, 'request', () => BbPromise.resolve(awsResponse)); - return serverless.variables.getValueFromSsm(`ssm:${secretParam}~true`) + const ssmStub = sinon + .stub(awsProvider, 'request') + .callsFake(() => BbPromise.resolve(awsResponse)); + return serverless.variables + .getValueFromSsm(`ssm:${secretParam}~true`) .should.become(jsonLikeText) - .then().finally(() => ssmStub.restore()); + .then() + .finally(() => ssmStub.restore()); }); it('should parse value as json if returned value is json-like', () => { const secretParam = '/aws/reference/secretsmanager/foo-bar'; @@ -2047,10 +2184,14 @@ module.exports = { Value: jsonLikeText, }, }; - const ssmStub = sinon.stub(awsProvider, 'request', () => BbPromise.resolve(awsResponse)); - return serverless.variables.getValueFromSsm(`ssm:${secretParam}~true`) + const ssmStub = sinon + .stub(awsProvider, 'request') + .callsFake(() => BbPromise.resolve(awsResponse)); + return serverless.variables + .getValueFromSsm(`ssm:${secretParam}~true`) .should.become(json) - .then().finally(() => ssmStub.restore()); + .then() + .finally(() => ssmStub.restore()); }); it('should get value as text if returned value is NOT json-like', () => { const secretParam = '/aws/reference/secretsmanager/foo-bar'; @@ -2060,27 +2201,39 @@ module.exports = { Value: plainText, }, }; - const ssmStub = sinon.stub(awsProvider, 'request', () => BbPromise.resolve(awsResponse)); - return serverless.variables.getValueFromSsm(`ssm:${secretParam}~true`) + const ssmStub = sinon + .stub(awsProvider, 'request') + .callsFake(() => BbPromise.resolve(awsResponse)); + return serverless.variables + .getValueFromSsm(`ssm:${secretParam}~true`) .should.become(plainText) - .then().finally(() => ssmStub.restore()); + .then() + .finally(() => ssmStub.restore()); }); }); it('should return undefined if SSM parameter does not exist', () => { const error = new serverless.classes.Error(`Parameter ${param} not found.`, 400); - const requestStub = sinon.stub(awsProvider, 'request', () => BbPromise.reject(error)); - return serverless.variables.getValueFromSsm(`ssm:${param}`) + const requestStub = sinon + .stub(awsProvider, 'request') + .callsFake(() => BbPromise.reject(error)); + return serverless.variables + .getValueFromSsm(`ssm:${param}`) .should.become(undefined) - .then().finally(() => requestStub.restore()); + .then() + .finally(() => requestStub.restore()); }); it('should reject if SSM request returns unexpected error', () => { const error = new Error( - 'User: is not authorized to perform: ssm:GetParameter on resource: '); - const requestStub = sinon.stub(awsProvider, 'request', () => BbPromise.reject(error)); - return serverless.variables.getValueFromSsm(`ssm:${param}`) - .should.be.rejected - .then().finally(() => requestStub.restore()); + 'User: is not authorized to perform: ssm:GetParameter on resource: ' + ); + const requestStub = sinon + .stub(awsProvider, 'request') + .callsFake(() => BbPromise.reject(error)); + return serverless.variables + .getValueFromSsm(`ssm:${param}`) + .should.be.rejected.then() + .finally(() => requestStub.restore()); }); }); @@ -2095,8 +2248,9 @@ module.exports = { }, }; serverless.variables.loadVariableSyntax(); - return serverless.variables.getDeeperValue(['custom', 'subProperty', 'deep'], - valueToPopulateMock).should.become('deepValue'); + return serverless.variables + .getDeeperValue(['custom', 'subProperty', 'deep'], valueToPopulateMock) + .should.become('deepValue'); }); it('should not throw error if referencing invalid properties', () => { const valueToPopulateMock = { @@ -2106,8 +2260,9 @@ module.exports = { }, }; serverless.variables.loadVariableSyntax(); - return serverless.variables.getDeeperValue(['custom', 'subProperty', 'deep', 'deeper'], - valueToPopulateMock).should.eventually.deep.equal({}); + return serverless.variables + .getDeeperValue(['custom', 'subProperty', 'deep', 'deeper'], valueToPopulateMock) + .should.eventually.deep.equal({}); }); it('should return a simple deep variable when final deep value is variable', () => { serverless.variables.service = { @@ -2121,10 +2276,9 @@ module.exports = { provider: serverless.service.provider, }; serverless.variables.loadVariableSyntax(); - return serverless.variables.getDeeperValue( - ['custom', 'subProperty', 'deep'], - serverless.variables.service - ).should.become('${deep:0}'); + return serverless.variables + .getDeeperValue(['custom', 'subProperty', 'deep'], serverless.variables.service) + .should.become('${deep:0}'); }); it('should return a deep continuation when middle deep value is variable', () => { serverless.variables.service = { @@ -2135,9 +2289,8 @@ module.exports = { provider: serverless.service.provider, }; serverless.variables.loadVariableSyntax(); - return serverless.variables.getDeeperValue( - ['custom', 'anotherVar', 'veryDeep'], - serverless.variables.service) + return serverless.variables + .getDeeperValue(['custom', 'anotherVar', 'veryDeep'], serverless.variables.service) .should.become('${deep:0.veryDeep}'); }); }); diff --git a/lib/classes/YamlParser.js b/lib/classes/YamlParser.js index 35907f337..161be346c 100644 --- a/lib/classes/YamlParser.js +++ b/lib/classes/YamlParser.js @@ -5,7 +5,6 @@ const YAML = require('js-yaml'); const resolve = require('json-refs').resolveRefs; class YamlParser { - constructor(serverless) { this.serverless = serverless; } @@ -14,7 +13,6 @@ class YamlParser { let parentDir = yamlFilePath.split(path.sep); parentDir.pop(); parentDir = parentDir.join('/'); - process.chdir(parentDir); const root = this.serverless.utils.readFileSync(yamlFilePath); const options = { @@ -24,8 +22,9 @@ class YamlParser { callback(null, YAML.load(res.text)); }, }, + relativeBase: parentDir, }; - return resolve(root, options).then((res) => (res.resolved)); + return resolve(root, options).then(res => res.resolved); } } diff --git a/lib/classes/YamlParser.test.js b/lib/classes/YamlParser.test.js index c64331f10..178be9659 100644 --- a/lib/classes/YamlParser.test.js +++ b/lib/classes/YamlParser.test.js @@ -8,7 +8,7 @@ const chai = require('chai'); const YAML = require('js-yaml'); const path = require('path'); const Serverless = require('../../lib/Serverless'); -const testUtils = require('../../tests/utils'); +const { getTmpFilePath, getTmpDirPath } = require('../../tests/utils/fs'); // Configure chai chai.use(require('chai-as-promised')); @@ -19,25 +19,27 @@ const serverless = new Serverless(); describe('YamlParser', () => { describe('#parse()', () => { it('should parse a simple .yaml file', () => { - const tmpFilePath = testUtils.getTmpFilePath('simple.yaml'); + const tmpFilePath = getTmpFilePath('simple.yaml'); serverless.utils.writeFileSync(tmpFilePath, YAML.dump({ foo: 'bar' })); return expect(serverless.yamlParser.parse(tmpFilePath)) - .to.eventually.have.property('foo').to.equal('bar'); + .to.eventually.have.property('foo') + .to.equal('bar'); }); it('should parse a simple .yml file', () => { - const tmpFilePath = testUtils.getTmpFilePath('simple.yml'); + const tmpFilePath = getTmpFilePath('simple.yml'); serverless.utils.writeFileSync(tmpFilePath, YAML.dump({ foo: 'bar' })); return expect(serverless.yamlParser.parse(tmpFilePath)) - .to.eventually.have.property('foo').to.equal('bar'); + .to.eventually.have.property('foo') + .to.equal('bar'); }); it('should parse a .yml file with JSON-REF to YAML', () => { - const tmpDirPath = testUtils.getTmpDirPath(); + const tmpDirPath = getTmpDirPath(); serverless.utils.writeFileSync(path.join(tmpDirPath, 'ref.yml'), { foo: 'bar' }); @@ -50,11 +52,12 @@ describe('YamlParser', () => { serverless.utils.writeFileSync(path.join(tmpDirPath, 'test.yml'), testYml); return expect(serverless.yamlParser.parse(path.join(tmpDirPath, 'test.yml'))) - .to.eventually.have.deep.property('main.foo').to.equal('bar'); + .to.eventually.have.nested.property('main.foo') + .to.equal('bar'); }); it('should parse a .yml file with JSON-REF to JSON', () => { - const tmpDirPath = testUtils.getTmpDirPath(); + const tmpDirPath = getTmpDirPath(); serverless.utils.writeFileSync(path.join(tmpDirPath, 'ref.json'), { foo: 'bar' }); @@ -67,11 +70,12 @@ describe('YamlParser', () => { serverless.utils.writeFileSync(path.join(tmpDirPath, 'test.yml'), testYml); return expect(serverless.yamlParser.parse(path.join(tmpDirPath, 'test.yml'))) - .to.eventually.have.deep.property('main.foo').to.equal('bar'); + .to.eventually.have.nested.property('main.foo') + .to.equal('bar'); }); it('should parse a .yml file with recursive JSON-REF', () => { - const tmpDirPath = testUtils.getTmpDirPath(); + const tmpDirPath = getTmpDirPath(); serverless.utils.writeFileSync(path.join(tmpDirPath, 'three.yml'), { foo: 'bar' }); @@ -92,7 +96,8 @@ describe('YamlParser', () => { serverless.utils.writeFileSync(path.join(tmpDirPath, 'one.yml'), oneYml); return expect(serverless.yamlParser.parse(path.join(tmpDirPath, 'one.yml'))) - .to.eventually.have.deep.property('one.two.foo').to.equal('bar'); + .to.eventually.have.nested.property('one.two.foo') + .to.equal('bar'); }); }); }); diff --git a/lib/plugins/Plugins.json b/lib/plugins/Plugins.json index 1e63fd66d..bbd2edb9e 100644 --- a/lib/plugins/Plugins.json +++ b/lib/plugins/Plugins.json @@ -1,5 +1,6 @@ { "plugins": [ + "./interactiveCli", "./config/config.js", "./create/create.js", "./install/install.js", @@ -38,6 +39,7 @@ "./aws/package/compile/events/websockets/index.js", "./aws/package/compile/events/sns/index.js", "./aws/package/compile/events/stream/index.js", + "./aws/package/compile/events/alb/index.js", "./aws/package/compile/events/alexaSkill/index.js", "./aws/package/compile/events/alexaSmartHome/index.js", "./aws/package/compile/events/iot/index.js", diff --git a/lib/plugins/aws/common/index.js b/lib/plugins/aws/common/index.js index 554def94f..ad1c8b4ad 100644 --- a/lib/plugins/aws/common/index.js +++ b/lib/plugins/aws/common/index.js @@ -17,12 +17,7 @@ class AwsCommon { this.options = options; this.provider = this.serverless.getProvider('aws'); - Object.assign( - this, - validate, - cleanupTempDir, - artifacts - ); + Object.assign(this, validate, cleanupTempDir, artifacts); // Internal commands are addressed as aws:common:[:lifecycleevent] this.commands = { @@ -33,24 +28,16 @@ class AwsCommon { common: { commands: { validate: { - lifecycleEvents: [ - 'validate', - ], + lifecycleEvents: ['validate'], }, cleanupTempDir: { - lifecycleEvents: [ - 'cleanup', - ], + lifecycleEvents: ['cleanup'], }, moveArtifactsToPackage: { - lifecycleEvents: [ - 'move', - ], + lifecycleEvents: ['move'], }, moveArtifactsToTemp: { - lifecycleEvents: [ - 'move', - ], + lifecycleEvents: ['move'], }, }, }, @@ -59,17 +46,15 @@ class AwsCommon { }; this.hooks = { - 'aws:common:validate:validate': () => BbPromise.bind(this) - .then(this.validate), + 'aws:common:validate:validate': () => BbPromise.bind(this).then(this.validate), - 'aws:common:cleanupTempDir:cleanup': () => BbPromise.bind(this) - .then(this.cleanupTempDir), + 'aws:common:cleanupTempDir:cleanup': () => BbPromise.bind(this).then(this.cleanupTempDir), - 'aws:common:moveArtifactsToPackage:move': () => BbPromise.bind(this) - .then(this.moveArtifactsToPackage), + 'aws:common:moveArtifactsToPackage:move': () => + BbPromise.bind(this).then(this.moveArtifactsToPackage), - 'aws:common:moveArtifactsToTemp:move': () => BbPromise.bind(this) - .then(this.moveArtifactsToTemp), + 'aws:common:moveArtifactsToTemp:move': () => + BbPromise.bind(this).then(this.moveArtifactsToTemp), }; } } diff --git a/lib/plugins/aws/common/index.test.js b/lib/plugins/aws/common/index.test.js index 3d9f9691b..eef7bb650 100644 --- a/lib/plugins/aws/common/index.test.js +++ b/lib/plugins/aws/common/index.test.js @@ -1,4 +1,3 @@ - 'use strict'; const AwsProvider = require('../provider/awsProvider'); @@ -33,8 +32,7 @@ describe('AwsCommon', () => { describe('hooks', () => { describe('aws:common:validate:validate', () => { it('should call validate', () => { - const validateStub = sinon - .stub(awsCommon, 'validate').resolves(); + const validateStub = sinon.stub(awsCommon, 'validate').resolves(); return awsCommon.hooks['aws:common:validate:validate']().then(() => { expect(validateStub.calledOnce).to.be.equal(true); @@ -44,8 +42,7 @@ describe('AwsCommon', () => { describe('aws:common:cleanupTempDir:cleanup', () => { it('should call cleanupTempDir', () => { - const cleanupTempDirStub = sinon - .stub(awsCommon, 'cleanupTempDir').resolves(); + const cleanupTempDirStub = sinon.stub(awsCommon, 'cleanupTempDir').resolves(); return awsCommon.hooks['aws:common:cleanupTempDir:cleanup']().then(() => { expect(cleanupTempDirStub.calledOnce).to.be.equal(true); @@ -56,7 +53,8 @@ describe('AwsCommon', () => { describe('aws:common:moveArtifactsToPackage:move', () => { it('should call cleanupTempDir', () => { const moveArtifactsToPackageStub = sinon - .stub(awsCommon, 'moveArtifactsToPackage').resolves(); + .stub(awsCommon, 'moveArtifactsToPackage') + .resolves(); return awsCommon.hooks['aws:common:moveArtifactsToPackage:move']().then(() => { expect(moveArtifactsToPackageStub.calledOnce).to.be.equal(true); @@ -66,8 +64,7 @@ describe('AwsCommon', () => { describe('aws:common:moveArtifactsToTemp:move', () => { it('should call cleanupTempDir', () => { - const moveArtifactsToTempStub = sinon - .stub(awsCommon, 'moveArtifactsToTemp').resolves(); + const moveArtifactsToTempStub = sinon.stub(awsCommon, 'moveArtifactsToTemp').resolves(); return awsCommon.hooks['aws:common:moveArtifactsToTemp:move']().then(() => { expect(moveArtifactsToTempStub.calledOnce).to.be.equal(true); @@ -78,34 +75,36 @@ describe('AwsCommon', () => { describe('commands', () => { it('should be only entrypoints', () => { - expect(awsCommon.commands).to.have.deep.property('aws.type', 'entrypoint'); + expect(awsCommon.commands).to.have.nested.property('aws.type', 'entrypoint'); }); describe('aws:common:validate', () => { it('should exist', () => { - expect(awsCommon.commands) - .to.have.deep.property('aws.commands.common.commands.validate'); + expect(awsCommon.commands).to.have.nested.property('aws.commands.common.commands.validate'); }); }); describe('aws:common:cleanupTempDir', () => { it('should exist', () => { - expect(awsCommon.commands) - .to.have.deep.property('aws.commands.common.commands.cleanupTempDir'); + expect(awsCommon.commands).to.have.nested.property( + 'aws.commands.common.commands.cleanupTempDir' + ); }); }); describe('aws:common:moveArtifactsToPackage', () => { it('should exist', () => { - expect(awsCommon.commands) - .to.have.deep.property('aws.commands.common.commands.moveArtifactsToPackage'); + expect(awsCommon.commands).to.have.nested.property( + 'aws.commands.common.commands.moveArtifactsToPackage' + ); }); }); describe('aws:common:moveArtifactsToTemp', () => { it('should exist', () => { - expect(awsCommon.commands) - .to.have.deep.property('aws.commands.common.commands.moveArtifactsToTemp'); + expect(awsCommon.commands).to.have.nested.property( + 'aws.commands.common.commands.moveArtifactsToTemp' + ); }); }); }); diff --git a/lib/plugins/aws/common/lib/artifacts.js b/lib/plugins/aws/common/lib/artifacts.js index 783b43d06..ea6ec1ed3 100644 --- a/lib/plugins/aws/common/lib/artifacts.js +++ b/lib/plugins/aws/common/lib/artifacts.js @@ -7,7 +7,8 @@ const _ = require('lodash'); module.exports = { moveArtifactsToPackage() { - const packagePath = this.options.package || + const packagePath = + this.options.package || this.serverless.service.package.path || path.join(this.serverless.config.servicePath || '.', '.serverless'); @@ -29,7 +30,8 @@ module.exports = { }, moveArtifactsToTemp() { - const packagePath = this.options.package || + const packagePath = + this.options.package || this.serverless.service.package.path || path.join(this.serverless.config.servicePath || '.', '.serverless'); diff --git a/lib/plugins/aws/common/lib/artifacts.test.js b/lib/plugins/aws/common/lib/artifacts.test.js index b61f6fc00..b898d7907 100644 --- a/lib/plugins/aws/common/lib/artifacts.test.js +++ b/lib/plugins/aws/common/lib/artifacts.test.js @@ -5,12 +5,12 @@ const path = require('path'); const fse = require('fs-extra'); const AWSCommon = require('../index'); const Serverless = require('../../../../../lib/Serverless'); -const testUtils = require('../../../../../tests/utils'); +const { getTmpDirPath } = require('../../../../../tests/utils/fs'); describe('#moveArtifactsToPackage()', () => { let serverless; let awsCommon; - const moveBasePath = path.join(testUtils.getTmpDirPath(), 'move'); + const moveBasePath = path.join(getTmpDirPath(), 'move'); const moveServerlessPath = path.join(moveBasePath, '.serverless'); beforeEach(() => { @@ -99,7 +99,7 @@ describe('#moveArtifactsToPackage()', () => { describe('#moveArtifactsToTemp()', () => { let serverless; let awsCommon; - const moveBasePath = path.join(testUtils.getTmpDirPath(), 'move'); + const moveBasePath = path.join(getTmpDirPath(), 'move'); const moveServerlessPath = path.join(moveBasePath, '.serverless'); const moveTargetPath = path.join(moveBasePath, 'target'); diff --git a/lib/plugins/aws/common/lib/cleanupTempDir.test.js b/lib/plugins/aws/common/lib/cleanupTempDir.test.js index 5eb71126c..47592dd93 100644 --- a/lib/plugins/aws/common/lib/cleanupTempDir.test.js +++ b/lib/plugins/aws/common/lib/cleanupTempDir.test.js @@ -4,7 +4,7 @@ const expect = require('chai').expect; const path = require('path'); const Package = require('../index'); const Serverless = require('../../../../../lib/Serverless'); -const testUtils = require('../../../../../tests/utils'); +const { getTmpDirPath } = require('../../../../../tests/utils/fs'); describe('#cleanupTempDir()', () => { let serverless; @@ -14,29 +14,34 @@ describe('#cleanupTempDir()', () => { serverless = new Serverless(); packageService = new Package(serverless); - serverless.config.servicePath = testUtils.getTmpDirPath(); + serverless.config.servicePath = getTmpDirPath(); }); it('should remove .serverless in the service directory', () => { - const serverlessTmpDirPath = path.join(packageService.serverless.config.servicePath, - '.serverless', 'README'); - serverless.utils.writeFileSync(serverlessTmpDirPath, - 'Some README content'); + const serverlessTmpDirPath = path.join( + packageService.serverless.config.servicePath, + '.serverless', + 'README' + ); + serverless.utils.writeFileSync(serverlessTmpDirPath, 'Some README content'); return packageService.cleanupTempDir().then(() => { - expect(serverless.utils.dirExistsSync(path.join(packageService.serverless.config.servicePath, - '.serverless'))).to.equal(false); + expect( + serverless.utils.dirExistsSync( + path.join(packageService.serverless.config.servicePath, '.serverless') + ) + ).to.equal(false); }); }); - it('should resolve if servicePath is not present', (done) => { + it('should resolve if servicePath is not present', done => { delete serverless.config.servicePath; packageService.cleanupTempDir().then(() => { done(); }); }); - it('should resolve if the .serverless directory is not present', (done) => { + it('should resolve if the .serverless directory is not present', done => { packageService.cleanupTempDir().then(() => { done(); }); diff --git a/lib/plugins/aws/configCredentials/awsConfigCredentials.js b/lib/plugins/aws/configCredentials/awsConfigCredentials.js index 4e5906bae..fbaf18425 100644 --- a/lib/plugins/aws/configCredentials/awsConfigCredentials.js +++ b/lib/plugins/aws/configCredentials/awsConfigCredentials.js @@ -1,12 +1,8 @@ 'use strict'; const BbPromise = require('bluebird'); -const constants = require('constants'); -const path = require('path'); -const fs = require('fs'); -const fse = require('fs-extra'); const os = require('os'); -const _ = require('lodash'); +const credentials = require('../utils/credentials'); class AwsConfigCredentials { constructor(serverless, options) { @@ -20,9 +16,7 @@ class AwsConfigCredentials { config: { commands: { credentials: { - lifecycleEvents: [ - 'config', - ], + lifecycleEvents: ['config'], options: { key: { usage: 'Access key for the provider', @@ -49,134 +43,55 @@ class AwsConfigCredentials { }; if (!os.homedir()) { - throw new this.serverless.classes - .Error('Can\'t find home directory on your local file system.'); + throw new this.serverless.classes.Error( + "Can't find home directory on your local file system." + ); } - this.credentialsFilePath = path.join(os.homedir(), '.aws', 'credentials'); - // Create the credentials file alongside the .aws directory if it's not yet present - fse.ensureFileSync(this.credentialsFilePath); - this.hooks = { - 'config:credentials:config': () => BbPromise.bind(this) - .then(this.configureCredentials), + 'config:credentials:config': () => this.configureCredentials(), }; } configureCredentials() { - // sanitize - this.options.provider = this.options.provider.toLowerCase(); - this.options.profile = this.options.profile ? this.options.profile : 'default'; + return BbPromise.try(() => { + // sanitize + this.options.provider = this.options.provider.toLowerCase(); + this.options.profile = this.options.profile ? this.options.profile : 'default'; - // resolve if provider option is not 'aws' - if (this.options.provider !== 'aws') { - return BbPromise.resolve(); - } + // resolve if provider option is not 'aws' + if (this.options.provider !== 'aws') return null; - // validate - if (!this.options.key || !this.options.secret) { - throw new this.serverless.classes.Error('Please include --key and --secret options for AWS.'); - } - - this.serverless.cli.log('Setting up AWS...'); - - this.credentials = this.getCredentials(); - - // Get the profile start line and end line numbers inside the credentials array - const profileBoundaries = this.getProfileBoundaries(); - - // Check if the profile exists - const isNewProfile = profileBoundaries.start === -1; - if (isNewProfile) { - this.addProfile(); - } else { - // Only update the profile if the overwrite flag was set - if (!this.options.overwrite) { - const message = [ - `Failed! ~/.aws/credentials already has a "${this.options.profile}" profile.`, - ' Use the overwrite flag ("-o" or "--overwrite") to force the update', - ].join(''); - this.serverless.cli.log(message); - return BbPromise.resolve(); + // validate + if (!this.options.key || !this.options.secret) { + throw new this.serverless.classes.Error( + 'Please include --key and --secret options for AWS.' + ); } - this.updateProfile(profileBoundaries); - } + this.serverless.cli.log('Setting up AWS...'); - return this.saveCredentialsFile(); - } - - getCredentials() { - // Get the credentials file lines - const credentialsFileContent = this.serverless.utils.readFileSync(this.credentialsFilePath); - return credentialsFileContent ? credentialsFileContent.split('\n') : []; - } - - addProfile() { - this.credentials.push(`[${this.options.profile}]`, - `aws_access_key_id = ${this.options.key}`, - `aws_secret_access_key = ${this.options.secret}`); - } - - updateProfile(profileBoundries) { - let currentLine = profileBoundries.start; - let endLine = profileBoundries.end; - - // Remove existing 'aws_access_key_id' and 'aws_secret_access_key' properties - while (currentLine < endLine) { - const line = this.credentials[currentLine]; - if ( - line.indexOf('aws_access_key_id') > -1 || - line.indexOf('aws_secret_access_key') > -1 - ) { - this.credentials.splice(currentLine, 1); - endLine--; - } else { - currentLine++; - } - } - - // Add the key and the secret to the beginning of the section - const keyLine = `aws_access_key_id = ${this.options.key}`; - const secretLine = `aws_secret_access_key = ${this.options.secret}`; - - this.credentials.splice(profileBoundries.start + 1, 0, secretLine); - this.credentials.splice(profileBoundries.start + 1, 0, keyLine); - } - - saveCredentialsFile() { - // Generate the file content and add a line break at the end - const updatedCredsFileContent = `${this.credentials.join('\n')}\n`; - - this.serverless.cli.log('Saving your AWS profile in "~/.aws/credentials"...'); - - return this.serverless.utils.writeFile(this.credentialsFilePath, updatedCredsFileContent) - .then(() => { - // set file permissions to only readable/writable by owner (equivalent to 'chmod 600') - // NOTE: `chmod` doesn't behave as intended on Windows, so skip if we're on Windows. - if (os.platform() !== 'win32') { - fs.chmodSync( - this.credentialsFilePath, - (fs.constants || constants).S_IRUSR | (fs.constants || constants).S_IWUSR - ); + return credentials.resolveFileProfiles().then(profiles => { + if (profiles.has(this.options.profile)) { + // Only update the profile if the overwrite flag was set + if (!this.options.overwrite) { + const message = [ + `Failed! ~/.aws/credentials already has a "${this.options.profile}" profile.`, + ' Use the overwrite flag ("-o" or "--overwrite") to force the update', + ].join(''); + this.serverless.cli.log(message); + return null; + } } + profiles.set(this.options.profile, { + accessKeyId: this.options.key, + secretAccessKey: this.options.secret, + }); - this.serverless.cli.log( - `Success! Your AWS access keys were stored under the "${this.options.profile}" profile.`); + return credentials.saveFileProfiles(profiles); }); + }); } - - getProfileBoundaries() { - // Get the line number of the aws profile definition, defaults to -1 - const start = this.credentials.indexOf(`[${this.options.profile}]`); - - const nextProfile = _.findIndex(this.credentials, line => /\[[^\]]+\]/.test(line), start + 1); - // Get the line number of the next aws profile definition, defaults to the file lines number - const end = nextProfile + 1 || this.credentials.length; - - return { start, end }; - } - } module.exports = AwsConfigCredentials; diff --git a/lib/plugins/aws/configCredentials/awsConfigCredentials.test.js b/lib/plugins/aws/configCredentials/awsConfigCredentials.test.js index 05e9a6f2b..6caf002f3 100644 --- a/lib/plugins/aws/configCredentials/awsConfigCredentials.test.js +++ b/lib/plugins/aws/configCredentials/awsConfigCredentials.test.js @@ -1,48 +1,38 @@ 'use strict'; const expect = require('chai').expect; -const sinon = require('sinon'); -const constants = require('constants'); +const sandbox = require('sinon'); +const { constants } = require('fs'); const fs = require('fs'); const fse = require('fs-extra'); const os = require('os'); const path = require('path'); -const testUtils = require('../../../../tests/utils'); const AwsConfigCredentials = require('./awsConfigCredentials'); const Serverless = require('../../../Serverless'); describe('AwsConfigCredentials', () => { let awsConfigCredentials; - let credentialsFileContent; - let credentialsFilePath; let serverless; - let sandbox; - let tmpDirPath; + const homeDirPath = os.homedir(); + const awsDirectoryPath = path.join(homeDirPath, '.aws'); + const credentialsFilePath = path.join(awsDirectoryPath, 'credentials'); + const credentialsFileContent = [ + '[my-profile]', + 'aws_access_key_id = my-old-profile-key', + 'aws_secret_access_key = my-old-profile-secret', + ].join('\n'); beforeEach(() => { - sandbox = sinon.sandbox.create(); - - tmpDirPath = testUtils.getTmpDirPath(); - credentialsFilePath = path.join(tmpDirPath, '.aws', 'credentials'); - credentialsFileContent = '[my-profile]\n'; - credentialsFileContent += 'aws_access_key_id = my-old-profile-key\n'; - credentialsFileContent += 'aws_secret_access_key = my-old-profile-secret\n'; - - // stub homedir handler to return the tmpDirPath - sandbox.stub(os, 'homedir').returns(tmpDirPath); - + fse.removeSync(credentialsFilePath); serverless = new Serverless(); - serverless.init(); - const options = { - provider: 'aws', - key: 'some-key', - secret: 'some-secret', - }; - awsConfigCredentials = new AwsConfigCredentials(serverless, options); - }); - - afterEach(() => { - sandbox.restore(); + return serverless.init().then(() => { + const options = { + provider: 'aws', + key: 'some-key', + secret: 'some-secret', + }; + awsConfigCredentials = new AwsConfigCredentials(serverless, options); + }); }); describe('#constructor()', () => { @@ -59,17 +49,18 @@ describe('AwsConfigCredentials', () => { }); it('should have the lifecycle event "config" for the "credentials" sub-command', () => { - expect(awsConfigCredentials.commands.config.commands.credentials.lifecycleEvents) - .to.deep.equal(['config']); + expect( + awsConfigCredentials.commands.config.commands.credentials.lifecycleEvents + ).to.deep.equal(['config']); }); it('should have the req. options "key" and "secret" for the "credentials" sub-command', () => { // eslint-disable-next-line no-unused-expressions - expect(awsConfigCredentials.commands.config.commands.credentials.options.key.required) - .to.be.true; + expect(awsConfigCredentials.commands.config.commands.credentials.options.key.required).to.be + .true; // eslint-disable-next-line no-unused-expressions - expect(awsConfigCredentials.commands.config.commands.credentials.options.secret.required) - .to.be.true; + expect(awsConfigCredentials.commands.config.commands.credentials.options.secret.required).to + .be.true; }); it('should have a "config:credentials:config" hook', () => { @@ -77,8 +68,9 @@ describe('AwsConfigCredentials', () => { }); it('should run promise chain in order for "config:credentials:config" hook', () => { - const awsConfigCredentialsStub = sinon - .stub(awsConfigCredentials, 'configureCredentials').resolves(); + const awsConfigCredentialsStub = sandbox + .stub(awsConfigCredentials, 'configureCredentials') + .resolves(); return awsConfigCredentials.hooks['config:credentials:config']().then(() => { expect(awsConfigCredentialsStub.calledOnce).to.equal(true); @@ -88,18 +80,12 @@ describe('AwsConfigCredentials', () => { }); it('should throw an error if the home directory was not found', () => { - os.homedir.returns(null); - expect(() => new AwsConfigCredentials(serverless, {})).to.throw(Error); - }); - - it('should create the .aws/credentials file if not yet present', () => { - // remove the .aws directory which was created in the before hook of the test - const awsDirectoryPath = path.join(tmpDirPath, '.aws'); - fse.removeSync(awsDirectoryPath); - - awsConfigCredentials = new AwsConfigCredentials(serverless, {}); - const isCredentialsFilePresent = fs.existsSync(path.join(awsDirectoryPath, 'credentials')); - expect(isCredentialsFilePresent).to.equal(true); + sandbox.stub(os, 'homedir').returns(null); + try { + expect(() => new AwsConfigCredentials(serverless, {})).to.throw(Error); + } finally { + sandbox.restore(); + } }); }); @@ -115,10 +101,9 @@ describe('AwsConfigCredentials', () => { it('should use the "default" profile if option is not given', () => awsConfigCredentials.configureCredentials().then(() => { expect(awsConfigCredentials.options.profile).to.equal('default'); - }) - ); + })); - it('should resolve if the provider option is not "aws"', (done) => { + it('should resolve if the provider option is not "aws"', done => { awsConfigCredentials.options.provider = 'invalid-provider'; awsConfigCredentials.configureCredentials().then(() => done()); @@ -127,8 +112,13 @@ describe('AwsConfigCredentials', () => { it('should throw an error if the "key" and "secret" options are not given', () => { awsConfigCredentials.options.key = false; awsConfigCredentials.options.secret = false; - - expect(() => awsConfigCredentials.configureCredentials()).to.throw(Error); + return awsConfigCredentials.configureCredentials().then( + () => { + throw new Error('Unexpected'); + }, + error => + expect(error.message).to.include('Please include --key and --secret options for AWS') + ); }); it('should not update the profile if the overwrite flag is not set', () => { @@ -154,8 +144,8 @@ describe('AwsConfigCredentials', () => { const lineByLineContent = UpdatedCredentialsFileContent.split('\n'); expect(lineByLineContent[0]).to.equal('[my-profile]'); - expect(lineByLineContent[1]).to.equal('aws_access_key_id = my-new-profile-key'); - expect(lineByLineContent[2]).to.equal('aws_secret_access_key = my-new-profile-secret'); + expect(lineByLineContent[1]).to.equal('aws_access_key_id=my-new-profile-key'); + expect(lineByLineContent[2]).to.equal('aws_secret_access_key=my-new-profile-secret'); }); }); @@ -165,40 +155,47 @@ describe('AwsConfigCredentials', () => { awsConfigCredentials.options.secret = 'my-new-profile-secret'; awsConfigCredentials.options.overwrite = true; - credentialsFileContent += '[my-other-profile]\n'; - credentialsFileContent += 'aws_secret_access_key = my-other-profile-secret\n'; + const newCredentialsFileContent = [ + credentialsFileContent, + '[my-other-profile]', + 'aws_access_key_id = my-other-profile-key', + 'aws_secret_access_key = my-other-profile-secret', + ].join('\n'); - fse.outputFileSync(credentialsFilePath, credentialsFileContent); + fse.outputFileSync(credentialsFilePath, newCredentialsFileContent); return awsConfigCredentials.configureCredentials().then(() => { const UpdatedCredentialsFileContent = fs.readFileSync(credentialsFilePath).toString(); const lineByLineContent = UpdatedCredentialsFileContent.split('\n'); expect(lineByLineContent[0]).to.equal('[my-profile]'); - expect(lineByLineContent[1]).to.equal('aws_access_key_id = my-new-profile-key'); - expect(lineByLineContent[2]).to.equal('aws_secret_access_key = my-new-profile-secret'); - expect(lineByLineContent[4]).to.equal('aws_secret_access_key = my-other-profile-secret'); + expect(lineByLineContent[1]).to.equal('aws_access_key_id=my-new-profile-key'); + expect(lineByLineContent[2]).to.equal('aws_secret_access_key=my-new-profile-secret'); + expect(lineByLineContent[6]).to.equal('aws_secret_access_key=my-other-profile-secret'); }); }); it('should add the missing credentials to the updated profile', () => { - credentialsFileContent = '[my-profile]\n'; - credentialsFileContent += 'aws_secret_access_key = my-profile-secret\n'; + const newCredentialsFileContent = [ + credentialsFileContent, + '[my-profile]', + 'aws_secret_access_key = my-profile-secret', + ].join('\n'); awsConfigCredentials.options.profile = 'my-profile'; awsConfigCredentials.options.key = 'my-new-profile-key'; awsConfigCredentials.options.secret = 'my-new-profile-secret'; awsConfigCredentials.options.overwrite = true; - fse.outputFileSync(credentialsFilePath, credentialsFileContent); + fse.outputFileSync(credentialsFilePath, newCredentialsFileContent); return awsConfigCredentials.configureCredentials().then(() => { const UpdatedCredentialsFileContent = fs.readFileSync(credentialsFilePath).toString(); const lineByLineContent = UpdatedCredentialsFileContent.split('\n'); expect(lineByLineContent[0]).to.equal('[my-profile]'); - expect(lineByLineContent[1]).to.equal('aws_access_key_id = my-new-profile-key'); - expect(lineByLineContent[2]).to.equal('aws_secret_access_key = my-new-profile-secret'); + expect(lineByLineContent[1]).to.equal('aws_access_key_id=my-new-profile-key'); + expect(lineByLineContent[2]).to.equal('aws_secret_access_key=my-new-profile-secret'); }); }); @@ -212,8 +209,8 @@ describe('AwsConfigCredentials', () => { const lineByLineContent = UpdatedCredentialsFileContent.split('\n'); expect(lineByLineContent[0]).to.equal('[my-profile]'); - expect(lineByLineContent[1]).to.equal('aws_access_key_id = my-profile-key'); - expect(lineByLineContent[2]).to.equal('aws_secret_access_key = my-profile-secret'); + expect(lineByLineContent[1]).to.equal('aws_access_key_id=my-profile-key'); + expect(lineByLineContent[2]).to.equal('aws_secret_access_key=my-profile-secret'); }); }); @@ -228,65 +225,7 @@ describe('AwsConfigCredentials', () => { const expectedFilePermissions = readableByOwnerPermission | writableByOwnerPermission; expect(filePermissions).to.equal(expectedFilePermissions); - }) - ); + })); } }); - - describe('#getCredentials()', () => { - it('should load credentials file and return the credentials lines', () => { - fse.outputFileSync(credentialsFilePath, credentialsFileContent); - const credentials = awsConfigCredentials.getCredentials(); - - expect(credentials[0]).to.equal('[my-profile]'); - expect(credentials[1]).to.equal('aws_access_key_id = my-old-profile-key'); - }); - - it('should return an empty array if the file is empty', () => { - const credentials = awsConfigCredentials.getCredentials(); - - expect(credentials.length).to.equal(0); - }); - }); - - describe('#getProfileBoundaries()', () => { - it('should return the start and end numbers of the profile', () => { - awsConfigCredentials.options.profile = 'my-profile'; - awsConfigCredentials.credentials = [ - '[my-profile]', - 'aws_access_key_id = my-other-profile-key', - ]; - - const profileBoundaries = awsConfigCredentials.getProfileBoundaries(); - - expect(profileBoundaries.start).to.equal(0); - expect(profileBoundaries.end).to.equal(2); - }); - - it('should set the start property to -1 if the profile was not found', () => { - awsConfigCredentials.options.profile = 'my-not-yet-saved-profile'; - awsConfigCredentials.credentials = [ - '[my-profile]', - 'aws_access_key_id = my-other-profile-key', - ]; - - const profileBoundaries = awsConfigCredentials.getProfileBoundaries(); - - expect(profileBoundaries.start).to.equal(-1); - }); - - it('should set the end to the credentials length if no other profile was found', () => { - awsConfigCredentials.options.profile = 'my-profile'; - awsConfigCredentials.credentials = [ - '[my-profile]', - 'aws_access_key_id = my-other-profile-key', - '# a comment', - 'aws_secret_access_key = my-profile-secret', - ]; - - const profileBoundaries = awsConfigCredentials.getProfileBoundaries(); - - expect(profileBoundaries.end).to.equal(4); - }); - }); }); diff --git a/lib/plugins/aws/customResources/index.js b/lib/plugins/aws/customResources/index.js new file mode 100644 index 000000000..5adccee57 --- /dev/null +++ b/lib/plugins/aws/customResources/index.js @@ -0,0 +1,110 @@ +'use strict'; + +const path = require('path'); +const createZipFile = require('../../../utils/fs/createZipFile'); + +function addCustomResourceToService(resourceName, iamRoleStatements) { + let FunctionName; + let Handler; + let customResourceFunctionLogicalId; + + const Statement = iamRoleStatements; + const customResourcesRoleLogicalId = this.provider.naming.getCustomResourcesRoleLogicalId(); + const srcDirPath = path.join(__dirname, 'resources'); + const destDirPath = path.join( + this.serverless.config.servicePath, + '.serverless', + this.provider.naming.getCustomResourcesArtifactDirectoryName() + ); + const zipFilePath = `${destDirPath}.zip`; + this.serverless.utils.writeFileDir(zipFilePath); + + if (resourceName === 's3') { + FunctionName = `${this.serverless.service.service}-${ + this.options.stage + }-${this.provider.naming.getCustomResourceS3HandlerFunctionName()}`; + Handler = 's3/handler.handler'; + customResourceFunctionLogicalId = this.provider.naming.getCustomResourceS3HandlerFunctionLogicalId(); + } + + return createZipFile(srcDirPath, zipFilePath).then(outputFilePath => { + let S3Bucket = { + Ref: this.provider.naming.getDeploymentBucketLogicalId(), + }; + if (this.serverless.service.package.deploymentBucket) { + S3Bucket = this.serverless.service.package.deploymentBucket; + } + const s3Folder = this.serverless.service.package.artifactDirectoryName; + const s3FileName = outputFilePath.split(path.sep).pop(); + const S3Key = `${s3Folder}/${s3FileName}`; + + const customResourceRole = { + [customResourcesRoleLogicalId]: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Principal: { + Service: ['lambda.amazonaws.com'], + }, + Action: ['sts:AssumeRole'], + }, + ], + }, + Policies: [ + { + PolicyName: { + 'Fn::Join': [ + '-', + [ + this.provider.getStage(), + this.provider.serverless.service.service, + 'custom-resources-lambda', + ], + ], + }, + PolicyDocument: { + Version: '2012-10-17', + Statement, + }, + }, + ], + }, + }, + }; + + const customResourceFunction = { + [customResourceFunctionLogicalId]: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket, + S3Key, + }, + FunctionName, + Handler, + MemorySize: 1024, + Role: { + 'Fn::GetAtt': [customResourcesRoleLogicalId, 'Arn'], + }, + Runtime: 'nodejs10.x', + Timeout: 6, + }, + DependsOn: [customResourcesRoleLogicalId], + }, + }; + + Object.assign( + this.serverless.service.provider.compiledCloudFormationTemplate.Resources, + customResourceFunction, + customResourceRole + ); + }); +} + +module.exports = { + addCustomResourceToService, +}; diff --git a/lib/plugins/aws/customResources/index.test.js b/lib/plugins/aws/customResources/index.test.js new file mode 100644 index 000000000..e3732939f --- /dev/null +++ b/lib/plugins/aws/customResources/index.test.js @@ -0,0 +1,117 @@ +'use strict'; + +const path = require('path'); +const fs = require('fs'); +const chai = require('chai'); +const AwsProvider = require('../provider/awsProvider'); +const Serverless = require('../../../Serverless'); +const { createTmpDir } = require('../../../../tests/utils/fs'); +const { addCustomResourceToService } = require('./index.js'); + +const expect = chai.expect; +chai.use(require('chai-as-promised')); + +describe('#addCustomResourceToService()', () => { + let tmpDirPath; + let serverless; + let provider; + let context; + const iamRoleStatements = [ + { + Effect: 'Allow', + Resource: 'arn:aws:lambda:*:*:function:custom-resource-func', + Action: ['lambda:AddPermission', 'lambda:RemovePermission'], + }, + ]; + + beforeEach(() => { + const options = { + stage: 'dev', + region: 'us-east-1', + }; + tmpDirPath = createTmpDir(); + serverless = new Serverless(); + provider = new AwsProvider(serverless, options); + serverless.setProvider('aws', provider); + serverless.service.service = 'some-service'; + serverless.service.provider.compiledCloudFormationTemplate = { + Resources: {}, + }; + serverless.config.servicePath = tmpDirPath; + serverless.service.package.artifactDirectoryName = 'artifact-dir-name'; + context = { + serverless, + provider, + options, + }; + }); + + describe('when using the custom S3 resouce', () => { + it('should add the custom resource to the service', () => { + return expect( + addCustomResourceToService.call(context, 's3', iamRoleStatements) + ).to.be.fulfilled.then(() => { + const { Resources } = serverless.service.provider.compiledCloudFormationTemplate; + const customResourcesZipFilePath = path.join( + tmpDirPath, + '.serverless', + 'custom-resources.zip' + ); + + expect(fs.existsSync(customResourcesZipFilePath)).to.equal(true); + expect(Resources).to.deep.equal({ + CustomDashresourceDashexistingDashs3LambdaFunction: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: { Ref: 'ServerlessDeploymentBucket' }, + S3Key: 'artifact-dir-name/custom-resources.zip', + }, + FunctionName: 'some-service-dev-custom-resource-existing-s3', + Handler: 's3/handler.handler', + MemorySize: 1024, + Role: { 'Fn::GetAtt': ['IamRoleCustomResourcesLambdaExecution', 'Arn'] }, + Runtime: 'nodejs10.x', + Timeout: 6, + }, + DependsOn: ['IamRoleCustomResourcesLambdaExecution'], + }, + IamRoleCustomResourcesLambdaExecution: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: ['sts:AssumeRole'], + Effect: 'Allow', + Principal: { + Service: ['lambda.amazonaws.com'], + }, + }, + ], + Version: '2012-10-17', + }, + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Effect: 'Allow', + Resource: 'arn:aws:lambda:*:*:function:custom-resource-func', + Action: ['lambda:AddPermission', 'lambda:RemovePermission'], + }, + ], + Version: '2012-10-17', + }, + PolicyName: { + 'Fn::Join': ['-', ['dev', 'some-service', 'custom-resources-lambda']], + }, + }, + ], + }, + }, + }); + }); + }); + }); +}); diff --git a/lib/plugins/aws/customResources/resources/README.md b/lib/plugins/aws/customResources/resources/README.md new file mode 100644 index 000000000..bde6a0fc3 --- /dev/null +++ b/lib/plugins/aws/customResources/resources/README.md @@ -0,0 +1,3 @@ +# Serverless Custom CloudFormation Resources + +This directory contains the Lambda functions for the Serverless Custom CloudFormation Resources. diff --git a/lib/plugins/aws/customResources/resources/s3/handler.js b/lib/plugins/aws/customResources/resources/s3/handler.js new file mode 100644 index 000000000..a54318145 --- /dev/null +++ b/lib/plugins/aws/customResources/resources/s3/handler.js @@ -0,0 +1,73 @@ +'use strict'; + +const { addPermission, removePermission } = require('./lib/permissions'); +const { updateConfiguration, removeConfiguration } = require('./lib/bucket'); +const { getEnvironment, getLambdaArn, handlerWrapper } = require('../utils'); + +function handler(event, context) { + if (event.RequestType === 'Create') { + return create(event, context); + } else if (event.RequestType === 'Update') { + return update(event, context); + } else if (event.RequestType === 'Delete') { + return remove(event, context); + } + throw new Error(`Unhandled RequestType ${event.RequestType}`); +} + +function create(event, context) { + const { Region, AccountId } = getEnvironment(context); + const { FunctionName, BucketName, BucketConfig } = event.ResourceProperties; + + const lambdaArn = getLambdaArn(Region, AccountId, FunctionName); + + return addPermission({ + functionName: FunctionName, + bucketName: BucketName, + region: Region, + }).then(() => + updateConfiguration({ + lambdaArn, + region: Region, + functionName: FunctionName, + bucketName: BucketName, + bucketConfig: BucketConfig, + }) + ); +} + +function update(event, context) { + const { Region, AccountId } = getEnvironment(context); + const { FunctionName, BucketName, BucketConfig } = event.ResourceProperties; + + const lambdaArn = getLambdaArn(Region, AccountId, FunctionName); + + return updateConfiguration({ + lambdaArn, + region: Region, + functionName: FunctionName, + bucketName: BucketName, + bucketConfig: BucketConfig, + }); +} + +function remove(event, context) { + const { Region } = getEnvironment(context); + const { FunctionName, BucketName } = event.ResourceProperties; + + return removePermission({ + functionName: FunctionName, + bucketName: BucketName, + region: Region, + }).then(() => + removeConfiguration({ + region: Region, + functionName: FunctionName, + bucketName: BucketName, + }) + ); +} + +module.exports = { + handler: handlerWrapper(handler, 'CustomResouceExistingS3'), +}; diff --git a/lib/plugins/aws/customResources/resources/s3/lib/bucket.js b/lib/plugins/aws/customResources/resources/s3/lib/bucket.js new file mode 100644 index 000000000..0b07588bf --- /dev/null +++ b/lib/plugins/aws/customResources/resources/s3/lib/bucket.js @@ -0,0 +1,101 @@ +'use strict'; + +const crypto = require('crypto'); +const AWS = require('aws-sdk'); + +function generateId(functionName, bucketConfig) { + const md5 = crypto + .createHash('md5') + .update(JSON.stringify(bucketConfig)) + .digest('hex'); + return `${functionName}-${md5}`; +} + +function createFilter(config) { + const rules = config.Rules; + if (rules && rules.length) { + const FilterRules = rules.map(rule => { + const key = Object.keys(rule)[0]; + const Name = key.toLowerCase(); + const Value = rule[key].toLowerCase(); + return { + Name, + Value, + }; + }); + return { + Key: { + FilterRules, + }, + }; + } + return undefined; +} + +function getConfiguration(config) { + const { bucketName, region } = config; + const s3 = new AWS.S3({ region }); + const Bucket = bucketName; + const payload = { + Bucket, + }; + return s3 + .getBucketNotificationConfiguration(payload) + .promise() + .then(data => data); +} + +function updateConfiguration(config) { + const { lambdaArn, functionName, bucketName, bucketConfig, region } = config; + const s3 = new AWS.S3({ region }); + const Bucket = bucketName; + + return getConfiguration(config).then(NotificationConfiguration => { + // remove configurations for this specific function + NotificationConfiguration.LambdaFunctionConfigurations = NotificationConfiguration.LambdaFunctionConfigurations.filter( + conf => !conf.Id.startsWith(functionName) + ); + + // add the event to the existing NotificationConfiguration + const Events = [bucketConfig.Event]; + const LambdaFunctionArn = lambdaArn; + const Id = generateId(functionName, bucketConfig); + const Filter = createFilter(bucketConfig); + NotificationConfiguration.LambdaFunctionConfigurations.push({ + Events, + LambdaFunctionArn, + Filter, + Id, + }); + + const payload = { + Bucket, + NotificationConfiguration, + }; + return s3.putBucketNotificationConfiguration(payload).promise(); + }); +} + +function removeConfiguration(config) { + const { functionName, bucketName, region } = config; + const s3 = new AWS.S3({ region }); + const Bucket = bucketName; + + return getConfiguration(config).then(NotificationConfiguration => { + // remove configurations for this specific function + NotificationConfiguration.LambdaFunctionConfigurations = NotificationConfiguration.LambdaFunctionConfigurations.filter( + conf => !conf.Id.startsWith(functionName) + ); + + const payload = { + Bucket, + NotificationConfiguration, + }; + return s3.putBucketNotificationConfiguration(payload).promise(); + }); +} + +module.exports = { + updateConfiguration, + removeConfiguration, +}; diff --git a/lib/plugins/aws/customResources/resources/s3/lib/permissions.js b/lib/plugins/aws/customResources/resources/s3/lib/permissions.js new file mode 100644 index 000000000..d26a215d0 --- /dev/null +++ b/lib/plugins/aws/customResources/resources/s3/lib/permissions.js @@ -0,0 +1,42 @@ +'use strict'; + +const AWS = require('aws-sdk'); + +function getStatementId(functionName, bucketName) { + const normalizedBucketName = bucketName.replace(/[.:*]/g, ''); + const id = `${functionName}-${normalizedBucketName}`; + if (id.length < 100) { + return id; + } + return id.substring(0, 100); +} + +function addPermission(config) { + const { functionName, bucketName, region } = config; + const lambda = new AWS.Lambda({ region }); + const partition = region && /^cn-/.test(region) ? 'aws-cn' : 'aws'; + const payload = { + Action: 'lambda:InvokeFunction', + FunctionName: functionName, + Principal: 's3.amazonaws.com', + StatementId: getStatementId(functionName, bucketName), + SourceArn: `arn:${partition}:s3:::${bucketName}`, + }; + return lambda.addPermission(payload).promise(); +} + +function removePermission(config) { + const { functionName, bucketName, region } = config; + const lambda = new AWS.Lambda({ region }); + const payload = { + FunctionName: functionName, + StatementId: getStatementId(functionName, bucketName), + }; + return lambda.removePermission(payload).promise(); +} + +module.exports = { + getStatementId, + addPermission, + removePermission, +}; diff --git a/lib/plugins/aws/customResources/resources/utils.js b/lib/plugins/aws/customResources/resources/utils.js new file mode 100644 index 000000000..fe497b0b1 --- /dev/null +++ b/lib/plugins/aws/customResources/resources/utils.js @@ -0,0 +1,93 @@ +'use strict'; + +const https = require('https'); +const url = require('url'); + +const logger = console; + +function response(event, context, status, data = {}, err) { + const reason = err ? err.message : ''; + const { StackId, RequestId, LogicalResourceId, PhysicalResourceId, ResponseURL } = event; + + const body = JSON.stringify({ + StackId, + RequestId, + LogicalResourceId, + PhysicalResourceId, + Status: status, + Reason: reason && `${reason} See details in CloudWatch Log: ${context.logStreamName}`, + Data: data, + }); + + logger.log(body); + + const parsedUrl = url.parse(ResponseURL); + const options = { + hostname: parsedUrl.hostname, + port: 443, + path: parsedUrl.path, + method: 'PUT', + headers: { + 'Content-Type': '', + 'Content-Length': body.length, + }, + }; + + return new Promise((resolve, reject) => { + const request = https.request(options, resp => { + logger.log(`STATUS: ${resp.statusCode}`); + logger.log(`HEADERS: ${JSON.stringify(resp.headers)}`); + return resolve(data); + }); + + request.on('error', error => { + logger.log(`sendResponse Error:\n${error}`); + return reject(error); + }); + + request.on('end', () => { + logger.log('end'); + return resolve(); + }); + + request.write(body); + request.end(); + }); +} + +function getLambdaArn(region, accountId, functionName) { + return `arn:aws:lambda:${region}:${accountId}:function:${functionName}`; +} + +function getEnvironment(context) { + const arn = context.invokedFunctionArn.match( + /^arn:aws.*:lambda:(\w+-\w+-\d+):(\d+):function:(.*)$/ + ); + return { + LambdaArn: arn[0], + Region: arn[1], + AccountId: arn[2], + LambdaName: arn[3], + }; +} + +function handlerWrapper(handler, PhysicalResourceId) { + return (event, context, callback) => { + // extend the `event` object to include the PhysicalResourceId + event = Object.assign({}, event, { PhysicalResourceId }); + return Promise.resolve(handler(event, context, callback)) + .then( + result => response(event, context, 'SUCCESS', result), + error => response(event, context, 'FAILED', {}, error) + ) + .then(result => callback(null, result), callback); + }; +} + +module.exports = { + logger, + response, + getEnvironment, + getLambdaArn, + handlerWrapper, +}; diff --git a/lib/plugins/aws/customResources/resources/utils.test.js b/lib/plugins/aws/customResources/resources/utils.test.js new file mode 100644 index 000000000..3a7409702 --- /dev/null +++ b/lib/plugins/aws/customResources/resources/utils.test.js @@ -0,0 +1,31 @@ +'use strict'; + +const { expect } = require('chai'); +const { getLambdaArn, getEnvironment } = require('./utils'); + +describe('#getLambdaArn()', () => { + it('should return the Lambda arn', () => { + const region = 'us-east-1'; + const accountId = '123456'; + const functionName = 'some-function'; + const arn = getLambdaArn(region, accountId, functionName); + + expect(arn).to.equal('arn:aws:lambda:us-east-1:123456:function:some-function'); + }); +}); + +describe('#getEnvironment()', () => { + it('should return an object with information about the execution environment', () => { + const context = { + invokedFunctionArn: 'arn:aws:lambda:us-east-1:123456:function:some-function', + }; + const env = getEnvironment(context); + + expect(env).to.deep.equal({ + LambdaArn: 'arn:aws:lambda:us-east-1:123456:function:some-function', + Region: 'us-east-1', + AccountId: '123456', + LambdaName: 'some-function', + }); + }); +}); diff --git a/lib/plugins/aws/deploy/index.js b/lib/plugins/aws/deploy/index.js index 46bb2cd5d..c2534760f 100644 --- a/lib/plugins/aws/deploy/index.js +++ b/lib/plugins/aws/deploy/index.js @@ -19,7 +19,8 @@ class AwsDeploy { this.options = options; this.provider = this.serverless.getProvider('aws'); this.servicePath = this.serverless.config.servicePath || ''; - this.packagePath = this.options.package || + this.packagePath = + this.options.package || this.serverless.service.package.path || path.join(this.servicePath, '.serverless'); @@ -54,9 +55,7 @@ class AwsDeploy { ], }, finalize: { - lifecycleEvents: [ - 'cleanup', - ], + lifecycleEvents: ['cleanup'], }, }, }, @@ -65,28 +64,29 @@ class AwsDeploy { }; this.hooks = { - 'before:deploy:deploy': () => BbPromise.bind(this) - .then(() => this.serverless.pluginManager.spawn('aws:common:validate')) - .then(() => { - const bucketName = this.serverless.service.provider.deploymentBucket; - if (bucketName) { - return this.existsDeploymentBucket(bucketName); - } + 'before:deploy:deploy': () => + BbPromise.bind(this) + .then(() => this.serverless.pluginManager.spawn('aws:common:validate')) + .then(() => { + const bucketName = this.serverless.service.provider.deploymentBucket; + if (bucketName) { + return this.existsDeploymentBucket(bucketName); + } - return BbPromise.resolve(); - }) - .then(() => { - if (!this.options.package && !this.serverless.service.package.path) { - return this.extendedValidate(); - } - return BbPromise.bind(this) - .then(() => this.serverless.pluginManager.spawn('aws:common:moveArtifactsToTemp')) - .then(this.extendedValidate); - }), + return BbPromise.resolve(); + }) + .then(() => { + if (!this.options.package && !this.serverless.service.package.path) { + return this.extendedValidate(); + } + return BbPromise.bind(this) + .then(() => this.serverless.pluginManager.spawn('aws:common:moveArtifactsToTemp')) + .then(this.extendedValidate); + }), // Deploy outer lifecycle - 'deploy:deploy': () => BbPromise.bind(this) - .then(() => { + 'deploy:deploy': () => + BbPromise.bind(this).then(() => { if (this.options.noDeploy) { return BbPromise.resolve(); } @@ -96,31 +96,31 @@ class AwsDeploy { 'deploy:finalize': () => this.serverless.pluginManager.spawn('aws:deploy:finalize'), // Deploy deploy inner lifecycle - 'aws:deploy:deploy:createStack': () => BbPromise.bind(this) - .then(this.createStack), + 'aws:deploy:deploy:createStack': () => BbPromise.bind(this).then(this.createStack), - 'aws:deploy:deploy:checkForChanges': () => BbPromise.bind(this) - .then(this.setBucketName) - .then(this.checkForChanges), + 'aws:deploy:deploy:checkForChanges': () => + BbPromise.bind(this) + .then(this.setBucketName) + .then(this.checkForChanges), - 'aws:deploy:deploy:uploadArtifacts': () => BbPromise.bind(this) - .then(() => { + 'aws:deploy:deploy:uploadArtifacts': () => + BbPromise.bind(this).then(() => { if (this.serverless.service.provider.shouldNotDeploy) { return BbPromise.resolve(); } return BbPromise.bind(this).then(this.uploadArtifacts); }), - 'aws:deploy:deploy:validateTemplate': () => BbPromise.bind(this) - .then(() => { + 'aws:deploy:deploy:validateTemplate': () => + BbPromise.bind(this).then(() => { if (this.serverless.service.provider.shouldNotDeploy) { return BbPromise.resolve(); } return BbPromise.bind(this).then(this.validateTemplate); }), - 'aws:deploy:deploy:updateStack': () => BbPromise.bind(this) - .then(() => { + 'aws:deploy:deploy:updateStack': () => + BbPromise.bind(this).then(() => { if (this.serverless.service.provider.shouldNotDeploy) { return BbPromise.resolve(); } @@ -128,20 +128,22 @@ class AwsDeploy { }), // Deploy finalize inner lifecycle - 'aws:deploy:finalize:cleanup': () => BbPromise.bind(this) - .then(() => { - if (this.options.noDeploy || this.serverless.service.provider.shouldNotDeploy) { + 'aws:deploy:finalize:cleanup': () => + BbPromise.bind(this) + .then(() => { + if (this.options.noDeploy || this.serverless.service.provider.shouldNotDeploy) { + return BbPromise.resolve(); + } + return this.cleanupS3Bucket(); + }) + .then(() => { + if (this.options.package || this.serverless.service.package.path) { + return BbPromise.bind(this).then(() => + this.serverless.pluginManager.spawn('aws:common:cleanupTempDir') + ); + } return BbPromise.resolve(); - } - return this.cleanupS3Bucket(); - }) - .then(() => { - if (this.options.package || this.serverless.service.package.path) { - return BbPromise.bind(this) - .then(() => this.serverless.pluginManager.spawn('aws:common:cleanupTempDir')); - } - return BbPromise.resolve(); - }), + }), }; } } diff --git a/lib/plugins/aws/deploy/index.test.js b/lib/plugins/aws/deploy/index.test.js index f8b7d324e..bcf29223d 100644 --- a/lib/plugins/aws/deploy/index.test.js +++ b/lib/plugins/aws/deploy/index.test.js @@ -87,14 +87,10 @@ describe('AwsDeploy', () => { let checkForChangesStub; beforeEach(() => { - spawnStub = sinon - .stub(serverless.pluginManager, 'spawn'); - createStackStub = sinon - .stub(awsDeploy, 'createStack').resolves(); - setBucketNameStub = sinon - .stub(awsDeploy, 'setBucketName').resolves(); - checkForChangesStub = sinon - .stub(awsDeploy, 'checkForChanges').resolves(); + spawnStub = sinon.stub(serverless.pluginManager, 'spawn'); + createStackStub = sinon.stub(awsDeploy, 'createStack').resolves(); + setBucketNameStub = sinon.stub(awsDeploy, 'setBucketName').resolves(); + checkForChangesStub = sinon.stub(awsDeploy, 'checkForChanges').resolves(); }); afterEach(() => { @@ -112,14 +108,13 @@ describe('AwsDeploy', () => { let spawnAwsCommonMoveArtifactsToTemp; beforeEach(() => { - extendedValidateStub = sinon - .stub(awsDeploy, 'extendedValidate').resolves(); - existsDeploymentBucketStub = sinon - .stub(awsDeploy, 'existsDeploymentBucket').resolves(); + extendedValidateStub = sinon.stub(awsDeploy, 'extendedValidate').resolves(); + existsDeploymentBucketStub = sinon.stub(awsDeploy, 'existsDeploymentBucket').resolves(); spawnPackageStub = spawnStub.withArgs('package').resolves(); spawnAwsCommonValidateStub = spawnStub.withArgs('aws:common:validate').resolves(); spawnAwsCommonMoveArtifactsToTemp = spawnStub - .withArgs('aws:common:moveArtifactsToTemp').resolves(); + .withArgs('aws:common:moveArtifactsToTemp') + .resolves(); }); afterEach(() => { @@ -143,10 +138,12 @@ describe('AwsDeploy', () => { return awsDeploy.hooks['before:deploy:deploy']().then(() => { expect(spawnAwsCommonValidateStub.calledOnce).to.equal(true); - expect(spawnAwsCommonMoveArtifactsToTemp.calledAfter(spawnAwsCommonValidateStub)) - .to.equal(true); - expect(extendedValidateStub.calledAfter(spawnAwsCommonMoveArtifactsToTemp)) - .to.equal(true); + expect( + spawnAwsCommonMoveArtifactsToTemp.calledAfter(spawnAwsCommonValidateStub) + ).to.equal(true); + expect(extendedValidateStub.calledAfter(spawnAwsCommonMoveArtifactsToTemp)).to.equal( + true + ); expect(spawnPackageStub.calledOnce).to.equal(false); }); }); @@ -157,10 +154,12 @@ describe('AwsDeploy', () => { return awsDeploy.hooks['before:deploy:deploy']().then(() => { expect(spawnAwsCommonValidateStub.calledOnce).to.equal(true); - expect(spawnAwsCommonMoveArtifactsToTemp.calledAfter(spawnAwsCommonValidateStub)) - .to.equal(true); - expect(extendedValidateStub.calledAfter(spawnAwsCommonMoveArtifactsToTemp)) - .to.equal(true); + expect( + spawnAwsCommonMoveArtifactsToTemp.calledAfter(spawnAwsCommonValidateStub) + ).to.equal(true); + expect(extendedValidateStub.calledAfter(spawnAwsCommonMoveArtifactsToTemp)).to.equal( + true + ); expect(spawnPackageStub.calledOnce).to.equal(false); }); }); @@ -173,12 +172,13 @@ describe('AwsDeploy', () => { return awsDeploy.hooks['before:deploy:deploy']().then(() => { expect(spawnAwsCommonValidateStub.calledOnce).to.equal(true); - expect(existsDeploymentBucketStub.calledAfter(spawnAwsCommonValidateStub)) - .to.equal(true); - expect(spawnAwsCommonMoveArtifactsToTemp.calledAfter(existsDeploymentBucketStub)) - .to.equal(true); - expect(extendedValidateStub.calledAfter(spawnAwsCommonMoveArtifactsToTemp)) - .to.equal(true); + expect(existsDeploymentBucketStub.calledAfter(spawnAwsCommonValidateStub)).to.equal(true); + expect( + spawnAwsCommonMoveArtifactsToTemp.calledAfter(existsDeploymentBucketStub) + ).to.equal(true); + expect(extendedValidateStub.calledAfter(spawnAwsCommonMoveArtifactsToTemp)).to.equal( + true + ); expect(spawnPackageStub.calledOnce).to.equal(false); }); }); @@ -200,44 +200,41 @@ describe('AwsDeploy', () => { }); }); - it('should run "aws:deploy:deploy:createStack" hook', () => awsDeploy - .hooks['aws:deploy:deploy:createStack']().then(() => { + it('should run "aws:deploy:deploy:createStack" hook', () => + awsDeploy.hooks['aws:deploy:deploy:createStack']().then(() => { expect(createStackStub.calledOnce).to.equal(true); - }) - ); + })); - it('should run "aws:deploy:deploy:checkForChanges" hook', () => awsDeploy - .hooks['aws:deploy:deploy:checkForChanges']().then(() => { + it('should run "aws:deploy:deploy:checkForChanges" hook', () => + awsDeploy.hooks['aws:deploy:deploy:checkForChanges']().then(() => { expect(setBucketNameStub.calledOnce).to.equal(true); expect(checkForChangesStub.calledAfter(setBucketNameStub)).to.equal(true); - }) - ); + })); describe('"aws:deploy:deploy:uploadArtifacts" hook', () => { let uploadArtifactsStub; beforeEach(() => { - uploadArtifactsStub = sinon - .stub(awsDeploy, 'uploadArtifacts').resolves(); + uploadArtifactsStub = sinon.stub(awsDeploy, 'uploadArtifacts').resolves(); }); afterEach(() => { awsDeploy.uploadArtifacts.restore(); }); - it('should upload the artifacts if a deployment is necessary', () => expect(awsDeploy - .hooks['aws:deploy:deploy:uploadArtifacts']()).to.be.fulfilled.then(() => { + it('should upload the artifacts if a deployment is necessary', () => + expect(awsDeploy.hooks['aws:deploy:deploy:uploadArtifacts']()).to.be.fulfilled.then(() => { expect(uploadArtifactsStub).to.have.been.calledOnce; - }) - ); + })); it('should resolve if no deployment is necessary', () => { awsDeploy.serverless.service.provider.shouldNotDeploy = true; - return expect(awsDeploy - .hooks['aws:deploy:deploy:uploadArtifacts']()).to.be.fulfilled.then(() => { + return expect(awsDeploy.hooks['aws:deploy:deploy:uploadArtifacts']()).to.be.fulfilled.then( + () => { expect(uploadArtifactsStub).to.not.have.been.called; - }); + } + ); }); }); @@ -245,27 +242,26 @@ describe('AwsDeploy', () => { let validateTemplateStub; beforeEach(() => { - validateTemplateStub = sinon - .stub(awsDeploy, 'validateTemplate').resolves(); + validateTemplateStub = sinon.stub(awsDeploy, 'validateTemplate').resolves(); }); afterEach(() => { awsDeploy.validateTemplate.restore(); }); - it('should validate the template if a deployment is necessary', () => expect(awsDeploy - .hooks['aws:deploy:deploy:validateTemplate']()).to.be.fulfilled.then(() => { + it('should validate the template if a deployment is necessary', () => + expect(awsDeploy.hooks['aws:deploy:deploy:validateTemplate']()).to.be.fulfilled.then(() => { expect(validateTemplateStub).to.have.been.calledOnce; - }) - ); + })); it('should resolve if no deployment is necessary', () => { awsDeploy.serverless.service.provider.shouldNotDeploy = true; - return expect(awsDeploy - .hooks['aws:deploy:deploy:validateTemplate']()).to.be.fulfilled.then(() => { + return expect(awsDeploy.hooks['aws:deploy:deploy:validateTemplate']()).to.be.fulfilled.then( + () => { expect(validateTemplateStub).to.not.have.been.called; - }); + } + ); }); }); @@ -273,27 +269,26 @@ describe('AwsDeploy', () => { let updateStackStub; beforeEach(() => { - updateStackStub = sinon - .stub(awsDeploy, 'updateStack').resolves(); + updateStackStub = sinon.stub(awsDeploy, 'updateStack').resolves(); }); afterEach(() => { awsDeploy.updateStack.restore(); }); - it('should update the stack if a deployment is necessary', () => expect(awsDeploy - .hooks['aws:deploy:deploy:updateStack']()).to.be.fulfilled.then(() => { + it('should update the stack if a deployment is necessary', () => + expect(awsDeploy.hooks['aws:deploy:deploy:updateStack']()).to.be.fulfilled.then(() => { expect(updateStackStub).to.have.been.calledOnce; - }) - ); + })); it('should resolve if no deployment is necessary', () => { awsDeploy.serverless.service.provider.shouldNotDeploy = true; - return expect(awsDeploy - .hooks['aws:deploy:deploy:updateStack']()).to.be.fulfilled.then(() => { + return expect(awsDeploy.hooks['aws:deploy:deploy:updateStack']()).to.be.fulfilled.then( + () => { expect(updateStackStub).to.not.have.been.called; - }); + } + ); }); }); @@ -302,9 +297,9 @@ describe('AwsDeploy', () => { let spawnAwsCommonCleanupTempDirStub; beforeEach(() => { - cleanupS3BucketStub = sinon - .stub(awsDeploy, 'cleanupS3Bucket').resolves(); - spawnAwsCommonCleanupTempDirStub = spawnStub.withArgs('aws:common:cleanupTempDir') + cleanupS3BucketStub = sinon.stub(awsDeploy, 'cleanupS3Bucket').resolves(); + spawnAwsCommonCleanupTempDirStub = spawnStub + .withArgs('aws:common:cleanupTempDir') .resolves(); }); @@ -328,8 +323,7 @@ describe('AwsDeploy', () => { return awsDeploy.hooks['aws:deploy:finalize:cleanup']().then(() => { expect(cleanupS3BucketStub.calledOnce).to.equal(true); - expect(spawnAwsCommonCleanupTempDirStub.calledAfter(cleanupS3BucketStub)) - .to.equal(true); + expect(spawnAwsCommonCleanupTempDirStub.calledAfter(cleanupS3BucketStub)).to.equal(true); }); }); @@ -339,8 +333,7 @@ describe('AwsDeploy', () => { return awsDeploy.hooks['aws:deploy:finalize:cleanup']().then(() => { expect(cleanupS3BucketStub.calledOnce).to.equal(true); - expect(spawnAwsCommonCleanupTempDirStub.calledAfter(cleanupS3BucketStub)) - .to.equal(true); + expect(spawnAwsCommonCleanupTempDirStub.calledAfter(cleanupS3BucketStub)).to.equal(true); }); }); diff --git a/lib/plugins/aws/deploy/lib/checkForChanges.js b/lib/plugins/aws/deploy/lib/checkForChanges.js index 07635ad45..ec4419c25 100644 --- a/lib/plugins/aws/deploy/lib/checkForChanges.js +++ b/lib/plugins/aws/deploy/lib/checkForChanges.js @@ -38,55 +38,56 @@ module.exports = { Prefix: `${this.provider.getDeploymentPrefix()}/${service}/${this.provider.getStage()}`, }; - return this.provider.request('S3', - 'listObjectsV2', - params - ).catch((reason) => { - if (!_.includes(reason.message, 'The specified bucket does not exist')) { - return BbPromise.reject(reason); - } - const stackName = this.provider.naming.getStackName(); - return BbPromise.reject(new this.serverless.classes.Error([ - `The serverless deployment bucket "${params.Bucket}" does not exist.`, - `Create it manually if you want to reuse the CloudFormation stack "${stackName}",`, - 'or delete the stack if it is no longer required.', - ].join(' '))); - }).then((result) => { - if (result && result.Contents && result.Contents.length) { - const objects = result.Contents; + return this.provider + .request('S3', 'listObjectsV2', params) + .catch(reason => { + if (!_.includes(reason.message, 'The specified bucket does not exist')) { + return BbPromise.reject(reason); + } + const stackName = this.provider.naming.getStackName(); + return BbPromise.reject( + new this.serverless.classes.Error( + [ + `The serverless deployment bucket "${params.Bucket}" does not exist.`, + `Create it manually if you want to reuse the CloudFormation stack "${stackName}",`, + 'or delete the stack if it is no longer required.', + ].join(' ') + ) + ); + }) + .then(result => { + if (result && result.Contents && result.Contents.length) { + const objects = result.Contents; - const ordered = _.orderBy(objects, ['Key'], ['desc']); + const ordered = _.orderBy(objects, ['Key'], ['desc']); - const firstKey = ordered[0].Key; - const directory = firstKey.substring(0, firstKey.lastIndexOf('/')); + const firstKey = ordered[0].Key; + const directory = firstKey.substring(0, firstKey.lastIndexOf('/')); - const mostRecentObjects = ordered.filter((obj) => { - const objKey = obj.Key; - const objDirectory = objKey.substring(0, objKey.lastIndexOf('/')); + const mostRecentObjects = ordered.filter(obj => { + const objKey = obj.Key; + const objDirectory = objKey.substring(0, objKey.lastIndexOf('/')); - return directory === objDirectory; - }); + return directory === objDirectory; + }); - return BbPromise.resolve(mostRecentObjects); - } + return BbPromise.resolve(mostRecentObjects); + } - return BbPromise.resolve([]); - }); + return BbPromise.resolve([]); + }); }, getObjectMetadata(objects) { if (objects && objects.length) { - const headObjectObjects = objects - .map((obj) => this.provider.request('S3', - 'headObject', - { - Bucket: this.bucketName, - Key: obj.Key, - } - )); + const headObjectObjects = objects.map(obj => + this.provider.request('S3', 'headObject', { + Bucket: this.bucketName, + Key: obj.Key, + }) + ); - return BbPromise.all(headObjectObjects) - .then((result) => result); + return BbPromise.all(headObjectObjects).then(result => result); } return BbPromise.resolve([]); @@ -94,7 +95,7 @@ module.exports = { checkIfDeploymentIsNecessary(objects) { if (objects && objects.length) { - const remoteHashes = objects.map((object) => object.Metadata.filesha256 || ''); + const remoteHashes = objects.map(object => object.Metadata.filesha256 || ''); const serverlessDirPath = path.join(this.serverless.config.servicePath, '.serverless'); @@ -108,11 +109,17 @@ module.exports = { // create hashes for all the zip files const zipFiles = globby.sync(['**.zip'], { cwd: serverlessDirPath, dot: true, silent: true }); - const zipFilePaths = zipFiles.map((zipFile) => path.join(serverlessDirPath, zipFile)); + const zipFilePaths = zipFiles.map(zipFile => path.join(serverlessDirPath, zipFile)); const readFile = BbPromise.promisify(fs.readFile); - const zipFileHashesPromises = zipFilePaths.map(zipFilePath => readFile(zipFilePath) - .then(zipFile => crypto.createHash('sha256').update(zipFile).digest('base64'))); + const zipFileHashesPromises = zipFilePaths.map(zipFilePath => + readFile(zipFilePath).then(zipFile => + crypto + .createHash('sha256') + .update(zipFile) + .digest('base64') + ) + ); return BbPromise.all(zipFileHashesPromises).then(zipFileHashes => { const localHashes = zipFileHashes; @@ -121,9 +128,7 @@ module.exports = { if (_.isEqual(remoteHashes.sort(), localHashes.sort())) { this.serverless.service.provider.shouldNotDeploy = true; - const message = [ - 'Service files not changed. Skipping deployment...', - ].join(''); + const message = ['Service files not changed. Skipping deployment...'].join(''); this.serverless.cli.log(message, 'Serverless', { color: 'orange' }); } }); @@ -141,23 +146,23 @@ module.exports = { * subscription filters of functions that a new filter was provided, by checking the * current ARN with the new one that will be generated. * See: https://git.io/fpKCM - */ + */ checkLogGroupSubscriptionFilterResourceLimitExceeded() { const region = this.provider.getRegion(); const serviceName = this.serverless.service.getServiceName(); const stage = this.provider.getStage(); const cloudWatchLogsSdk = new this.provider.sdk.CloudWatchLogs({ region }); - return this.provider.getAccountId() - .then(accountId => - Promise.all(this.serverless.service.getAllFunctions().map((functionName) => { + return this.provider.getAccountId().then(accountId => + Promise.all( + this.serverless.service.getAllFunctions().map(functionName => { const functionObj = this.serverless.service.getFunction(functionName); if (!functionObj.events) { return BbPromise.resolve(); } - const promises = functionObj.events.map((event) => { + const promises = functionObj.events.map(event => { if (!event.cloudwatchLog) { return BbPromise.resolve(); } @@ -190,7 +195,9 @@ module.exports = { }); return Promise.all(promises); - }))); + }) + ) + ); }, fixLogGroupSubscriptionFilters(params) { @@ -202,36 +209,38 @@ module.exports = { const serviceName = params.serviceName; const stage = params.stage; - return cloudWatchLogsSdk.describeSubscriptionFilters({ logGroupName }).promise() - .then((response) => { - const subscriptionFilter = response.subscriptionFilters[0]; + return ( + cloudWatchLogsSdk + .describeSubscriptionFilters({ logGroupName }) + .promise() + .then(response => { + const subscriptionFilter = response.subscriptionFilters[0]; - // log group doesn't have any subscription filters currently - if (!subscriptionFilter) { - return false; - } + // log group doesn't have any subscription filters currently + if (!subscriptionFilter) { + return false; + } - const oldDestinationArn = subscriptionFilter.destinationArn; - const filterName = subscriptionFilter.filterName; - const newDestinationArn = - `arn:aws:lambda:${region}:${accountId}:function:${serviceName}-${stage}-${functionName}`; + const oldDestinationArn = subscriptionFilter.destinationArn; + const filterName = subscriptionFilter.filterName; + const newDestinationArn = `arn:aws:lambda:${region}:${accountId}:function:${serviceName}-${stage}-${functionName}`; - // everything is fine, just return - if (oldDestinationArn === newDestinationArn) { - return false; - } + // everything is fine, just return + if (oldDestinationArn === newDestinationArn) { + return false; + } - /* + /* If the destinations functions' ARNs doesn't match, we need to delete the current subscription filter to prevent the resource limit exceeded error to happen */ - return cloudWatchLogsSdk - .deleteSubscriptionFilter({ logGroupName, filterName }).promise(); - }) - /* + return cloudWatchLogsSdk.deleteSubscriptionFilter({ logGroupName, filterName }).promise(); + }) + /* it will throw when trying to get subscription filters of a log group that was just added to the serverless.yml (therefore not created in AWS yet), we can safely ignore this error */ - .catch(() => undefined); + .catch(() => undefined) + ); }, }; diff --git a/lib/plugins/aws/deploy/lib/checkForChanges.test.js b/lib/plugins/aws/deploy/lib/checkForChanges.test.js index 1e4b013d6..9cc808d38 100644 --- a/lib/plugins/aws/deploy/lib/checkForChanges.test.js +++ b/lib/plugins/aws/deploy/lib/checkForChanges.test.js @@ -5,7 +5,7 @@ const fs = require('fs'); const path = require('path'); const globby = require('globby'); -const sinon = require('sinon'); +const sandbox = require('sinon'); const chai = require('chai'); const BbPromise = require('bluebird'); const proxyquire = require('proxyquire'); @@ -42,19 +42,20 @@ describe('checkForChanges', () => { foo: 'bar', }; s3Key = `serverless/${serverless.service.service}/${provider.getStage()}`; - awsDeploy.serverless.cli = { log: sinon.spy() }; + awsDeploy.serverless.cli = { log: sandbox.spy() }; cryptoStub = { - createHash: function () { return this; }, // eslint-disable-line - update: function () { return this; }, // eslint-disable-line - digest: sinon.stub(), + createHash() { + return this; + }, // eslint-disable-line + update() { + return this; + }, // eslint-disable-line + digest: sandbox.stub(), }; const checkForChanges = proxyquire('./checkForChanges.js', { crypto: cryptoStub, }); - Object.assign( - awsDeploy, - checkForChanges - ); + Object.assign(awsDeploy, checkForChanges); }); describe('#checkForChanges()', () => { @@ -64,14 +65,14 @@ describe('checkForChanges', () => { let checkLogGroupSubscriptionFilterResourceLimitExceededStub; beforeEach(() => { - getMostRecentObjectsStub = sinon - .stub(awsDeploy, 'getMostRecentObjects').resolves(); - getObjectMetadataStub = sinon - .stub(awsDeploy, 'getObjectMetadata').resolves(); - checkIfDeploymentIsNecessaryStub = sinon - .stub(awsDeploy, 'checkIfDeploymentIsNecessary').resolves(); - checkLogGroupSubscriptionFilterResourceLimitExceededStub = sinon - .stub(awsDeploy, 'checkLogGroupSubscriptionFilterResourceLimitExceeded').resolves(); + getMostRecentObjectsStub = sandbox.stub(awsDeploy, 'getMostRecentObjects').resolves(); + getObjectMetadataStub = sandbox.stub(awsDeploy, 'getObjectMetadata').resolves(); + checkIfDeploymentIsNecessaryStub = sandbox + .stub(awsDeploy, 'checkIfDeploymentIsNecessary') + .resolves(); + checkLogGroupSubscriptionFilterResourceLimitExceededStub = sandbox + .stub(awsDeploy, 'checkLogGroupSubscriptionFilterResourceLimitExceeded') + .resolves(); }); afterEach(() => { @@ -81,29 +82,28 @@ describe('checkForChanges', () => { awsDeploy.checkLogGroupSubscriptionFilterResourceLimitExceeded.restore(); }); - it('should run promise chain in order', () => expect(awsDeploy.checkForChanges()) - .to.be.fulfilled.then(() => { + it('should run promise chain in order', () => + expect(awsDeploy.checkForChanges()).to.be.fulfilled.then(() => { expect(getMostRecentObjectsStub).to.have.been.calledOnce; expect(getObjectMetadataStub).to.have.been.calledAfter(getMostRecentObjectsStub); expect(checkIfDeploymentIsNecessaryStub).to.have.been.calledAfter(getObjectMetadataStub); - expect(checkLogGroupSubscriptionFilterResourceLimitExceededStub).to.have.been - .calledAfter(checkIfDeploymentIsNecessaryStub); + expect(checkLogGroupSubscriptionFilterResourceLimitExceededStub).to.have.been.calledAfter( + checkIfDeploymentIsNecessaryStub + ); expect(awsDeploy.serverless.service.provider.shouldNotDeploy).to.equal(false); - }) - ); + })); it('should resolve if the "force" option is used', () => { awsDeploy.options.force = true; - return expect(awsDeploy.checkForChanges()) - .to.be.fulfilled.then(() => { - expect(getMostRecentObjectsStub).to.not.have.been.called; - expect(getObjectMetadataStub).to.not.have.been.called; - expect(checkIfDeploymentIsNecessaryStub).to.not.have.been.called; + return expect(awsDeploy.checkForChanges()).to.be.fulfilled.then(() => { + expect(getMostRecentObjectsStub).to.not.have.been.called; + expect(getObjectMetadataStub).to.not.have.been.called; + expect(checkIfDeploymentIsNecessaryStub).to.not.have.been.called; - expect(awsDeploy.serverless.service.provider.shouldNotDeploy).to.equal(false); - }); + expect(awsDeploy.serverless.service.provider.shouldNotDeploy).to.equal(false); + }); }); }); @@ -111,8 +111,7 @@ describe('checkForChanges', () => { let listObjectsV2Stub; beforeEach(() => { - listObjectsV2Stub = sinon - .stub(awsDeploy.provider, 'request'); + listObjectsV2Stub = sandbox.stub(awsDeploy.provider, 'request'); }); afterEach(() => { @@ -122,33 +121,31 @@ describe('checkForChanges', () => { it('should resolve if no result is returned', () => { listObjectsV2Stub.resolves(); - return expect(awsDeploy.getMostRecentObjects()).to.be.fulfilled.then((result) => { - expect(listObjectsV2Stub).to.have.been.calledWithExactly( - 'S3', - 'listObjectsV2', - { - Bucket: awsDeploy.bucketName, - Prefix: 'serverless/my-service/dev', - } - ); + return expect(awsDeploy.getMostRecentObjects()).to.be.fulfilled.then(result => { + expect(listObjectsV2Stub).to.have.been.calledWithExactly('S3', 'listObjectsV2', { + Bucket: awsDeploy.bucketName, + Prefix: 'serverless/my-service/dev', + }); expect(result).to.deep.equal([]); }); }); it('should translate error if rejected due to missing bucket', () => { - listObjectsV2Stub - .rejects(new serverless.classes.Error('The specified bucket does not exist')); + listObjectsV2Stub.rejects( + new serverless.classes.Error('The specified bucket does not exist') + ); - return expect(awsDeploy.getMostRecentObjects()).to.be.rejectedWith([ - `The serverless deployment bucket "${awsDeploy.bucketName}" does not exist.`, - 'Create it manually if you want to reuse the CloudFormation stack "my-service-dev",', - 'or delete the stack if it is no longer required.', - ].join(' ')); + return expect(awsDeploy.getMostRecentObjects()).to.be.rejectedWith( + [ + `The serverless deployment bucket "${awsDeploy.bucketName}" does not exist.`, + 'Create it manually if you want to reuse the CloudFormation stack "my-service-dev",', + 'or delete the stack if it is no longer required.', + ].join(' ') + ); }); it('should throw original error if rejected not due to missing bucket', () => { - listObjectsV2Stub - .rejects(new serverless.classes.Error('Other reason')); + listObjectsV2Stub.rejects(new serverless.classes.Error('Other reason')); return expect(awsDeploy.getMostRecentObjects()).to.be.rejectedWith('Other reason'); }); @@ -159,15 +156,11 @@ describe('checkForChanges', () => { listObjectsV2Stub.resolves(serviceObjects); - return expect(awsDeploy.getMostRecentObjects()).to.be.fulfilled.then((result) => { - expect(listObjectsV2Stub).to.have.been.calledWithExactly( - 'S3', - 'listObjectsV2', - { - Bucket: awsDeploy.bucketName, - Prefix: 'serverless/my-service/dev', - } - ); + return expect(awsDeploy.getMostRecentObjects()).to.be.fulfilled.then(result => { + expect(listObjectsV2Stub).to.have.been.calledWithExactly('S3', 'listObjectsV2', { + Bucket: awsDeploy.bucketName, + Prefix: 'serverless/my-service/dev', + }); expect(result).to.deep.equal([]); }); }); @@ -184,15 +177,11 @@ describe('checkForChanges', () => { listObjectsV2Stub.resolves(serviceObjects); - return expect(awsDeploy.getMostRecentObjects()).to.be.fulfilled.then((result) => { - expect(listObjectsV2Stub).to.have.been.calledWithExactly( - 'S3', - 'listObjectsV2', - { - Bucket: awsDeploy.bucketName, - Prefix: 'serverless/my-service/dev', - } - ); + return expect(awsDeploy.getMostRecentObjects()).to.be.fulfilled.then(result => { + expect(listObjectsV2Stub).to.have.been.calledWithExactly('S3', 'listObjectsV2', { + Bucket: awsDeploy.bucketName, + Prefix: 'serverless/my-service/dev', + }); expect(result).to.deep.equal([ { Key: `${s3Key}/151224711231-2016-08-18T15:43:00/cloudformation.json` }, { Key: `${s3Key}/151224711231-2016-08-18T15:43:00/artifact.zip` }, @@ -205,25 +194,23 @@ describe('checkForChanges', () => { let headObjectStub; beforeEach(() => { - headObjectStub = sinon - .stub(awsDeploy.provider, 'request').resolves(); + headObjectStub = sandbox.stub(awsDeploy.provider, 'request').resolves(); }); afterEach(() => { awsDeploy.provider.request.restore(); }); - it('should resolve if no input is provided', () => expect(awsDeploy.getObjectMetadata()) - .to.be.fulfilled.then((result) => { + it('should resolve if no input is provided', () => + expect(awsDeploy.getObjectMetadata()).to.be.fulfilled.then(result => { expect(headObjectStub).to.not.have.been.called; expect(result).to.deep.equal([]); - }) - ); + })); it('should resolve if no objects are provided as input', () => { const input = []; - return expect(awsDeploy.getObjectMetadata(input)).to.be.fulfilled.then((result) => { + return expect(awsDeploy.getObjectMetadata(input)).to.be.fulfilled.then(result => { expect(headObjectStub).to.not.have.been.called; expect(result).to.deep.equal([]); }); @@ -239,38 +226,22 @@ describe('checkForChanges', () => { return expect(awsDeploy.getObjectMetadata(input)).to.be.fulfilled.then(() => { expect(headObjectStub.callCount).to.equal(4); - expect(headObjectStub).to.have.been.calledWithExactly( - 'S3', - 'headObject', - { - Bucket: awsDeploy.bucketName, - Key: `${s3Key}/151224711231-2016-08-18T15:43:00/artifact.zip`, - } - ); - expect(headObjectStub).to.have.been.calledWithExactly( - 'S3', - 'headObject', - { - Bucket: awsDeploy.bucketName, - Key: `${s3Key}/151224711231-2016-08-18T15:43:00/cloudformation.json`, - } - ); - expect(headObjectStub).to.have.been.calledWithExactly( - 'S3', - 'headObject', - { - Bucket: awsDeploy.bucketName, - Key: `${s3Key}/141264711231-2016-08-18T15:42:00/artifact.zip`, - } - ); - expect(headObjectStub).to.have.been.calledWithExactly( - 'S3', - 'headObject', - { - Bucket: awsDeploy.bucketName, - Key: `${s3Key}/141264711231-2016-08-18T15:42:00/cloudformation.json`, - } - ); + expect(headObjectStub).to.have.been.calledWithExactly('S3', 'headObject', { + Bucket: awsDeploy.bucketName, + Key: `${s3Key}/151224711231-2016-08-18T15:43:00/artifact.zip`, + }); + expect(headObjectStub).to.have.been.calledWithExactly('S3', 'headObject', { + Bucket: awsDeploy.bucketName, + Key: `${s3Key}/151224711231-2016-08-18T15:43:00/cloudformation.json`, + }); + expect(headObjectStub).to.have.been.calledWithExactly('S3', 'headObject', { + Bucket: awsDeploy.bucketName, + Key: `${s3Key}/141264711231-2016-08-18T15:42:00/artifact.zip`, + }); + expect(headObjectStub).to.have.been.calledWithExactly('S3', 'headObject', { + Bucket: awsDeploy.bucketName, + Key: `${s3Key}/141264711231-2016-08-18T15:42:00/cloudformation.json`, + }); }); }); }); @@ -281,14 +252,11 @@ describe('checkForChanges', () => { let readFileStub; beforeEach(() => { - normalizeCloudFormationTemplateStub = sinon + normalizeCloudFormationTemplateStub = sandbox .stub(normalizeFiles, 'normalizeCloudFormationTemplate') .returns(); - globbySyncStub = sinon - .stub(globby, 'sync'); - readFileStub = sinon - .stub(fs, 'readFile') - .yields(null, undefined); + globbySyncStub = sandbox.stub(globby, 'sync'); + readFileStub = sandbox.stub(fs, 'readFile').yields(null, undefined); }); afterEach(() => { @@ -297,175 +265,205 @@ describe('checkForChanges', () => { fs.readFile.restore(); }); - it('should resolve if no input is provided', () => expect(awsDeploy - .checkIfDeploymentIsNecessary()).to.be.fulfilled.then(() => { + it('should resolve if no input is provided', () => + expect(awsDeploy.checkIfDeploymentIsNecessary()).to.be.fulfilled.then(() => { expect(normalizeCloudFormationTemplateStub).to.not.have.been.called; expect(globbySyncStub).to.not.have.been.called; expect(readFileStub).to.not.have.been.called; expect(awsDeploy.serverless.cli.log).to.not.have.been.called; - }) - ); + })); it('should resolve if no objects are provided as input', () => { const input = []; - return expect(awsDeploy.checkIfDeploymentIsNecessary(input)) - .to.be.fulfilled.then(() => { - expect(normalizeCloudFormationTemplateStub).to.not.have.been.called; - expect(globbySyncStub).to.not.have.been.called; - expect(readFileStub).to.not.have.been.called; - expect(awsDeploy.serverless.cli.log).to.not.have.been.called; - }); + return expect(awsDeploy.checkIfDeploymentIsNecessary(input)).to.be.fulfilled.then(() => { + expect(normalizeCloudFormationTemplateStub).to.not.have.been.called; + expect(globbySyncStub).to.not.have.been.called; + expect(readFileStub).to.not.have.been.called; + expect(awsDeploy.serverless.cli.log).to.not.have.been.called; + }); }); it('should not set a flag if there are more remote hashes', () => { globbySyncStub.returns(['my-service.zip']); - cryptoStub.createHash().update().digest.onCall(0).returns('local-hash-cf-template'); - cryptoStub.createHash().update().digest.onCall(1).returns('local-hash-zip-file-1'); + cryptoStub + .createHash() + .update() + .digest.onCall(0) + .returns('local-hash-cf-template'); + cryptoStub + .createHash() + .update() + .digest.onCall(1) + .returns('local-hash-zip-file-1'); const input = [ { Metadata: { filesha256: 'remote-hash-cf-template' } }, { Metadata: { filesha256: 'remote-hash-zip-file-1' } }, - { Metadata: { /* no filesha256 available */ } }, // will be translated to '' + { + Metadata: { + /* no filesha256 available */ + }, + }, // will be translated to '' ]; - return expect(awsDeploy.checkIfDeploymentIsNecessary(input)) - .to.be.fulfilled.then(() => { - expect(normalizeCloudFormationTemplateStub).to.have.been.calledOnce; - expect(globbySyncStub).to.have.been.calledOnce; - expect(readFileStub).to.have.been.calledOnce; - expect(awsDeploy.serverless.cli.log).to.not.have.been.called; - expect(normalizeCloudFormationTemplateStub).to.have.been.calledWithExactly( - awsDeploy.serverless.service.provider.compiledCloudFormationTemplate - ); - expect(globbySyncStub).to.have.been.calledWithExactly( - ['**.zip'], - { - cwd: path.join(awsDeploy.serverless.config.servicePath, '.serverless'), - dot: true, - silent: true, - } - ); - expect(readFileStub).to.have.been.calledWith( - path.join('my-service/.serverless/my-service.zip') - ); - expect(awsDeploy.serverless.service.provider.shouldNotDeploy).to.equal(undefined); + return expect(awsDeploy.checkIfDeploymentIsNecessary(input)).to.be.fulfilled.then(() => { + expect(normalizeCloudFormationTemplateStub).to.have.been.calledOnce; + expect(globbySyncStub).to.have.been.calledOnce; + expect(readFileStub).to.have.been.calledOnce; + expect(awsDeploy.serverless.cli.log).to.not.have.been.called; + expect(normalizeCloudFormationTemplateStub).to.have.been.calledWithExactly( + awsDeploy.serverless.service.provider.compiledCloudFormationTemplate + ); + expect(globbySyncStub).to.have.been.calledWithExactly(['**.zip'], { + cwd: path.join(awsDeploy.serverless.config.servicePath, '.serverless'), + dot: true, + silent: true, }); + expect(readFileStub).to.have.been.calledWith( + path.join('my-service/.serverless/my-service.zip') + ); + expect(awsDeploy.serverless.service.provider.shouldNotDeploy).to.equal(undefined); + }); }); it('should not set a flag if remote and local hashes are different', () => { globbySyncStub.returns(['my-service.zip']); - cryptoStub.createHash().update().digest.onCall(0).returns('local-hash-cf-template'); - cryptoStub.createHash().update().digest.onCall(1).returns('local-hash-zip-file-1'); + cryptoStub + .createHash() + .update() + .digest.onCall(0) + .returns('local-hash-cf-template'); + cryptoStub + .createHash() + .update() + .digest.onCall(1) + .returns('local-hash-zip-file-1'); const input = [ { Metadata: { filesha256: 'remote-hash-cf-template' } }, { Metadata: { filesha256: 'remote-hash-zip-file-1' } }, ]; - return expect(awsDeploy.checkIfDeploymentIsNecessary(input)) - .to.be.fulfilled.then(() => { - expect(normalizeCloudFormationTemplateStub).to.have.been.calledOnce; - expect(globbySyncStub).to.have.been.calledOnce; - expect(readFileStub).to.have.been.calledOnce; - expect(awsDeploy.serverless.cli.log).to.not.have.been.called; - expect(normalizeCloudFormationTemplateStub).to.have.been.calledWithExactly( - awsDeploy.serverless.service.provider.compiledCloudFormationTemplate - ); - expect(globbySyncStub).to.have.been.calledWithExactly( - ['**.zip'], - { - cwd: path.join(awsDeploy.serverless.config.servicePath, '.serverless'), - dot: true, - silent: true, - } - ); - expect(readFileStub).to.have.been.calledWith( - path.join('my-service/.serverless/my-service.zip') - ); - expect(awsDeploy.serverless.service.provider.shouldNotDeploy).to.equal(undefined); + return expect(awsDeploy.checkIfDeploymentIsNecessary(input)).to.be.fulfilled.then(() => { + expect(normalizeCloudFormationTemplateStub).to.have.been.calledOnce; + expect(globbySyncStub).to.have.been.calledOnce; + expect(readFileStub).to.have.been.calledOnce; + expect(awsDeploy.serverless.cli.log).to.not.have.been.called; + expect(normalizeCloudFormationTemplateStub).to.have.been.calledWithExactly( + awsDeploy.serverless.service.provider.compiledCloudFormationTemplate + ); + expect(globbySyncStub).to.have.been.calledWithExactly(['**.zip'], { + cwd: path.join(awsDeploy.serverless.config.servicePath, '.serverless'), + dot: true, + silent: true, }); + expect(readFileStub).to.have.been.calledWith( + path.join('my-service/.serverless/my-service.zip') + ); + expect(awsDeploy.serverless.service.provider.shouldNotDeploy).to.equal(undefined); + }); }); it('should not set a flag if remote and local hashes are the same but are duplicated', () => { globbySyncStub.returns(['func1.zip', 'func2.zip']); - cryptoStub.createHash().update().digest.onCall(0).returns('remote-hash-cf-template'); + cryptoStub + .createHash() + .update() + .digest.onCall(0) + .returns('remote-hash-cf-template'); // happens when package.individually is used - cryptoStub.createHash().update().digest.onCall(1).returns('remote-hash-zip-file-1'); - cryptoStub.createHash().update().digest.onCall(2).returns('remote-hash-zip-file-1'); + cryptoStub + .createHash() + .update() + .digest.onCall(1) + .returns('remote-hash-zip-file-1'); + cryptoStub + .createHash() + .update() + .digest.onCall(2) + .returns('remote-hash-zip-file-1'); const input = [ { Metadata: { filesha256: 'remote-hash-cf-template' } }, { Metadata: { filesha256: 'remote-hash-zip-file-1' } }, ]; - return expect(awsDeploy.checkIfDeploymentIsNecessary(input)) - .to.be.fulfilled.then(() => { - expect(normalizeCloudFormationTemplateStub).to.have.been.calledOnce; - expect(globbySyncStub).to.have.been.calledOnce; - expect(readFileStub).to.have.been.calledTwice; - expect(awsDeploy.serverless.cli.log).to.not.have.been.called; - expect(normalizeCloudFormationTemplateStub).to.have.been.calledWithExactly( - awsDeploy.serverless.service.provider.compiledCloudFormationTemplate - ); - expect(globbySyncStub).to.have.been.calledWithExactly( - ['**.zip'], - { - cwd: path.join(awsDeploy.serverless.config.servicePath, '.serverless'), - dot: true, - silent: true, - } - ); - expect(readFileStub).to.have.been.calledWith( - path.join('my-service/.serverless/func1.zip') - ); - expect(readFileStub).to.have.been.calledWith( - path.join('my-service/.serverless/func2.zip') - ); - expect(awsDeploy.serverless.service.provider.shouldNotDeploy).to.equal(undefined); + return expect(awsDeploy.checkIfDeploymentIsNecessary(input)).to.be.fulfilled.then(() => { + expect(normalizeCloudFormationTemplateStub).to.have.been.calledOnce; + expect(globbySyncStub).to.have.been.calledOnce; + expect(readFileStub).to.have.been.calledTwice; + expect(awsDeploy.serverless.cli.log).to.not.have.been.called; + expect(normalizeCloudFormationTemplateStub).to.have.been.calledWithExactly( + awsDeploy.serverless.service.provider.compiledCloudFormationTemplate + ); + expect(globbySyncStub).to.have.been.calledWithExactly(['**.zip'], { + cwd: path.join(awsDeploy.serverless.config.servicePath, '.serverless'), + dot: true, + silent: true, }); + expect(readFileStub).to.have.been.calledWith(path.join('my-service/.serverless/func1.zip')); + expect(readFileStub).to.have.been.calledWith(path.join('my-service/.serverless/func2.zip')); + expect(awsDeploy.serverless.service.provider.shouldNotDeploy).to.equal(undefined); + }); }); it('should set a flag if the remote and local hashes are equal', () => { globbySyncStub.returns(['my-service.zip']); - cryptoStub.createHash().update().digest.onCall(0).returns('hash-cf-template'); - cryptoStub.createHash().update().digest.onCall(1).returns('hash-zip-file-1'); + cryptoStub + .createHash() + .update() + .digest.onCall(0) + .returns('hash-cf-template'); + cryptoStub + .createHash() + .update() + .digest.onCall(1) + .returns('hash-zip-file-1'); const input = [ { Metadata: { filesha256: 'hash-cf-template' } }, { Metadata: { filesha256: 'hash-zip-file-1' } }, ]; - return expect(awsDeploy.checkIfDeploymentIsNecessary(input)) - .to.be.fulfilled.then(() => { - expect(normalizeCloudFormationTemplateStub).to.have.been.calledOnce; - expect(globbySyncStub).to.have.been.calledOnce; - expect(readFileStub).to.have.been.calledOnce; - expect(awsDeploy.serverless.cli.log).to.have.been.calledOnce; - expect(normalizeCloudFormationTemplateStub).to.have.been.calledWithExactly( - awsDeploy.serverless.service.provider.compiledCloudFormationTemplate - ); - expect(globbySyncStub).to.have.been.calledWithExactly( - ['**.zip'], - { - cwd: path.join(awsDeploy.serverless.config.servicePath, '.serverless'), - dot: true, - silent: true, - } - ); - expect(readFileStub).to.have.been.calledWith( - path.join('my-service/.serverless/my-service.zip') - ); - expect(awsDeploy.serverless.service.provider.shouldNotDeploy).to.equal(true); + return expect(awsDeploy.checkIfDeploymentIsNecessary(input)).to.be.fulfilled.then(() => { + expect(normalizeCloudFormationTemplateStub).to.have.been.calledOnce; + expect(globbySyncStub).to.have.been.calledOnce; + expect(readFileStub).to.have.been.calledOnce; + expect(awsDeploy.serverless.cli.log).to.have.been.calledOnce; + expect(normalizeCloudFormationTemplateStub).to.have.been.calledWithExactly( + awsDeploy.serverless.service.provider.compiledCloudFormationTemplate + ); + expect(globbySyncStub).to.have.been.calledWithExactly(['**.zip'], { + cwd: path.join(awsDeploy.serverless.config.servicePath, '.serverless'), + dot: true, + silent: true, }); + expect(readFileStub).to.have.been.calledWith( + path.join('my-service/.serverless/my-service.zip') + ); + expect(awsDeploy.serverless.service.provider.shouldNotDeploy).to.equal(true); + }); }); it('should set a flag if the remote and local hashes are duplicated and equal', () => { globbySyncStub.returns(['func1.zip', 'func2.zip']); - cryptoStub.createHash().update().digest.onCall(0).returns('hash-cf-template'); + cryptoStub + .createHash() + .update() + .digest.onCall(0) + .returns('hash-cf-template'); // happens when package.individually is used - cryptoStub.createHash().update().digest.onCall(1).returns('hash-zip-file-1'); - cryptoStub.createHash().update().digest.onCall(2).returns('hash-zip-file-1'); + cryptoStub + .createHash() + .update() + .digest.onCall(1) + .returns('hash-zip-file-1'); + cryptoStub + .createHash() + .update() + .digest.onCall(2) + .returns('hash-zip-file-1'); const input = [ { Metadata: { filesha256: 'hash-cf-template' } }, @@ -473,31 +471,23 @@ describe('checkForChanges', () => { { Metadata: { filesha256: 'hash-zip-file-1' } }, ]; - return expect(awsDeploy.checkIfDeploymentIsNecessary(input)) - .to.be.fulfilled.then(() => { - expect(normalizeCloudFormationTemplateStub).to.have.been.calledOnce; - expect(globbySyncStub).to.have.been.calledOnce; - expect(readFileStub).to.have.been.calledTwice; - expect(awsDeploy.serverless.cli.log).to.have.been.calledOnce; - expect(normalizeCloudFormationTemplateStub).to.have.been.calledWithExactly( - awsDeploy.serverless.service.provider.compiledCloudFormationTemplate - ); - expect(globbySyncStub).to.have.been.calledWithExactly( - ['**.zip'], - { - cwd: path.join(awsDeploy.serverless.config.servicePath, '.serverless'), - dot: true, - silent: true, - } - ); - expect(readFileStub).to.have.been.calledWith( - path.join('my-service/.serverless/func1.zip') - ); - expect(readFileStub).to.have.been.calledWith( - path.join('my-service/.serverless/func2.zip') - ); - expect(awsDeploy.serverless.service.provider.shouldNotDeploy).to.equal(true); + return expect(awsDeploy.checkIfDeploymentIsNecessary(input)).to.be.fulfilled.then(() => { + expect(normalizeCloudFormationTemplateStub).to.have.been.calledOnce; + expect(globbySyncStub).to.have.been.calledOnce; + expect(readFileStub).to.have.been.calledTwice; + expect(awsDeploy.serverless.cli.log).to.have.been.calledOnce; + expect(normalizeCloudFormationTemplateStub).to.have.been.calledWithExactly( + awsDeploy.serverless.service.provider.compiledCloudFormationTemplate + ); + expect(globbySyncStub).to.have.been.calledWithExactly(['**.zip'], { + cwd: path.join(awsDeploy.serverless.config.servicePath, '.serverless'), + dot: true, + silent: true, }); + expect(readFileStub).to.have.been.calledWith(path.join('my-service/.serverless/func1.zip')); + expect(readFileStub).to.have.been.calledWith(path.join('my-service/.serverless/func2.zip')); + expect(awsDeploy.serverless.service.provider.shouldNotDeploy).to.equal(true); + }); }); }); @@ -508,18 +498,15 @@ describe('checkForChanges', () => { const serviceName = 'my-service'; const region = 'us-east-1'; let describeSubscriptionFiltersResponse = {}; - let sandbox; beforeEach(() => { - sandbox = sinon.sandbox.create(); - CloudWatchLogsStub = class { constructor() { - this.deleteSubscriptionFilter = deleteSubscriptionFilterStub = sinon.spy(() => ({ + this.deleteSubscriptionFilter = deleteSubscriptionFilterStub = sandbox.spy(() => ({ promise: () => BbPromise.resolve(), })); - this.describeSubscriptionFilters = sinon.spy(() => ({ + this.describeSubscriptionFilters = sandbox.spy(() => ({ promise: () => BbPromise.resolve(describeSubscriptionFiltersResponse), })); } @@ -527,11 +514,9 @@ describe('checkForChanges', () => { provider.sdk.CloudWatchLogs = CloudWatchLogsStub; - sandbox.stub(provider, 'getAccountId') - .returns(BbPromise.resolve(accountId)); + sandbox.stub(provider, 'getAccountId').returns(BbPromise.resolve(accountId)); - sandbox.stub(awsDeploy.serverless.service, 'getServiceName') - .returns(serviceName); + sandbox.stub(awsDeploy.serverless.service, 'getServiceName').returns(serviceName); sandbox.stub(awsDeploy, 'getMostRecentObjects').resolves(); sandbox.stub(awsDeploy, 'getObjectMetadata').resolves(); @@ -545,16 +530,17 @@ describe('checkForChanges', () => { it('should not call checkLogGroup if deployment is not required', () => { awsDeploy.checkIfDeploymentIsNecessary.restore(); - sandbox.stub(awsDeploy, 'checkIfDeploymentIsNecessary', () => new Promise((resolve) => { - awsDeploy.serverless.service.provider.shouldNotDeploy = true; - resolve(); - })); + sandbox.stub(awsDeploy, 'checkIfDeploymentIsNecessary').callsFake( + () => + new Promise(resolve => { + awsDeploy.serverless.service.provider.shouldNotDeploy = true; + resolve(); + }) + ); - const spy = sinon.spy(awsDeploy, 'checkLogGroupSubscriptionFilterResourceLimitExceeded'); + const spy = sandbox.spy(awsDeploy, 'checkLogGroupSubscriptionFilterResourceLimitExceeded'); - return awsDeploy.checkForChanges().then(() => { - expect(spy).to.not.have.been.called; - }); + return awsDeploy.checkForChanges().then(() => expect(spy).to.not.have.been.called); }); it('should work normally when there are functions without events', () => { @@ -562,19 +548,17 @@ describe('checkForChanges', () => { first: {}, }; - expect(awsDeploy.checkForChanges()).to.be.fulfilled; + return expect(awsDeploy.checkForChanges()).to.be.fulfilled; }); it('should work normally when there are functions events that are not cloudWwatchLog', () => { awsDeploy.serverless.service.functions = { first: { - events: [ - { dummyEvent: 'test' }, - ], + events: [{ dummyEvent: 'test' }], }, }; - expect(awsDeploy.checkForChanges()).to.be.fulfilled; + return expect(awsDeploy.checkForChanges()).to.be.fulfilled; }); describe('option to force update is set', () => { @@ -589,9 +573,7 @@ describe('checkForChanges', () => { it('should not call delete if there are no subscriptionFilters', () => { awsDeploy.serverless.service.functions = { first: { - events: [ - { cloudwatchLog: '/aws/lambda/hello1' }, - ], + events: [{ cloudwatchLog: '/aws/lambda/hello1' }], }, }; @@ -599,57 +581,51 @@ describe('checkForChanges', () => { subscriptionFilters: [], }; - return awsDeploy.checkForChanges().then(() => { - expect(deleteSubscriptionFilterStub).to.not.have.been.called; - }); + return awsDeploy + .checkForChanges() + .then(() => expect(deleteSubscriptionFilterStub).to.not.have.been.called); }); it('should not call delete if there is a subFilter and the ARNs are the same', () => { awsDeploy.serverless.service.functions = { first: { - events: [ - { cloudwatchLog: '/aws/lambda/hello1' }, - ], + events: [{ cloudwatchLog: '/aws/lambda/hello1' }], }, }; describeSubscriptionFiltersResponse = { subscriptionFilters: [ { - destinationArn: - `arn:aws:lambda:${region}:${accountId}:function:${serviceName}-dev-first`, + destinationArn: `arn:aws:lambda:${region}:${accountId}:function:${serviceName}-dev-first`, filterName: 'dummy-filter', }, ], }; - return awsDeploy.checkForChanges().then(() => { - expect(deleteSubscriptionFilterStub).to.not.have.been.called; - }); + return awsDeploy + .checkForChanges() + .then(() => expect(deleteSubscriptionFilterStub).to.not.have.been.called); }); it('should call delete if there is a subFilter but the ARNs are not the same', () => { awsDeploy.serverless.service.functions = { first: { - events: [ - { cloudwatchLog: '/aws/lambda/hello1' }, - ], + events: [{ cloudwatchLog: '/aws/lambda/hello1' }], }, }; describeSubscriptionFiltersResponse = { subscriptionFilters: [ { - destinationArn: - `arn:aws:lambda:${region}:${accountId}:function:${serviceName}-dev-not-first`, + destinationArn: `arn:aws:lambda:${region}:${accountId}:function:${serviceName}-dev-not-first`, filterName: 'dummy-filter', }, ], }; - return awsDeploy.checkForChanges().then(() => { - expect(deleteSubscriptionFilterStub).to.have.been.called; - }); + return awsDeploy + .checkForChanges() + .then(() => expect(deleteSubscriptionFilterStub).to.have.been.called); }); }); }); diff --git a/lib/plugins/aws/deploy/lib/cleanupS3Bucket.js b/lib/plugins/aws/deploy/lib/cleanupS3Bucket.js index 4f2bbfb2d..8de368159 100644 --- a/lib/plugins/aws/deploy/lib/cleanupS3Bucket.js +++ b/lib/plugins/aws/deploy/lib/cleanupS3Bucket.js @@ -12,13 +12,12 @@ module.exports = { const stage = this.provider.getStage(); const prefix = this.provider.getDeploymentPrefix(); - return this.provider.request('S3', - 'listObjectsV2', - { + return this.provider + .request('S3', 'listObjectsV2', { Bucket: this.bucketName, Prefix: `${prefix}/${service}/${stage}`, }) - .then((response) => { + .then(response => { const stacks = findAndGroupDeployments(response, prefix, service, stage); const stacksToKeep = _.takeRight(stacks, stacksToKeepCount); const stacksToRemove = _.pullAllWith(stacks, stacksToKeep, _.isEqual); @@ -36,12 +35,10 @@ module.exports = { if (objectsToRemove && objectsToRemove.length) { this.serverless.cli.log('Removing old service artifacts from S3...'); - return this.provider.request('S3', - 'deleteObjects', - { - Bucket: this.bucketName, - Delete: { Objects: objectsToRemove }, - }); + return this.provider.request('S3', 'deleteObjects', { + Bucket: this.bucketName, + Delete: { Objects: objectsToRemove }, + }); } return BbPromise.resolve(); diff --git a/lib/plugins/aws/deploy/lib/cleanupS3Bucket.test.js b/lib/plugins/aws/deploy/lib/cleanupS3Bucket.test.js index 84447eedc..abd44b8ad 100644 --- a/lib/plugins/aws/deploy/lib/cleanupS3Bucket.test.js +++ b/lib/plugins/aws/deploy/lib/cleanupS3Bucket.test.js @@ -42,19 +42,14 @@ describe('cleanupS3Bucket', () => { Contents: [], }; - const listObjectsStub = sinon - .stub(awsDeploy.provider, 'request').resolves(serviceObjects); + const listObjectsStub = sinon.stub(awsDeploy.provider, 'request').resolves(serviceObjects); return awsDeploy.getObjectsToRemove().then(() => { expect(listObjectsStub).to.have.been.calledOnce; - expect(listObjectsStub).to.have.been.calledWithExactly( - 'S3', - 'listObjectsV2', - { - Bucket: awsDeploy.bucketName, - Prefix: `${s3Key}`, - } - ); + expect(listObjectsStub).to.have.been.calledWithExactly('S3', 'listObjectsV2', { + Bucket: awsDeploy.bucketName, + Prefix: `${s3Key}`, + }); awsDeploy.provider.request.restore(); }); }); @@ -77,43 +72,38 @@ describe('cleanupS3Bucket', () => { ], }; - const listObjectsStub = sinon - .stub(awsDeploy.provider, 'request').resolves(serviceObjects); + const listObjectsStub = sinon.stub(awsDeploy.provider, 'request').resolves(serviceObjects); - return awsDeploy.getObjectsToRemove().then((objectsToRemove) => { - expect(objectsToRemove).to.not - .include( - { Key: `${s3Key}${s3Key}/141321321541-2016-08-18T11:23:02/artifact.zip` }); - expect(objectsToRemove).to.not - .include( - { Key: `${s3Key}${s3Key}/141321321541-2016-08-18T11:23:02/cloudformation.json` }); - expect(objectsToRemove).to.not - .include( - { Key: `${s3Key}${s3Key}/142003031341-2016-08-18T12:46:04/artifact.zip` }); - expect(objectsToRemove).to.not - .include( - { Key: `${s3Key}${s3Key}/142003031341-2016-08-18T12:46:04/cloudformation.json` }); - expect(objectsToRemove).to.not - .include( - { Key: `${s3Key}${s3Key}/151224711231-2016-08-18T15:42:00/artifact.zip` }); - expect(objectsToRemove).to.not - .include( - { Key: `${s3Key}${s3Key}/151224711231-2016-08-18T15:42:00/cloudformation.json` }); - expect(objectsToRemove).to.not - .include( - { Key: `${s3Key}${s3Key}/903940390431-2016-08-18T23:42:08/artifact.zip` }); - expect(objectsToRemove).to.not - .include( - { Key: `${s3Key}${s3Key}/903940390431-2016-08-18T23:42:08/cloudformation.json` }); + return awsDeploy.getObjectsToRemove().then(objectsToRemove => { + expect(objectsToRemove).to.not.include({ + Key: `${s3Key}${s3Key}/141321321541-2016-08-18T11:23:02/artifact.zip`, + }); + expect(objectsToRemove).to.not.include({ + Key: `${s3Key}${s3Key}/141321321541-2016-08-18T11:23:02/cloudformation.json`, + }); + expect(objectsToRemove).to.not.include({ + Key: `${s3Key}${s3Key}/142003031341-2016-08-18T12:46:04/artifact.zip`, + }); + expect(objectsToRemove).to.not.include({ + Key: `${s3Key}${s3Key}/142003031341-2016-08-18T12:46:04/cloudformation.json`, + }); + expect(objectsToRemove).to.not.include({ + Key: `${s3Key}${s3Key}/151224711231-2016-08-18T15:42:00/artifact.zip`, + }); + expect(objectsToRemove).to.not.include({ + Key: `${s3Key}${s3Key}/151224711231-2016-08-18T15:42:00/cloudformation.json`, + }); + expect(objectsToRemove).to.not.include({ + Key: `${s3Key}${s3Key}/903940390431-2016-08-18T23:42:08/artifact.zip`, + }); + expect(objectsToRemove).to.not.include({ + Key: `${s3Key}${s3Key}/903940390431-2016-08-18T23:42:08/cloudformation.json`, + }); expect(listObjectsStub.calledOnce).to.be.equal(true); - expect(listObjectsStub).to.have.been.calledWithExactly( - 'S3', - 'listObjectsV2', - { - Bucket: awsDeploy.bucketName, - Prefix: `${s3Key}`, - } - ); + expect(listObjectsStub).to.have.been.calledWithExactly('S3', 'listObjectsV2', { + Bucket: awsDeploy.bucketName, + Prefix: `${s3Key}`, + }); awsDeploy.provider.request.restore(); }); }); @@ -130,20 +120,15 @@ describe('cleanupS3Bucket', () => { ], }; - const listObjectsStub = sinon - .stub(awsDeploy.provider, 'request').resolves(serviceObjects); + const listObjectsStub = sinon.stub(awsDeploy.provider, 'request').resolves(serviceObjects); - return awsDeploy.getObjectsToRemove().then((objectsToRemove) => { + return awsDeploy.getObjectsToRemove().then(objectsToRemove => { expect(objectsToRemove.length).to.equal(0); expect(listObjectsStub.calledOnce).to.be.equal(true); - expect(listObjectsStub).to.have.been.calledWithExactly( - 'S3', - 'listObjectsV2', - { - Bucket: awsDeploy.bucketName, - Prefix: `${s3Key}`, - } - ); + expect(listObjectsStub).to.have.been.calledWithExactly('S3', 'listObjectsV2', { + Bucket: awsDeploy.bucketName, + Prefix: `${s3Key}`, + }); awsDeploy.provider.request.restore(); }); }); @@ -162,20 +147,15 @@ describe('cleanupS3Bucket', () => { ], }; - const listObjectsStub = sinon - .stub(awsDeploy.provider, 'request').resolves(serviceObjects); + const listObjectsStub = sinon.stub(awsDeploy.provider, 'request').resolves(serviceObjects); - return awsDeploy.getObjectsToRemove().then((objectsToRemove) => { + return awsDeploy.getObjectsToRemove().then(objectsToRemove => { expect(objectsToRemove).to.have.lengthOf(0); expect(listObjectsStub).to.have.been.calledOnce; - expect(listObjectsStub).to.have.been.calledWithExactly( - 'S3', - 'listObjectsV2', - { - Bucket: awsDeploy.bucketName, - Prefix: `${s3Key}`, - } - ); + expect(listObjectsStub).to.have.been.calledWithExactly('S3', 'listObjectsV2', { + Bucket: awsDeploy.bucketName, + Prefix: `${s3Key}`, + }); awsDeploy.provider.request.restore(); }); }); @@ -185,16 +165,14 @@ describe('cleanupS3Bucket', () => { let deleteObjectsStub; beforeEach(() => { - deleteObjectsStub = sinon - .stub(awsDeploy.provider, 'request').resolves(); + deleteObjectsStub = sinon.stub(awsDeploy.provider, 'request').resolves(); }); - it('should resolve if no service objects are found in the S3 bucket', () => awsDeploy - .removeObjects().then(() => { + it('should resolve if no service objects are found in the S3 bucket', () => + awsDeploy.removeObjects().then(() => { expect(deleteObjectsStub.calledOnce).to.be.equal(false); awsDeploy.provider.request.restore(); - }) - ); + })); it('should remove all old service files from the S3 bucket if available', () => { const objectsToRemove = [ @@ -206,16 +184,12 @@ describe('cleanupS3Bucket', () => { return awsDeploy.removeObjects(objectsToRemove).then(() => { expect(deleteObjectsStub).to.have.been.calledOnce; - expect(deleteObjectsStub).to.have.been.calledWithExactly( - 'S3', - 'deleteObjects', - { - Bucket: awsDeploy.bucketName, - Delete: { - Objects: objectsToRemove, - }, - } - ); + expect(deleteObjectsStub).to.have.been.calledWithExactly('S3', 'deleteObjects', { + Bucket: awsDeploy.bucketName, + Delete: { + Objects: objectsToRemove, + }, + }); awsDeploy.provider.request.restore(); }); }); @@ -223,15 +197,12 @@ describe('cleanupS3Bucket', () => { describe('#cleanupS3Bucket()', () => { it('should run promise chain in order', () => { - const getObjectsToRemoveStub = sinon - .stub(awsDeploy, 'getObjectsToRemove').resolves(); - const removeObjectsStub = sinon - .stub(awsDeploy, 'removeObjects').resolves(); + const getObjectsToRemoveStub = sinon.stub(awsDeploy, 'getObjectsToRemove').resolves(); + const removeObjectsStub = sinon.stub(awsDeploy, 'removeObjects').resolves(); return awsDeploy.cleanupS3Bucket().then(() => { expect(getObjectsToRemoveStub.calledOnce).to.be.equal(true); - expect(removeObjectsStub.calledAfter(getObjectsToRemoveStub)) - .to.be.equal(true); + expect(removeObjectsStub.calledAfter(getObjectsToRemoveStub)).to.be.equal(true); awsDeploy.getObjectsToRemove.restore(); awsDeploy.removeObjects.restore(); diff --git a/lib/plugins/aws/deploy/lib/createStack.js b/lib/plugins/aws/deploy/lib/createStack.js index c47b83455..b35865aab 100644 --- a/lib/plugins/aws/deploy/lib/createStack.js +++ b/lib/plugins/aws/deploy/lib/createStack.js @@ -19,17 +19,16 @@ module.exports = { const params = { StackName: stackName, OnFailure: 'DELETE', - Capabilities: [ - 'CAPABILITY_IAM', - 'CAPABILITY_NAMED_IAM', - ], + Capabilities: ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'], Parameters: [], TemplateBody: JSON.stringify(this.serverless.service.provider.coreCloudFormationTemplate), - Tags: Object.keys(stackTags).map((key) => ({ Key: key, Value: stackTags[key] })), + Tags: Object.keys(stackTags).map(key => ({ Key: key, Value: stackTags[key] })), }; - if (this.serverless.service.provider.compiledCloudFormationTemplate && - this.serverless.service.provider.compiledCloudFormationTemplate.Transform) { + if ( + this.serverless.service.provider.compiledCloudFormationTemplate && + this.serverless.service.provider.compiledCloudFormationTemplate.Transform + ) { params.Capabilities.push('CAPABILITY_AUTO_EXPAND'); } @@ -41,11 +40,9 @@ module.exports = { params.NotificationARNs = this.serverless.service.provider.notificationArns; } - return this.provider.request( - 'CloudFormation', - 'createStack', - params - ).then((cfData) => this.monitorStack('create', cfData)); + return this.provider + .request('CloudFormation', 'createStack', params) + .then(cfData => this.monitorStack('create', cfData)); }, createStack() { @@ -55,41 +52,41 @@ module.exports = { `The stack service name "${stackName}" is not valid. `, 'A service name should only contain alphanumeric', ' (case sensitive) and hyphens. It should start', - ' with an alphabetic character and shouldn\'t', + " with an alphabetic character and shouldn't", ' exceed 128 characters.', ].join(''); throw new this.serverless.classes.Error(errorMessage); } return BbPromise.bind(this) - .then(() => this.provider.request('CloudFormation', - 'describeStacks', - { StackName: stackName } + .then(() => + this.provider + .request('CloudFormation', 'describeStacks', { StackName: stackName }) + .then(data => { + const shouldCheckStackOutput = + // check stack output only if acceleration is requested + this.provider.isS3TransferAccelerationEnabled() && + // custom deployment bucket won't generate any output (no check) + !this.serverless.service.provider.deploymentBucket; + if (shouldCheckStackOutput) { + const isAlreadyAccelerated = !!_.find(data.Stacks[0].Outputs, { + OutputKey: 'ServerlessDeploymentBucketAccelerated', + }); + if (!isAlreadyAccelerated) { + this.serverless.cli.log('Not using S3 Transfer Acceleration (1st deploy)'); + this.provider.disableTransferAccelerationForCurrentDeploy(); + } + } + return BbPromise.resolve('alreadyCreated'); + }) ) - .then((data) => { - const shouldCheckStackOutput = - // check stack output only if acceleration is requested - this.provider.isS3TransferAccelerationEnabled() && - // custom deployment bucket won't generate any output (no check) - !this.serverless.service.provider.deploymentBucket; - if (shouldCheckStackOutput) { - const isAlreadyAccelerated = !!_.find(data.Stacks[0].Outputs, - { OutputKey: 'ServerlessDeploymentBucketAccelerated' }); - if (!isAlreadyAccelerated) { - this.serverless.cli.log('Not using S3 Transfer Acceleration (1st deploy)'); - this.provider.disableTransferAccelerationForCurrentDeploy(); - } - } - return BbPromise.resolve('alreadyCreated'); - })) - .catch((e) => { + .catch(e => { if (e.message.indexOf('does not exist') > -1) { if (this.serverless.service.provider.deploymentBucket) { this.createLater = true; return BbPromise.resolve(); } - return BbPromise.bind(this) - .then(this.create); + return BbPromise.bind(this).then(this.create); } throw new this.serverless.classes.Error(e); }); diff --git a/lib/plugins/aws/deploy/lib/createStack.test.js b/lib/plugins/aws/deploy/lib/createStack.test.js index 467061309..a959fdc41 100644 --- a/lib/plugins/aws/deploy/lib/createStack.test.js +++ b/lib/plugins/aws/deploy/lib/createStack.test.js @@ -1,17 +1,16 @@ 'use strict'; const expect = require('chai').expect; -const sinon = require('sinon'); +const sandbox = require('sinon'); const path = require('path'); const AwsProvider = require('../../provider/awsProvider'); const AwsDeploy = require('../index'); const Serverless = require('../../../../Serverless'); -const testUtils = require('../../../../../tests/utils'); +const { getTmpDirPath } = require('../../../../../tests/utils/fs'); describe('createStack', () => { let awsDeploy; - let sandbox; - const tmpDirPath = testUtils.getTmpDirPath(); + const tmpDirPath = getTmpDirPath(); const serverlessYmlPath = path.join(tmpDirPath, 'serverless.yml'); const serverlessYml = { @@ -25,7 +24,6 @@ describe('createStack', () => { }; beforeEach(() => { - sandbox = sinon.sandbox.create(); const serverless = new Serverless(); serverless.setProvider('aws', new AwsProvider(serverless, {})); serverless.utils.writeFileSync(serverlessYmlPath, serverlessYml); @@ -35,7 +33,7 @@ describe('createStack', () => { region: 'us-east-1', }; awsDeploy = new AwsDeploy(serverless, options); - awsDeploy.serverless.service.service = `service-${(new Date()).getTime().toString()}`; + awsDeploy.serverless.service.service = `service-${new Date().getTime().toString()}`; awsDeploy.serverless.cli = new serverless.classes.CLI(); }); @@ -47,43 +45,40 @@ describe('createStack', () => { it('should include custom stack tags', () => { awsDeploy.serverless.service.provider.stackTags = { STAGE: 'overridden', tag1: 'value1' }; - const createStackStub = sandbox - .stub(awsDeploy.provider, 'request').resolves(); + const createStackStub = sandbox.stub(awsDeploy.provider, 'request').resolves(); sandbox.stub(awsDeploy, 'monitorStack').resolves(); return awsDeploy.create().then(() => { - expect(createStackStub.args[0][2].Tags) - .to.deep.equal([ - { Key: 'STAGE', Value: 'overridden' }, - { Key: 'tag1', Value: 'value1' }, - ]); + expect(createStackStub.args[0][2].Tags).to.deep.equal([ + { Key: 'STAGE', Value: 'overridden' }, + { Key: 'tag1', Value: 'value1' }, + ]); }); }); it('should add CAPABILITY_AUTO_EXPAND if a Transform directive is specified', () => { - awsDeploy.serverless.service.provider - .compiledCloudFormationTemplate = { Transform: 'MyMacro' }; + awsDeploy.serverless.service.provider.compiledCloudFormationTemplate = { + Transform: 'MyMacro', + }; - const createStackStub = sandbox - .stub(awsDeploy.provider, 'request').resolves(); + const createStackStub = sandbox.stub(awsDeploy.provider, 'request').resolves(); sandbox.stub(awsDeploy, 'monitorStack').resolves(); return awsDeploy.create().then(() => { - expect(createStackStub.args[0][2].Capabilities) - .to.contain('CAPABILITY_AUTO_EXPAND'); + expect(createStackStub.args[0][2].Capabilities).to.contain('CAPABILITY_AUTO_EXPAND'); }); }); it('should use CloudFormation service role ARN if it is specified', () => { awsDeploy.serverless.service.provider.cfnRole = 'arn:aws:iam::123456789012:role/myrole'; - const createStackStub = sandbox - .stub(awsDeploy.provider, 'request').resolves(); + const createStackStub = sandbox.stub(awsDeploy.provider, 'request').resolves(); sandbox.stub(awsDeploy, 'monitorStack').resolves(); return awsDeploy.create().then(() => { - expect(createStackStub.args[0][2].RoleARN) - .to.equal('arn:aws:iam::123456789012:role/myrole'); + expect(createStackStub.args[0][2].RoleARN).to.equal( + 'arn:aws:iam::123456789012:role/myrole' + ); }); }); @@ -91,13 +86,11 @@ describe('createStack', () => { const mytopicArn = 'arn:aws:sns::123456789012:mytopic'; awsDeploy.serverless.service.provider.notificationArns = [mytopicArn]; - const createStackStub = sinon - .stub(awsDeploy.provider, 'request').resolves(); - sinon.stub(awsDeploy, 'monitorStack').resolves(); + const createStackStub = sandbox.stub(awsDeploy.provider, 'request').resolves(); + sandbox.stub(awsDeploy, 'monitorStack').resolves(); return awsDeploy.create().then(() => { - expect(createStackStub.args[0][2].NotificationARNs) - .to.deep.equal([mytopicArn]); + expect(createStackStub.args[0][2].NotificationARNs).to.deep.equal([mytopicArn]); awsDeploy.provider.request.restore(); awsDeploy.monitorStack.restore(); }); @@ -106,8 +99,7 @@ describe('createStack', () => { describe('#createStack()', () => { it('should resolve if stack already created', () => { - const createStub = sandbox - .stub(awsDeploy, 'create').resolves(); + const createStub = sandbox.stub(awsDeploy, 'create').resolves(); sandbox.stub(awsDeploy.provider, 'request').resolves(); @@ -121,8 +113,10 @@ describe('createStack', () => { sandbox.stub(awsDeploy.provider, 'request').resolves(); awsDeploy.serverless.service.service = 'service-name'.repeat(100); - return expect(awsDeploy.createStack.bind(awsDeploy)) - .to.throw(awsDeploy.serverless.classes.Error, /not valid/); + return expect(awsDeploy.createStack.bind(awsDeploy)).to.throw( + awsDeploy.serverless.classes.Error, + /not valid/ + ); }); it('should set the createLater flag and resolve if deployment bucket is provided', () => { @@ -141,10 +135,9 @@ describe('createStack', () => { sandbox.stub(awsDeploy.provider, 'request').rejects(errorMock); - const createStub = sandbox - .stub(awsDeploy, 'create').resolves(); + const createStub = sandbox.stub(awsDeploy, 'create').resolves(); - return awsDeploy.createStack().catch((e) => { + return awsDeploy.createStack().catch(e => { expect(createStub.called).to.be.equal(false); expect(e.name).to.be.equal('ServerlessError'); expect(e.message).to.be.equal(errorMock); @@ -158,8 +151,7 @@ describe('createStack', () => { sandbox.stub(awsDeploy.provider, 'request').rejects(errorMock); - const createStub = sandbox - .stub(awsDeploy, 'create').resolves(); + const createStub = sandbox.stub(awsDeploy, 'create').resolves(); return awsDeploy.createStack().then(() => { expect(createStub.calledOnce).to.be.equal(true); @@ -168,8 +160,8 @@ describe('createStack', () => { it('should disable S3 Transfer Acceleration if missing Output', () => { const disableTransferAccelerationStub = sandbox - .stub(awsDeploy.provider, - 'disableTransferAccelerationForCurrentDeploy').resolves(); + .stub(awsDeploy.provider, 'disableTransferAccelerationForCurrentDeploy') + .resolves(); const describeStacksOutput = { Stacks: [ @@ -189,8 +181,8 @@ describe('createStack', () => { it('should not disable S3 Transfer Acceleration if custom bucket is used', () => { const disableTransferAccelerationStub = sandbox - .stub(awsDeploy.provider, - 'disableTransferAccelerationForCurrentDeploy').resolves(); + .stub(awsDeploy.provider, 'disableTransferAccelerationForCurrentDeploy') + .resolves(); const describeStacksOutput = { Stacks: [ diff --git a/lib/plugins/aws/deploy/lib/existsDeploymentBucket.js b/lib/plugins/aws/deploy/lib/existsDeploymentBucket.js index af2e96e06..32929bac8 100644 --- a/lib/plugins/aws/deploy/lib/existsDeploymentBucket.js +++ b/lib/plugins/aws/deploy/lib/existsDeploymentBucket.js @@ -5,12 +5,11 @@ const BbPromise = require('bluebird'); module.exports = { existsDeploymentBucket(bucketName) { return BbPromise.resolve() - .then(() => this.provider.request('S3', - 'getBucketLocation', - { + .then(() => + this.provider.request('S3', 'getBucketLocation', { Bucket: bucketName, - } - )) + }) + ) .then(resultParam => { const result = resultParam; if (result.LocationConstraint === '') result.LocationConstraint = 'us-east-1'; diff --git a/lib/plugins/aws/deploy/lib/existsDeploymentBucket.test.js b/lib/plugins/aws/deploy/lib/existsDeploymentBucket.test.js index a41192971..ce7a00c9e 100644 --- a/lib/plugins/aws/deploy/lib/existsDeploymentBucket.test.js +++ b/lib/plugins/aws/deploy/lib/existsDeploymentBucket.test.js @@ -32,12 +32,11 @@ describe('#existsDeploymentBucket()', () => { LocationConstraint: awsPlugin.provider.options.region, }); - return expect(awsPlugin.existsDeploymentBucket(bucketName)).to.be.fulfilled - .then(() => { - expect(awsPluginStub.args[0][0]).to.equal('S3'); - expect(awsPluginStub.args[0][1]).to.equal('getBucketLocation'); - expect(awsPluginStub.args[0][2].Bucket).to.equal(bucketName); - }); + return expect(awsPlugin.existsDeploymentBucket(bucketName)).to.be.fulfilled.then(() => { + expect(awsPluginStub.args[0][0]).to.equal('S3'); + expect(awsPluginStub.args[0][1]).to.equal('getBucketLocation'); + expect(awsPluginStub.args[0][2].Bucket).to.equal(bucketName); + }); }); it('should reject an S3 bucket that does not exist', () => { @@ -45,14 +44,13 @@ describe('#existsDeploymentBucket()', () => { const errorObj = { message: 'Access Denied' }; sinon.stub(awsPlugin.provider, 'request').throws(errorObj); - return expect(awsPlugin.existsDeploymentBucket(bucketName)).to.be.rejected - .then(err => { - expect(awsPluginStub.args[0][0]).to.equal('S3'); - expect(awsPluginStub.args[0][1]).to.equal('getBucketLocation'); - expect(awsPluginStub.args[0][2].Bucket).to.equal(bucketName); - expect(err.message).to.contain(errorObj.message); - expect(err.message).to.contain('Could not locate deployment bucket'); - }); + return expect(awsPlugin.existsDeploymentBucket(bucketName)).to.be.rejected.then(err => { + expect(awsPluginStub.args[0][0]).to.equal('S3'); + expect(awsPluginStub.args[0][1]).to.equal('getBucketLocation'); + expect(awsPluginStub.args[0][2].Bucket).to.equal(bucketName); + expect(err.message).to.contain(errorObj.message); + expect(err.message).to.contain('Could not locate deployment bucket'); + }); }); it('should reject an S3 bucket in the wrong region', () => { @@ -62,36 +60,32 @@ describe('#existsDeploymentBucket()', () => { LocationConstraint: 'us-west-1', }); - return expect(awsPlugin.existsDeploymentBucket(bucketName)).to.be.rejected - .then(err => { - expect(awsPluginStub.args[0][0]).to.equal('S3'); - expect(awsPluginStub.args[0][1]).to.equal('getBucketLocation'); - expect(awsPluginStub.args[0][2].Bucket).to.equal(bucketName); - expect(err.message).to.contain('not in the same region'); - }); + return expect(awsPlugin.existsDeploymentBucket(bucketName)).to.be.rejected.then(err => { + expect(awsPluginStub.args[0][0]).to.equal('S3'); + expect(awsPluginStub.args[0][1]).to.equal('getBucketLocation'); + expect(awsPluginStub.args[0][2].Bucket).to.equal(bucketName); + expect(err.message).to.contain('not in the same region'); + }); }); - [ - { region: 'eu-west-1', response: 'EU' }, - { region: 'us-east-1', response: '' }, - ].forEach((value) => { - it(`should handle inconsistent getBucketLocation responses for ${value.region} region`, () => { - const bucketName = 'com.serverless.deploys'; + [{ region: 'eu-west-1', response: 'EU' }, { region: 'us-east-1', response: '' }].forEach( + value => { + it(`should handle inconsistent getBucketLocation responses for ${value.region} region`, () => { + const bucketName = 'com.serverless.deploys'; - awsPlugin.provider.options.region = value.region; + awsPlugin.provider.options.region = value.region; - sinon - .stub(awsPlugin.provider, 'request').resolves({ + sinon.stub(awsPlugin.provider, 'request').resolves({ LocationConstraint: value.response, }); - awsPlugin.serverless.service.provider.deploymentBucket = bucketName; - return expect(awsPlugin.existsDeploymentBucket(bucketName)).to.be.fulfilled - .then(() => { + awsPlugin.serverless.service.provider.deploymentBucket = bucketName; + return expect(awsPlugin.existsDeploymentBucket(bucketName)).to.be.fulfilled.then(() => { expect(awsPluginStub.args[0][0]).to.equal('S3'); expect(awsPluginStub.args[0][1]).to.equal('getBucketLocation'); expect(awsPluginStub.args[0][2].Bucket).to.equal(bucketName); }); - }); - }); + }); + } + ); }); diff --git a/lib/plugins/aws/deploy/lib/extendedValidate.js b/lib/plugins/aws/deploy/lib/extendedValidate.js index 2a1bf15e3..1f2f99d4c 100644 --- a/lib/plugins/aws/deploy/lib/extendedValidate.js +++ b/lib/plugins/aws/deploy/lib/extendedValidate.js @@ -9,15 +9,17 @@ module.exports = { extendedValidate() { // Restore state const serviceStateFileName = this.provider.naming.getServiceStateFileName(); - const serviceStateFilePath = path.join(this.serverless.config.servicePath, + const serviceStateFilePath = path.join( + this.serverless.config.servicePath, '.serverless', - serviceStateFileName); + serviceStateFileName + ); if (!this.serverless.utils.fileExistsSync(serviceStateFilePath)) { - throw new this.serverless.classes - .Error(`No ${serviceStateFileName} file found in the package path you provided.`); + throw new this.serverless.classes.Error( + `No ${serviceStateFileName} file found in the package path you provided.` + ); } - const state = this.serverless - .utils.readFileSync(serviceStateFilePath); + const state = this.serverless.utils.readFileSync(serviceStateFilePath); const selfReferences = findReferences(state.service, '${self:}'); _.forEach(selfReferences, ref => _.set(state.service, ref, this.serverless.service)); @@ -26,8 +28,11 @@ module.exports = { this.serverless.service.package.artifactDirectoryName = state.package.artifactDirectoryName; // only restore the default artifact path if the user is not using a custom path if (!_.isEmpty(state.package.artifact) && this.serverless.service.artifact) { - this.serverless.service.package.artifact = path - .join(this.serverless.config.servicePath, '.serverless', state.package.artifact); + this.serverless.service.package.artifact = path.join( + this.serverless.config.servicePath, + '.serverless', + state.package.artifact + ); } // Check function's attached to API Gateway timeout @@ -41,7 +46,7 @@ module.exports = { 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 ', + "seconds, however, it's attached to API Gateway so it's automatically ", 'limited to 30 seconds.', ].join(''); @@ -52,8 +57,10 @@ module.exports = { }); } - if (!_.isEmpty(this.serverless.service.functions) && - this.serverless.service.package.individually) { + if ( + !_.isEmpty(this.serverless.service.functions) && + this.serverless.service.package.individually + ) { // artifact file validation (multiple function artifacts) this.serverless.service.getAllFunctions().forEach(functionName => { let artifactFileName = this.provider.naming.getFunctionArtifactName(functionName); @@ -67,8 +74,9 @@ module.exports = { } if (!this.serverless.utils.fileExistsSync(artifactFilePath)) { - throw new this.serverless.classes - .Error(`No ${artifactFileName} file found in the package path you provided.`); + throw new this.serverless.classes.Error( + `No ${artifactFileName} file found in the package path you provided.` + ); } }); } else if (!_.isEmpty(this.serverless.service.functions)) { @@ -82,8 +90,9 @@ module.exports = { artifactFilePath = path.join(this.packagePath, artifactFileName); } if (!this.serverless.utils.fileExistsSync(artifactFilePath)) { - throw new this.serverless.classes - .Error(`No ${artifactFileName} file found in the package path you provided.`); + throw new this.serverless.classes.Error( + `No ${artifactFileName} file found in the package path you provided.` + ); } } diff --git a/lib/plugins/aws/deploy/lib/extendedValidate.test.js b/lib/plugins/aws/deploy/lib/extendedValidate.test.js index 7cbabdc5f..3a659602b 100644 --- a/lib/plugins/aws/deploy/lib/extendedValidate.test.js +++ b/lib/plugins/aws/deploy/lib/extendedValidate.test.js @@ -6,7 +6,7 @@ const path = require('path'); const AwsProvider = require('../../provider/awsProvider'); const AwsDeploy = require('../index'); const Serverless = require('../../../../Serverless'); -const testUtils = require('../../../../../tests/utils'); +const { getTmpDirPath } = require('../../../../../tests/utils/fs'); chai.use(require('sinon-chai')); @@ -14,7 +14,7 @@ const expect = chai.expect; describe('extendedValidate', () => { let awsDeploy; - const tmpDirPath = testUtils.getTmpDirPath(); + const tmpDirPath = getTmpDirPath(); const serverlessYmlPath = path.join(tmpDirPath, 'serverless.yml'); const serverlessYml = { @@ -45,7 +45,7 @@ describe('extendedValidate', () => { serverless.utils.writeFileSync(serverlessYmlPath, serverlessYml); serverless.config.servicePath = tmpDirPath; awsDeploy = new AwsDeploy(serverless, options); - awsDeploy.serverless.service.service = `service-${(new Date()).getTime().toString()}`; + awsDeploy.serverless.service.service = `service-${new Date().getTime().toString()}`; awsDeploy.serverless.cli = { log: sinon.spy(), }; @@ -56,10 +56,8 @@ describe('extendedValidate', () => { let readFileSyncStub; beforeEach(() => { - fileExistsSyncStub = sinon - .stub(awsDeploy.serverless.utils, 'fileExistsSync'); - readFileSyncStub = sinon - .stub(awsDeploy.serverless.utils, 'readFileSync'); + fileExistsSyncStub = sinon.stub(awsDeploy.serverless.utils, 'fileExistsSync'); + readFileSyncStub = sinon.stub(awsDeploy.serverless.utils, 'readFileSync'); awsDeploy.serverless.service.package.individually = false; }); @@ -155,16 +153,18 @@ describe('extendedValidate', () => { }); }); - it('should warn if function\'s timeout is greater than 30 and it\'s attached to APIGW', () => { + 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: {}, - }], + events: [ + { + http: {}, + }, + ], }, }; awsDeploy.serverless.service.package.individually = true; @@ -173,8 +173,8 @@ describe('extendedValidate', () => { 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.', + "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/deploy/lib/uploadArtifacts.js b/lib/plugins/aws/deploy/lib/uploadArtifacts.js index e921fce18..57484fc3f 100644 --- a/lib/plugins/aws/deploy/lib/uploadArtifacts.js +++ b/lib/plugins/aws/deploy/lib/uploadArtifacts.js @@ -1,7 +1,5 @@ 'use strict'; -/* eslint-disable no-use-before-define */ - const _ = require('lodash'); const fs = require('fs'); const path = require('path'); @@ -16,7 +14,8 @@ module.exports = { uploadArtifacts() { return BbPromise.bind(this) .then(this.uploadCloudFormationFile) - .then(this.uploadFunctionsAndLayers); + .then(this.uploadFunctionsAndLayers) + .then(this.uploadCustomResources); }, uploadCloudFormationFile() { @@ -46,9 +45,7 @@ module.exports = { params = setServersideEncryptionOptions(params, deploymentBucketObject); } - return this.provider.request('S3', - 'upload', - params); + return this.provider.request('S3', 'upload', params); }, uploadZipFile(artifactFilePath) { @@ -56,7 +53,10 @@ module.exports = { // TODO refactor to be async (use util function to compute checksum async) const data = fs.readFileSync(artifactFilePath); - const fileHash = crypto.createHash('sha256').update(data).digest('base64'); + const fileHash = crypto + .createHash('sha256') + .update(data) + .digest('base64'); let params = { Bucket: this.bucketName, @@ -73,9 +73,7 @@ module.exports = { params = setServersideEncryptionOptions(params, deploymentBucketObject); } - return this.provider.request('S3', - 'upload', - params); + return this.provider.request('S3', 'upload', params); }, uploadFunctionsAndLayers() { @@ -83,15 +81,17 @@ module.exports = { const functionNames = this.serverless.service.getAllFunctions(); let artifactFilePaths = _.uniq( - _.map(functionNames, (name) => { + _.map(functionNames, name => { const functionArtifactFileName = this.provider.naming.getFunctionArtifactName(name); const functionObject = this.serverless.service.getFunction(name); functionObject.package = functionObject.package || {}; - const artifactFilePath = functionObject.package.artifact || - this.serverless.service.package.artifact; + const artifactFilePath = + functionObject.package.artifact || this.serverless.service.package.artifact; - if (!artifactFilePath || - (this.serverless.service.artifact && !functionObject.package.artifact)) { + if ( + !artifactFilePath || + (this.serverless.service.artifact && !functionObject.package.artifact) + ) { if (this.serverless.service.package.individually || functionObject.package.individually) { const artifactFileName = functionArtifactFileName; return path.join(this.packagePath, artifactFileName); @@ -104,25 +104,45 @@ module.exports = { ); const layerNames = this.serverless.service.getAllLayers(); - artifactFilePaths = artifactFilePaths.concat(_.map(layerNames, (name) => { - const layerObject = this.serverless.service.getLayer(name); + artifactFilePaths = artifactFilePaths.concat( + _.map(layerNames, name => { + const layerObject = this.serverless.service.getLayer(name); - if (layerObject.package && layerObject.package.artifact) { - return layerObject.package.artifact; - } + if (layerObject.package && layerObject.package.artifact) { + return layerObject.package.artifact; + } - return path.join(this.packagePath, this.provider.naming.getLayerArtifactName(name)); - })); - - return BbPromise.map(artifactFilePaths, (artifactFilePath) => { - const stats = fs.statSync(artifactFilePath); - const fileName = path.basename(artifactFilePath); - this.serverless.cli.log( - `Uploading service ${fileName} file to S3 (${filesize(stats.size)})...` - ); - return this.uploadZipFile(artifactFilePath); - }, { concurrency: NUM_CONCURRENT_UPLOADS } + return path.join(this.packagePath, this.provider.naming.getLayerArtifactName(name)); + }) ); + + return BbPromise.map( + artifactFilePaths, + artifactFilePath => { + const stats = fs.statSync(artifactFilePath); + const fileName = path.basename(artifactFilePath); + this.serverless.cli.log( + `Uploading service ${fileName} file to S3 (${filesize(stats.size)})...` + ); + return this.uploadZipFile(artifactFilePath); + }, + { concurrency: NUM_CONCURRENT_UPLOADS } + ); + }, + + uploadCustomResources() { + const artifactFilePath = path.join( + this.serverless.config.servicePath, + '.serverless', + `${this.provider.naming.getCustomResourcesArtifactDirectoryName()}.zip` + ); + + if (this.serverless.utils.fileExistsSync(artifactFilePath)) { + this.serverless.cli.log('Uploading custom CloudFormation resources...'); + return this.uploadZipFile(artifactFilePath); + } + + return BbPromise.resolve(); }, }; @@ -137,7 +157,7 @@ function setServersideEncryptionOptions(putParams, deploymentBucketOptions) { const params = putParams; - encryptionFields.forEach((element) => { + encryptionFields.forEach(element => { if (deploymentBucketOptions[element[0]]) { params[element[1]] = deploymentBucketOptions[element[0]]; } diff --git a/lib/plugins/aws/deploy/lib/uploadArtifacts.test.js b/lib/plugins/aws/deploy/lib/uploadArtifacts.test.js index 532a8bac2..3bfe4e94d 100644 --- a/lib/plugins/aws/deploy/lib/uploadArtifacts.test.js +++ b/lib/plugins/aws/deploy/lib/uploadArtifacts.test.js @@ -4,6 +4,7 @@ const sinon = require('sinon'); const fs = require('fs'); +const fse = require('fs-extra'); const path = require('path'); const chai = require('chai'); const proxyquire = require('proxyquire'); @@ -11,7 +12,7 @@ const normalizeFiles = require('../../lib/normalizeFiles'); const AwsProvider = require('../../provider/awsProvider'); const AwsDeploy = require('../index'); const Serverless = require('../../../../Serverless'); -const testUtils = require('../../../../../tests/utils'); +const { getTmpDirPath, createTmpDir } = require('../../../../../tests/utils/fs'); chai.use(require('chai-as-promised')); chai.use(require('sinon-chai')); @@ -44,32 +45,35 @@ describe('uploadArtifacts', () => { }; awsDeploy.serverless.cli = new serverless.classes.CLI(); cryptoStub = { - createHash: function () { return this; }, // eslint-disable-line - update: function () { return this; }, // eslint-disable-line + createHash() { + return this; + }, // eslint-disable-line + update() { + return this; + }, // eslint-disable-line digest: sinon.stub(), }; const uploadArtifacts = proxyquire('./uploadArtifacts.js', { crypto: cryptoStub, }); - Object.assign( - awsDeploy, - uploadArtifacts - ); + Object.assign(awsDeploy, uploadArtifacts); }); describe('#uploadArtifacts()', () => { it('should run promise chain in order', () => { const uploadCloudFormationFileStub = sinon - .stub(awsDeploy, 'uploadCloudFormationFile').resolves(); + .stub(awsDeploy, 'uploadCloudFormationFile') + .resolves(); const uploadFunctionsAndLayersStub = sinon - .stub(awsDeploy, 'uploadFunctionsAndLayers').resolves(); + .stub(awsDeploy, 'uploadFunctionsAndLayers') + .resolves(); return awsDeploy.uploadArtifacts().then(() => { - expect(uploadCloudFormationFileStub.calledOnce) - .to.be.equal(true); + expect(uploadCloudFormationFileStub.calledOnce).to.be.equal(true); expect(uploadFunctionsAndLayersStub.calledAfter(uploadCloudFormationFileStub)).to.be.equal( - true); + true + ); awsDeploy.uploadCloudFormationFile.restore(); awsDeploy.uploadFunctionsAndLayers.restore(); @@ -85,9 +89,7 @@ describe('uploadArtifacts', () => { normalizeCloudFormationTemplateStub = sinon .stub(normalizeFiles, 'normalizeCloudFormationTemplate') .returns(); - uploadStub = sinon - .stub(awsDeploy.provider, 'request') - .resolves(); + uploadStub = sinon.stub(awsDeploy.provider, 'request').resolves(); }); afterEach(() => { @@ -96,30 +98,34 @@ describe('uploadArtifacts', () => { }); it('should upload the CloudFormation file to the S3 bucket', () => { - cryptoStub.createHash().update().digest.onCall(0).returns('local-hash-cf-template'); + cryptoStub + .createHash() + .update() + .digest.onCall(0) + .returns('local-hash-cf-template'); return awsDeploy.uploadCloudFormationFile().then(() => { expect(normalizeCloudFormationTemplateStub).to.have.been.calledOnce; expect(uploadStub).to.have.been.calledOnce; - expect(uploadStub).to.have.been.calledWithExactly( - 'S3', - 'upload', - { - Bucket: awsDeploy.bucketName, - Key: `${awsDeploy.serverless.service.package - .artifactDirectoryName}/compiled-cloudformation-template.json`, - Body: JSON.stringify({ foo: 'bar' }), - ContentType: 'application/json', - Metadata: { - filesha256: 'local-hash-cf-template', - }, - }); + expect(uploadStub).to.have.been.calledWithExactly('S3', 'upload', { + Bucket: awsDeploy.bucketName, + Key: `${awsDeploy.serverless.service.package.artifactDirectoryName}/compiled-cloudformation-template.json`, + Body: JSON.stringify({ foo: 'bar' }), + ContentType: 'application/json', + Metadata: { + filesha256: 'local-hash-cf-template', + }, + }); expect(normalizeCloudFormationTemplateStub).to.have.been.calledWithExactly({ foo: 'bar' }); }); }); it('should upload the CloudFormation file to a bucket with SSE bucket policy', () => { - cryptoStub.createHash().update().digest.onCall(0).returns('local-hash-cf-template'); + cryptoStub + .createHash() + .update() + .digest.onCall(0) + .returns('local-hash-cf-template'); awsDeploy.serverless.service.provider.deploymentBucketObject = { serverSideEncryption: 'AES256', }; @@ -127,20 +133,16 @@ describe('uploadArtifacts', () => { return awsDeploy.uploadCloudFormationFile().then(() => { expect(normalizeCloudFormationTemplateStub).to.have.been.calledOnce; expect(uploadStub).to.have.been.calledOnce; - expect(uploadStub).to.have.been.calledWithExactly( - 'S3', - 'upload', - { - Bucket: awsDeploy.bucketName, - Key: `${awsDeploy.serverless.service.package - .artifactDirectoryName}/compiled-cloudformation-template.json`, - Body: JSON.stringify({ foo: 'bar' }), - ContentType: 'application/json', - ServerSideEncryption: 'AES256', - Metadata: { - filesha256: 'local-hash-cf-template', - }, - }); + expect(uploadStub).to.have.been.calledWithExactly('S3', 'upload', { + Bucket: awsDeploy.bucketName, + Key: `${awsDeploy.serverless.service.package.artifactDirectoryName}/compiled-cloudformation-template.json`, + Body: JSON.stringify({ foo: 'bar' }), + ContentType: 'application/json', + ServerSideEncryption: 'AES256', + Metadata: { + filesha256: 'local-hash-cf-template', + }, + }); expect(normalizeCloudFormationTemplateStub).to.have.been.calledWithExactly({ foo: 'bar' }); }); }); @@ -151,12 +153,8 @@ describe('uploadArtifacts', () => { let uploadStub; beforeEach(() => { - readFileSyncStub = sinon - .stub(fs, 'readFileSync') - .returns(); - uploadStub = sinon - .stub(awsDeploy.provider, 'request') - .resolves(); + readFileSyncStub = sinon.stub(fs, 'readFileSync').returns(); + uploadStub = sinon.stub(awsDeploy.provider, 'request').resolves(); }); afterEach(() => { @@ -169,35 +167,40 @@ describe('uploadArtifacts', () => { }); it('should upload the .zip file to the S3 bucket', () => { - cryptoStub.createHash().update().digest.onCall(0).returns('local-hash-zip-file'); + cryptoStub + .createHash() + .update() + .digest.onCall(0) + .returns('local-hash-zip-file'); - const tmpDirPath = testUtils.getTmpDirPath(); + const tmpDirPath = getTmpDirPath(); const artifactFilePath = path.join(tmpDirPath, 'artifact.zip'); serverless.utils.writeFileSync(artifactFilePath, 'artifact.zip file content'); return awsDeploy.uploadZipFile(artifactFilePath).then(() => { expect(uploadStub).to.have.been.calledOnce; expect(readFileSyncStub).to.have.been.calledOnce; - expect(uploadStub).to.have.been.calledWithExactly( - 'S3', - 'upload', - { - Bucket: awsDeploy.bucketName, - Key: `${awsDeploy.serverless.service.package.artifactDirectoryName}/artifact.zip`, - Body: sinon.match.object.and(sinon.match.has('path', artifactFilePath)), - ContentType: 'application/zip', - Metadata: { - filesha256: 'local-hash-zip-file', - }, - }); + expect(uploadStub).to.have.been.calledWithExactly('S3', 'upload', { + Bucket: awsDeploy.bucketName, + Key: `${awsDeploy.serverless.service.package.artifactDirectoryName}/artifact.zip`, + Body: sinon.match.object.and(sinon.match.has('path', artifactFilePath)), + ContentType: 'application/zip', + Metadata: { + filesha256: 'local-hash-zip-file', + }, + }); expect(readFileSyncStub).to.have.been.calledWithExactly(artifactFilePath); }); }); it('should upload the .zip file to a bucket with SSE bucket policy', () => { - cryptoStub.createHash().update().digest.onCall(0).returns('local-hash-zip-file'); + cryptoStub + .createHash() + .update() + .digest.onCall(0) + .returns('local-hash-zip-file'); - const tmpDirPath = testUtils.getTmpDirPath(); + const tmpDirPath = getTmpDirPath(); const artifactFilePath = path.join(tmpDirPath, 'artifact.zip'); serverless.utils.writeFileSync(artifactFilePath, 'artifact.zip file content'); awsDeploy.serverless.service.provider.deploymentBucketObject = { @@ -207,19 +210,16 @@ describe('uploadArtifacts', () => { return awsDeploy.uploadZipFile(artifactFilePath).then(() => { expect(uploadStub).to.have.been.calledOnce; expect(readFileSyncStub).to.have.been.calledOnce; - expect(uploadStub).to.have.been.calledWithExactly( - 'S3', - 'upload', - { - Bucket: awsDeploy.bucketName, - Key: `${awsDeploy.serverless.service.package.artifactDirectoryName}/artifact.zip`, - Body: sinon.match.object.and(sinon.match.has('path', artifactFilePath)), - ContentType: 'application/zip', - ServerSideEncryption: 'AES256', - Metadata: { - filesha256: 'local-hash-zip-file', - }, - }); + expect(uploadStub).to.have.been.calledWithExactly('S3', 'upload', { + Bucket: awsDeploy.bucketName, + Key: `${awsDeploy.serverless.service.package.artifactDirectoryName}/artifact.zip`, + Body: sinon.match.object.and(sinon.match.has('path', artifactFilePath)), + ContentType: 'application/zip', + ServerSideEncryption: 'AES256', + Metadata: { + filesha256: 'local-hash-zip-file', + }, + }); expect(readFileSyncStub).to.have.been.calledWithExactly(artifactFilePath); }); }); @@ -286,10 +286,12 @@ describe('uploadArtifacts', () => { return awsDeploy.uploadFunctionsAndLayers().then(() => { expect(uploadZipFileStub.calledTwice).to.be.equal(true); - expect(uploadZipFileStub.args[0][0]) - .to.be.equal(awsDeploy.serverless.service.functions.first.package.artifact); - expect(uploadZipFileStub.args[1][0]) - .to.be.equal(awsDeploy.serverless.service.functions.second.package.artifact); + expect(uploadZipFileStub.args[0][0]).to.be.equal( + awsDeploy.serverless.service.functions.first.package.artifact + ); + expect(uploadZipFileStub.args[1][0]).to.be.equal( + awsDeploy.serverless.service.functions.second.package.artifact + ); }); }); @@ -310,10 +312,12 @@ describe('uploadArtifacts', () => { return awsDeploy.uploadFunctionsAndLayers().then(() => { expect(uploadZipFileStub.calledTwice).to.be.equal(true); - expect(uploadZipFileStub.args[0][0]) - .to.be.equal(awsDeploy.serverless.service.functions.first.package.artifact); - expect(uploadZipFileStub.args[1][0]) - .to.be.equal(awsDeploy.serverless.service.package.artifact); + expect(uploadZipFileStub.args[0][0]).to.be.equal( + awsDeploy.serverless.service.functions.first.package.artifact + ); + expect(uploadZipFileStub.args[1][0]).to.be.equal( + awsDeploy.serverless.service.package.artifact + ); }); }); @@ -329,4 +333,53 @@ describe('uploadArtifacts', () => { }); }); }); + + describe('#uploadCustomResources()', () => { + let uploadStub; + let serviceDirPath; + + beforeEach(() => { + uploadStub = sinon.stub(awsDeploy.provider, 'request').resolves(); + serviceDirPath = createTmpDir(); + serverless.config.servicePath = serviceDirPath; + }); + + afterEach(() => { + uploadStub.restore(); + }); + + it('should not attempt to upload a custom resources if the artifact does not exist', () => { + return expect(awsDeploy.uploadCustomResources()).to.eventually.be.fulfilled.then(() => { + expect(uploadStub).not.to.be.calledOnce; + }); + }); + + it('should upload the custom resources .zip file to the S3 bucket', () => { + const customResourcesFilePath = path.join( + serviceDirPath, + '.serverless', + 'custom-resources.zip' + ); + fse.ensureFileSync(customResourcesFilePath); + + cryptoStub + .createHash() + .update() + .digest.onCall(0) + .returns('local-hash-zip-file'); + + return expect(awsDeploy.uploadCustomResources()).to.eventually.be.fulfilled.then(() => { + expect(uploadStub).to.have.been.calledOnce; + expect(uploadStub).to.have.been.calledWithExactly('S3', 'upload', { + Bucket: awsDeploy.bucketName, + Key: `${awsDeploy.serverless.service.package.artifactDirectoryName}/custom-resources.zip`, + Body: sinon.match.object.and(sinon.match.has('path', customResourcesFilePath)), + ContentType: 'application/zip', + Metadata: { + filesha256: 'local-hash-zip-file', + }, + }); + }); + }); + }); }); diff --git a/lib/plugins/aws/deploy/lib/validateTemplate.js b/lib/plugins/aws/deploy/lib/validateTemplate.js index 8c1626447..b11c15ff7 100644 --- a/lib/plugins/aws/deploy/lib/validateTemplate.js +++ b/lib/plugins/aws/deploy/lib/validateTemplate.js @@ -12,15 +12,10 @@ module.exports = { TemplateURL: `https://${s3Endpoint}/${bucketName}/${artifactDirectoryName}/${compiledTemplateFileName}`, }; - return this.provider.request( - 'CloudFormation', - 'validateTemplate', - params - ).catch((error) => { - const errorMessage = [ - 'The CloudFormation template is invalid:', - ` ${error.message}`, - ].join(''); + return this.provider.request('CloudFormation', 'validateTemplate', params).catch(error => { + const errorMessage = ['The CloudFormation template is invalid:', ` ${error.message}`].join( + '' + ); throw new Error(errorMessage); }); }, diff --git a/lib/plugins/aws/deploy/lib/validateTemplate.test.js b/lib/plugins/aws/deploy/lib/validateTemplate.test.js index 8d504a45b..3e83f4128 100644 --- a/lib/plugins/aws/deploy/lib/validateTemplate.test.js +++ b/lib/plugins/aws/deploy/lib/validateTemplate.test.js @@ -55,7 +55,8 @@ describe('validateTemplate', () => { 'CloudFormation', 'validateTemplate', { - TemplateURL: 'https://s3.amazonaws.com/deployment-bucket/somedir/compiled-cloudformation-template.json', + TemplateURL: + 'https://s3.amazonaws.com/deployment-bucket/somedir/compiled-cloudformation-template.json', } ); }); @@ -64,14 +65,15 @@ describe('validateTemplate', () => { it('should throw an error if the CloudFormation template is invalid', () => { validateTemplateStub.rejects({ message: 'Some error while validating' }); - return expect(awsDeploy.validateTemplate()).to.be.rejected.then((error) => { + return expect(awsDeploy.validateTemplate()).to.be.rejected.then(error => { expect(awsDeploy.serverless.cli.log).to.have.been.called; expect(validateTemplateStub).to.have.been.calledOnce; expect(validateTemplateStub).to.have.been.calledWithExactly( 'CloudFormation', 'validateTemplate', { - TemplateURL: 'https://s3.amazonaws.com/deployment-bucket/somedir/compiled-cloudformation-template.json', + TemplateURL: + 'https://s3.amazonaws.com/deployment-bucket/somedir/compiled-cloudformation-template.json', } ); expect(error.message).to.match(/is invalid: Some error while validating/); diff --git a/lib/plugins/aws/deployFunction/index.js b/lib/plugins/aws/deployFunction/index.js index cab694f3b..820435e73 100644 --- a/lib/plugins/aws/deployFunction/index.js +++ b/lib/plugins/aws/deployFunction/index.js @@ -12,7 +12,8 @@ class AwsDeployFunction { constructor(serverless, options) { this.serverless = serverless; this.options = options || {}; - this.packagePath = this.options.package || + this.packagePath = + this.options.package || this.serverless.service.package.path || path.join(this.serverless.config.servicePath || '.', '.serverless'); this.provider = this.serverless.getProvider('aws'); @@ -23,23 +24,25 @@ class AwsDeployFunction { Object.assign(this, validate); this.hooks = { - 'deploy:function:initialize': () => BbPromise.bind(this) - .then(this.validate) - .then(this.checkIfFunctionExists), + 'deploy:function:initialize': () => + BbPromise.bind(this) + .then(this.validate) + .then(this.checkIfFunctionExists), - 'deploy:function:packageFunction': () => this.serverless.pluginManager - .spawn('package:function'), + 'deploy:function:packageFunction': () => + this.serverless.pluginManager.spawn('package:function'), - 'deploy:function:deploy': () => BbPromise.bind(this) - .then(() => { - if (!this.options['update-config']) { - return this.deployFunction(); - } + 'deploy:function:deploy': () => + BbPromise.bind(this) + .then(() => { + if (!this.options['update-config']) { + return this.deployFunction(); + } - return BbPromise.resolve(); - }) - .then(this.updateFunctionConfiguration) - .then(() => this.serverless.pluginManager.spawn('aws:common:cleanupTempDir')), + return BbPromise.resolve(); + }) + .then(this.updateFunctionConfiguration) + .then(() => this.serverless.pluginManager.spawn('aws:common:cleanupTempDir')), }; } @@ -52,12 +55,9 @@ class AwsDeployFunction { FunctionName: this.options.functionObj.name, }; - return this.provider.request( - 'Lambda', - 'getFunction', - params - ) - .then((result) => { + return this.provider + .request('Lambda', 'getFunction', params) + .then(result => { this.serverless.service.provider.remoteFunctionData = result; return result; }) @@ -84,29 +84,25 @@ class AwsDeployFunction { const roleProperties = roleResource.Properties; const compiledFullRoleName = `${roleProperties.Path || '/'}${roleProperties.RoleName}`; - return this.provider.getAccountInfo().then((result) => - `arn:${result.partition}:iam::${result.accountId}:role${compiledFullRoleName}` - ); + return this.provider + .getAccountInfo() + .then( + result => `arn:${result.partition}:iam::${result.accountId}:role${compiledFullRoleName}` + ); } return BbPromise.resolve(role); } - return this.provider.request( - 'IAM', - 'getRole', - { + return this.provider + .request('IAM', 'getRole', { RoleName: role['Fn::GetAtt'][0], - } - ).then((data) => data.Arn); + }) + .then(data => data.Arn); } callUpdateFunctionConfiguration(params) { - return this.provider.request( - 'Lambda', - 'updateFunctionConfiguration', - params - ).then(() => { + return this.provider.request('Lambda', 'updateFunctionConfiguration', params).then(() => { this.serverless.cli.log(`Successfully updated function: ${this.options.function}`); }); } @@ -146,9 +142,9 @@ class AwsDeployFunction { } if ( - 'layers' in functionObj - && _.isArray(functionObj.layers) - && !_.some(functionObj.layers, _.isObject) + 'layers' in functionObj && + _.isArray(functionObj.layers) && + !_.some(functionObj.layers, _.isObject) ) { params.Layers = functionObj.layers; } @@ -170,7 +166,7 @@ class AwsDeployFunction { if (_.some(params.Environment.Variables, value => _.isObject(value))) { delete params.Environment; } else { - Object.keys(params.Environment.Variables).forEach((key) => { + Object.keys(params.Environment.Variables).forEach(key => { // taken from the bash man pages if (!key.match(/^[A-Za-z_][a-zA-Z0-9_]*$/)) { const errorMessage = 'Invalid characters in environment variable'; @@ -223,10 +219,9 @@ class AwsDeployFunction { } deployFunction() { - const artifactFileName = this.provider.naming - .getFunctionArtifactName(this.options.function); - let artifactFilePath = this.serverless.service.package.artifact || - path.join(this.packagePath, artifactFileName); + const artifactFileName = this.provider.naming.getFunctionArtifactName(this.options.function); + let artifactFilePath = + this.serverless.service.package.artifact || path.join(this.packagePath, artifactFileName); // check if an artifact is used in function package level const functionObject = this.serverless.service.getFunction(this.options.function); @@ -237,7 +232,10 @@ class AwsDeployFunction { const data = fs.readFileSync(artifactFilePath); const remoteHash = this.serverless.service.provider.remoteFunctionData.Configuration.CodeSha256; - const localHash = crypto.createHash('sha256').update(data).digest('base64'); + const localHash = crypto + .createHash('sha256') + .update(data) + .digest('base64'); if (remoteHash === localHash && !this.options.force) { this.serverless.cli.log('Code not changed. Skipping function deployment.'); @@ -254,11 +252,7 @@ class AwsDeployFunction { `Uploading function: ${this.options.function} (${filesize(stats.size)})...` ); - return this.provider.request( - 'Lambda', - 'updateFunctionCode', - params - ).then(() => { + return this.provider.request('Lambda', 'updateFunctionCode', params).then(() => { this.serverless.cli.log(`Successfully deployed function: ${this.options.function}`); }); } diff --git a/lib/plugins/aws/deployFunction/index.test.js b/lib/plugins/aws/deployFunction/index.test.js index 0321e2422..34471f3dd 100644 --- a/lib/plugins/aws/deployFunction/index.test.js +++ b/lib/plugins/aws/deployFunction/index.test.js @@ -7,7 +7,7 @@ const fs = require('fs'); const proxyquire = require('proxyquire'); const AwsProvider = require('../provider/awsProvider'); const Serverless = require('../../../Serverless'); -const testUtils = require('../../../../tests/utils'); +const { getTmpDirPath } = require('../../../../tests/utils/fs'); chai.use(require('chai-as-promised')); chai.use(require('sinon-chai')); @@ -49,17 +49,22 @@ describe('AwsDeployFunction', () => { name: 'first', }, }; - serverless.init(); - serverless.setProvider('aws', new AwsProvider(serverless, options)); - cryptoStub = { - createHash: function () { return this; }, // eslint-disable-line - update: function () { return this; }, // eslint-disable-line - digest: sinon.stub(), - }; - AwsDeployFunction = proxyquire('./index.js', { - crypto: cryptoStub, + return serverless.init().then(() => { + serverless.setProvider('aws', new AwsProvider(serverless, options)); + cryptoStub = { + createHash() { + return this; + }, // eslint-disable-line + update() { + return this; + }, // eslint-disable-line + digest: sinon.stub(), + }; + AwsDeployFunction = proxyquire('./index.js', { + crypto: cryptoStub, + }); + awsDeployFunction = new AwsDeployFunction(serverless, options); }); - awsDeployFunction = new AwsDeployFunction(serverless, options); }); describe('#constructor()', () => { @@ -89,7 +94,7 @@ describe('AwsDeployFunction', () => { }); it('it should throw error if function is not provided', () => { - serverless.service.functions = null; + serverless.service.functions = {}; expect(() => awsDeployFunction.checkIfFunctionExists()).to.throw(Error); }); @@ -103,13 +108,11 @@ describe('AwsDeployFunction', () => { return awsDeployFunction.checkIfFunctionExists().then(() => { expect(getFunctionStub.calledOnce).to.be.equal(true); - expect(getFunctionStub.calledWithExactly( - 'Lambda', - 'getFunction', - { + expect( + getFunctionStub.calledWithExactly('Lambda', 'getFunction', { FunctionName: 'first', - } - )).to.be.equal(true); + }) + ).to.be.equal(true); expect(awsDeployFunction.serverless.service.provider.remoteFunctionData).to.deep.equal({ func: { name: 'first', @@ -152,7 +155,7 @@ describe('AwsDeployFunction', () => { it('should return unmodified ARN if ARN was provided', () => { const arn = 'arn:aws:iam::123456789012:role/role'; - return awsDeployFunction.normalizeArnRole(arn).then((result) => { + return awsDeployFunction.normalizeArnRole(arn).then(result => { expect(getAccountInfoStub).to.not.have.been.called; expect(result).to.be.equal(arn); }); @@ -161,7 +164,7 @@ describe('AwsDeployFunction', () => { it('should return compiled ARN if role name was provided', () => { const roleName = 'MyCustomRole'; - return awsDeployFunction.normalizeArnRole(roleName).then((result) => { + return awsDeployFunction.normalizeArnRole(roleName).then(result => { expect(getAccountInfoStub).to.have.been.called; expect(result).to.be.equal('arn:aws:iam::123456789012:role/role_123'); }); @@ -169,13 +172,10 @@ describe('AwsDeployFunction', () => { it('should return compiled ARN if object role was provided', () => { const roleObj = { - 'Fn::GetAtt': [ - 'role_2', - 'Arn', - ], + 'Fn::GetAtt': ['role_2', 'Arn'], }; - return awsDeployFunction.normalizeArnRole(roleObj).then((result) => { + return awsDeployFunction.normalizeArnRole(roleObj).then(result => { expect(getRoleStub.calledOnce).to.be.equal(true); expect(getAccountInfoStub).to.not.have.been.called; expect(result).to.be.equal('arn:aws:iam::123456789012:role/role_2'); @@ -214,7 +214,7 @@ describe('AwsDeployFunction', () => { awsDeployFunction.serverless.service.provider.vpc = undefined; }); - it('should update function\'s configuration', () => { + it("should update function's configuration", () => { options.functionObj = { awsKmsKeyArn: 'arn:aws:kms:us-east-1:123456789012', description: 'desc', @@ -240,32 +240,34 @@ describe('AwsDeployFunction', () => { 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', - 'updateFunctionConfiguration', - { - DeadLetterConfig: { - TargetArn: 'arn:aws:sqs:us-east-1:123456789012:dlq', - }, - Handler: 'my_handler', - Description: 'desc', - Environment: { - Variables: { - VARIABLE: 'value', + expect( + updateFunctionConfigurationStub.calledWithExactly( + 'Lambda', + 'updateFunctionConfiguration', + { + DeadLetterConfig: { + TargetArn: 'arn:aws:sqs:us-east-1:123456789012:dlq', }, - }, - FunctionName: 'first', - KMSKeyArn: 'arn:aws:kms:us-east-1:123456789012', - MemorySize: 128, - Role: 'arn:aws:us-east-1:123456789012:role/role', - Timeout: 3, - VpcConfig: { - SecurityGroupIds: ['1'], - SubnetIds: ['2'], - }, - Layers: ['arn:aws:lambda:us-east-1:123456789012:layer:layer:1'], - } - )).to.be.equal(true); + Handler: 'my_handler', + Description: 'desc', + Environment: { + Variables: { + VARIABLE: 'value', + }, + }, + FunctionName: 'first', + KMSKeyArn: 'arn:aws:kms:us-east-1:123456789012', + MemorySize: 128, + Role: 'arn:aws:us-east-1:123456789012:role/role', + Timeout: 3, + VpcConfig: { + SecurityGroupIds: ['1'], + SubnetIds: ['2'], + }, + Layers: ['arn:aws:lambda:us-east-1:123456789012:layer:layer:1'], + } + ) + ).to.be.equal(true); }); }); @@ -280,14 +282,16 @@ describe('AwsDeployFunction', () => { return awsDeployFunction.updateFunctionConfiguration().then(() => { expect(updateFunctionConfigurationStub.calledOnce).to.be.equal(true); - expect(updateFunctionConfigurationStub.calledWithExactly( - 'Lambda', - 'updateFunctionConfiguration', - { - FunctionName: 'first', - Description: 'change', - } - )).to.be.equal(true); + expect( + updateFunctionConfigurationStub.calledWithExactly( + 'Lambda', + 'updateFunctionConfiguration', + { + FunctionName: 'first', + Description: 'change', + } + ) + ).to.be.equal(true); }); }); @@ -297,12 +301,18 @@ describe('AwsDeployFunction', () => { description: 'change', handler: 'my_handler', vpc: { - securityGroupIds: ['xxxxx', { - ref: 'myVPCRef', - }], - subnetIds: ['xxxxx', { - ref: 'myVPCRef', - }], + securityGroupIds: [ + 'xxxxx', + { + ref: 'myVPCRef', + }, + ], + subnetIds: [ + 'xxxxx', + { + ref: 'myVPCRef', + }, + ], }, environment: { myvar: 'this is my var', @@ -316,15 +326,17 @@ describe('AwsDeployFunction', () => { return awsDeployFunction.updateFunctionConfiguration().then(() => { expect(updateFunctionConfigurationStub.calledOnce).to.be.equal(true); - expect(updateFunctionConfigurationStub.calledWithExactly( - 'Lambda', - 'updateFunctionConfiguration', - { - FunctionName: 'first', - Handler: 'my_handler', - Description: 'change', - } - )).to.be.equal(true); + expect( + updateFunctionConfigurationStub.calledWithExactly( + 'Lambda', + 'updateFunctionConfiguration', + { + FunctionName: 'first', + Handler: 'my_handler', + Description: 'change', + } + ) + ).to.be.equal(true); }); }); @@ -341,8 +353,9 @@ describe('AwsDeployFunction', () => { awsDeployFunction.options = options; - return expect(awsDeployFunction.updateFunctionConfiguration()).to.be.fulfilled - .then(() => expect(updateFunctionConfigurationStub).to.not.be.called); + return expect(awsDeployFunction.updateFunctionConfiguration()).to.be.fulfilled.then( + () => expect(updateFunctionConfigurationStub).to.not.be.called + ); }); it('should fail when using invalid characters in environment variable', () => { @@ -359,37 +372,37 @@ describe('AwsDeployFunction', () => { expect(() => awsDeployFunction.updateFunctionConfiguration()).to.throw(Error); }); - it('should transform to string values when using non-string values as environment variables', - () => { - options.functionObj = { - name: 'first', - handler: 'my_handler', - description: 'change', - environment: { - COUNTER: 6, - }, - }; + it('should transform to string values when using non-string values as environment variables', () => { + options.functionObj = { + name: 'first', + handler: 'my_handler', + description: 'change', + environment: { + COUNTER: 6, + }, + }; - awsDeployFunction.options = options; - return expect(awsDeployFunction.updateFunctionConfiguration()).to.be.fulfilled - .then(() => { - expect(updateFunctionConfigurationStub.calledOnce).to.be.equal(true); - expect(updateFunctionConfigurationStub.calledWithExactly( - 'Lambda', - 'updateFunctionConfiguration', - { - FunctionName: 'first', - Handler: 'my_handler', - Description: 'change', - Environment: { - Variables: { - COUNTER: '6', - }, + awsDeployFunction.options = options; + return expect(awsDeployFunction.updateFunctionConfiguration()).to.be.fulfilled.then(() => { + expect(updateFunctionConfigurationStub.calledOnce).to.be.equal(true); + expect( + updateFunctionConfigurationStub.calledWithExactly( + 'Lambda', + 'updateFunctionConfiguration', + { + FunctionName: 'first', + Handler: 'my_handler', + Description: 'change', + Environment: { + Variables: { + COUNTER: '6', }, - } - )).to.be.equal(true); - }); + }, + } + ) + ).to.be.equal(true); }); + }); it('should inherit provider-level config', () => { options.functionObj = { @@ -412,22 +425,24 @@ describe('AwsDeployFunction', () => { expect(normalizeArnRoleStub.calledOnce).to.be.equal(true); expect(normalizeArnRoleStub.calledWithExactly('role')); expect(updateFunctionConfigurationStub.calledOnce).to.be.equal(true); - expect(updateFunctionConfigurationStub.calledWithExactly( - 'Lambda', - 'updateFunctionConfiguration', - { - FunctionName: 'first', - Handler: 'my_handler', - Description: 'change', - VpcConfig: { - SubnetIds: [1234, 12345], - SecurityGroupIds: [123, 12], - }, - Timeout: 12, - MemorySize: 512, - Role: 'arn:aws:us-east-1:123456789012:role/role', - } - )).to.be.equal(true); + expect( + updateFunctionConfigurationStub.calledWithExactly( + 'Lambda', + 'updateFunctionConfiguration', + { + FunctionName: 'first', + Handler: 'my_handler', + Description: 'change', + VpcConfig: { + SubnetIds: [1234, 12345], + SecurityGroupIds: [123, 12], + }, + Timeout: 12, + MemorySize: 512, + Role: 'arn:aws:us-east-1:123456789012:role/role', + } + ) + ).to.be.equal(true); }); }); }); @@ -440,19 +455,13 @@ describe('AwsDeployFunction', () => { beforeEach(() => { // write a file to disc to simulate that the deployment artifact exists - awsDeployFunction.packagePath = testUtils.getTmpDirPath(); + awsDeployFunction.packagePath = getTmpDirPath(); artifactFilePath = path.join(awsDeployFunction.packagePath, 'first.zip'); serverless.utils.writeFileSync(artifactFilePath, 'first.zip file content'); - updateFunctionCodeStub = sinon - .stub(awsDeployFunction.provider, 'request') - .resolves(); - statSyncStub = sinon - .stub(fs, 'statSync') - .returns({ size: 1024 }); + updateFunctionCodeStub = sinon.stub(awsDeployFunction.provider, 'request').resolves(); + statSyncStub = sinon.stub(fs, 'statSync').returns({ size: 1024 }); sinon.spy(awsDeployFunction.serverless.cli, 'log'); - readFileSyncStub = sinon - .stub(fs, 'readFileSync') - .returns(); + readFileSyncStub = sinon.stub(fs, 'readFileSync').returns(); awsDeployFunction.serverless.service.provider.remoteFunctionData = { Configuration: { CodeSha256: 'remote-hash-zip-file', @@ -467,48 +476,56 @@ describe('AwsDeployFunction', () => { }); it('should deploy the function if the hashes are different', () => { - cryptoStub.createHash().update().digest.onCall(0).returns('local-hash-zip-file'); + cryptoStub + .createHash() + .update() + .digest.onCall(0) + .returns('local-hash-zip-file'); return awsDeployFunction.deployFunction().then(() => { const data = fs.readFileSync(artifactFilePath); expect(updateFunctionCodeStub.calledOnce).to.be.equal(true); expect(readFileSyncStub.called).to.equal(true); - expect(updateFunctionCodeStub.calledWithExactly( - 'Lambda', - 'updateFunctionCode', - { + expect( + updateFunctionCodeStub.calledWithExactly('Lambda', 'updateFunctionCode', { FunctionName: 'first', ZipFile: data, - } - )).to.be.equal(true); + }) + ).to.be.equal(true); expect(readFileSyncStub.calledWithExactly(artifactFilePath)).to.equal(true); }); }); it('should deploy the function if the hashes are same but the "force" option is used', () => { awsDeployFunction.options.force = true; - cryptoStub.createHash().update().digest.onCall(0).returns('remote-hash-zip-file'); + cryptoStub + .createHash() + .update() + .digest.onCall(0) + .returns('remote-hash-zip-file'); return awsDeployFunction.deployFunction().then(() => { const data = fs.readFileSync(artifactFilePath); expect(updateFunctionCodeStub.calledOnce).to.be.equal(true); expect(readFileSyncStub.called).to.equal(true); - expect(updateFunctionCodeStub.calledWithExactly( - 'Lambda', - 'updateFunctionCode', - { + expect( + updateFunctionCodeStub.calledWithExactly('Lambda', 'updateFunctionCode', { FunctionName: 'first', ZipFile: data, - } - )).to.be.equal(true); + }) + ).to.be.equal(true); expect(readFileSyncStub.calledWithExactly(artifactFilePath)).to.equal(true); }); }); it('should resolve if the hashes are the same', () => { - cryptoStub.createHash().update().digest.onCall(0).returns('remote-hash-zip-file'); + cryptoStub + .createHash() + .update() + .digest.onCall(0) + .returns('remote-hash-zip-file'); return awsDeployFunction.deployFunction().then(() => { const expected = 'Code not changed. Skipping function deployment.'; @@ -551,22 +568,21 @@ describe('AwsDeployFunction', () => { serverless.service.getFunction.restore(); }); - it('should read the provided artifact', () => awsDeployFunction.deployFunction().then(() => { - const data = fs.readFileSync(artifactZipFile); + it('should read the provided artifact', () => + awsDeployFunction.deployFunction().then(() => { + const data = fs.readFileSync(artifactZipFile); - expect(readFileSyncStub).to.have.been.calledWithExactly(artifactZipFile); - expect(statSyncStub).to.have.been.calledWithExactly(artifactZipFile); - expect(getFunctionStub).to.have.been.calledWithExactly('first'); - expect(updateFunctionCodeStub.calledOnce).to.equal(true); - expect(updateFunctionCodeStub.calledWithExactly( - 'Lambda', - 'updateFunctionCode', - { - FunctionName: 'first', - ZipFile: data, - } - )).to.be.equal(true); - })); + expect(readFileSyncStub).to.have.been.calledWithExactly(artifactZipFile); + expect(statSyncStub).to.have.been.calledWithExactly(artifactZipFile); + expect(getFunctionStub).to.have.been.calledWithExactly('first'); + expect(updateFunctionCodeStub.calledOnce).to.equal(true); + expect( + updateFunctionCodeStub.calledWithExactly('Lambda', 'updateFunctionCode', { + FunctionName: 'first', + ZipFile: data, + }) + ).to.be.equal(true); + })); }); }); }); diff --git a/lib/plugins/aws/deployList/index.js b/lib/plugins/aws/deployList/index.js index 81f651854..4465b7caa 100644 --- a/lib/plugins/aws/deployList/index.js +++ b/lib/plugins/aws/deployList/index.js @@ -12,23 +12,17 @@ class AwsDeployList { this.options = options || {}; this.provider = this.serverless.getProvider('aws'); - Object.assign( - this, - validate, - setBucketName - ); + Object.assign(this, validate, setBucketName); this.hooks = { - 'before:deploy:list:log': () => BbPromise.bind(this) - .then(this.validate), - 'before:deploy:list:functions:log': () => BbPromise.bind(this) - .then(this.validate), + 'before:deploy:list:log': () => BbPromise.bind(this).then(this.validate), + 'before:deploy:list:functions:log': () => BbPromise.bind(this).then(this.validate), - 'deploy:list:log': () => BbPromise.bind(this) - .then(this.setBucketName) - .then(this.listDeployments), - 'deploy:list:functions:log': () => BbPromise.bind(this) - .then(this.listFunctions), + 'deploy:list:log': () => + BbPromise.bind(this) + .then(this.setBucketName) + .then(this.listDeployments), + 'deploy:list:functions:log': () => BbPromise.bind(this).then(this.listFunctions), }; } @@ -37,40 +31,39 @@ class AwsDeployList { const stage = this.provider.getStage(); const prefix = this.provider.getDeploymentPrefix(); - return this.provider.request('S3', - 'listObjectsV2', - { + return this.provider + .request('S3', 'listObjectsV2', { Bucket: this.bucketName, Prefix: `${prefix}/${service}/${stage}`, - } - ) - .then((response) => { - const directoryRegex = new RegExp('(.+)-(.+-.+-.+)'); - const deployments = findAndGroupDeployments(response, prefix, service, stage); + }) + .then(response => { + const directoryRegex = new RegExp('(.+)-(.+-.+-.+)'); + const deployments = findAndGroupDeployments(response, prefix, service, stage); - if (deployments.length === 0) { - this.serverless.cli.log('Couldn\'t find any existing deployments.'); - this.serverless.cli.log('Please verify that stage and region are correct.'); - return BbPromise.resolve(); - } - this.serverless.cli.log('Listing deployments:'); - deployments.forEach((deployment) => { - this.serverless.cli.log('-------------'); - const match = deployment[0].directory.match(directoryRegex); - this.serverless.cli.log(`Timestamp: ${match[1]}`); - this.serverless.cli.log(`Datetime: ${match[2]}`); - this.serverless.cli.log('Files:'); - deployment.forEach((entry) => { - this.serverless.cli.log(`- ${entry.file}`); + if (deployments.length === 0) { + this.serverless.cli.log("Couldn't find any existing deployments."); + this.serverless.cli.log('Please verify that stage and region are correct.'); + return BbPromise.resolve(); + } + this.serverless.cli.log('Listing deployments:'); + deployments.forEach(deployment => { + this.serverless.cli.log('-------------'); + const match = deployment[0].directory.match(directoryRegex); + this.serverless.cli.log(`Timestamp: ${match[1]}`); + this.serverless.cli.log(`Datetime: ${match[2]}`); + this.serverless.cli.log('Files:'); + deployment.forEach(entry => { + this.serverless.cli.log(`- ${entry.file}`); + }); }); + return BbPromise.resolve(); }); - return BbPromise.resolve(); - }); } // list all functions and their versions listFunctions() { - return BbPromise.resolve().bind(this) + return BbPromise.resolve() + .bind(this) .then(this.getFunctions) .then(this.getFunctionVersions) .then(this.displayFunctions); @@ -79,27 +72,23 @@ class AwsDeployList { getFunctions() { const funcs = this.serverless.service.getAllFunctionsNames(); - return BbPromise.map(funcs, (funcName) => { + return BbPromise.map(funcs, funcName => { const params = { FunctionName: funcName, }; - return this.provider.request('Lambda', - 'getFunction', - params); - }).then((result) => _.map(result, (item) => item.Configuration)); + return this.provider.request('Lambda', 'getFunction', params); + }).then(result => _.map(result, item => item.Configuration)); } getFunctionPaginatedVersions(params, totalVersions) { - return this.provider.request('Lambda', - 'listVersionsByFunction', - params - ) - .then((response) => { + return this.provider.request('Lambda', 'listVersionsByFunction', params).then(response => { const Versions = (totalVersions || []).concat(response.Versions); if (response.NextMarker) { return this.getFunctionPaginatedVersions( - Object.assign({}, params, { Marker: response.NextMarker }), Versions); + Object.assign({}, params, { Marker: response.NextMarker }), + Versions + ); } return Promise.resolve({ Versions }); @@ -109,7 +98,7 @@ class AwsDeployList { getFunctionVersions(funcs) { const requestPromises = []; - funcs.forEach((func) => { + funcs.forEach(func => { const params = { FunctionName: func.FunctionName, }; @@ -124,7 +113,7 @@ class AwsDeployList { this.serverless.cli.log('Listing functions and their last 5 versions:'); this.serverless.cli.log('-------------'); - funcs.forEach((func) => { + funcs.forEach(func => { let message = ''; let name = func.Versions[0].FunctionName; @@ -132,8 +121,9 @@ class AwsDeployList { name = name.replace(`${this.provider.getStage()}-`, ''); message += `${name}: `; - const versions = func.Versions.map((funcEntry) => funcEntry.Version) - .slice(Math.max(0, func.Versions.length - 5)); + const versions = func.Versions.map(funcEntry => funcEntry.Version).slice( + Math.max(0, func.Versions.length - 5) + ); message += versions.join(', '); this.serverless.cli.log(message); diff --git a/lib/plugins/aws/deployList/index.test.js b/lib/plugins/aws/deployList/index.test.js index 92ea6568b..ee51ecb39 100644 --- a/lib/plugins/aws/deployList/index.test.js +++ b/lib/plugins/aws/deployList/index.test.js @@ -35,20 +35,17 @@ describe('AwsDeployList', () => { const s3Response = { Contents: [], }; - const listObjectsStub = sinon - .stub(awsDeployList.provider, 'request').resolves(s3Response); + const listObjectsStub = sinon.stub(awsDeployList.provider, 'request').resolves(s3Response); return awsDeployList.listDeployments().then(() => { expect(listObjectsStub.calledOnce).to.be.equal(true); - expect(listObjectsStub.calledWithExactly( - 'S3', - 'listObjectsV2', - { + expect( + listObjectsStub.calledWithExactly('S3', 'listObjectsV2', { Bucket: awsDeployList.bucketName, Prefix: `${s3Key}`, - } - )).to.be.equal(true); - const infoText = 'Couldn\'t find any existing deployments.'; + }) + ).to.be.equal(true); + const infoText = "Couldn't find any existing deployments."; expect(awsDeployList.serverless.cli.log.calledWithExactly(infoText)).to.be.equal(true); const verifyText = 'Please verify that stage and region are correct.'; expect(awsDeployList.serverless.cli.log.calledWithExactly(verifyText)).to.be.equal(true); @@ -66,19 +63,16 @@ describe('AwsDeployList', () => { ], }; - const listObjectsStub = sinon - .stub(awsDeployList.provider, 'request').resolves(s3Response); + const listObjectsStub = sinon.stub(awsDeployList.provider, 'request').resolves(s3Response); return awsDeployList.listDeployments().then(() => { expect(listObjectsStub.calledOnce).to.be.equal(true); - expect(listObjectsStub.calledWithExactly( - 'S3', - 'listObjectsV2', - { + expect( + listObjectsStub.calledWithExactly('S3', 'listObjectsV2', { Bucket: awsDeployList.bucketName, Prefix: `${s3Key}`, - } - )).to.be.equal(true); + }) + ).to.be.equal(true); const infoText = 'Listing deployments:'; expect(awsDeployList.serverless.cli.log.calledWithExactly(infoText)).to.be.equal(true); const timestampOne = 'Timestamp: 113304333331'; @@ -111,13 +105,12 @@ describe('AwsDeployList', () => { awsDeployList.displayFunctions.restore(); }); - it('should run promise chain in order', () => awsDeployList - .listFunctions().then(() => { + it('should run promise chain in order', () => + awsDeployList.listFunctions().then(() => { expect(getFunctionsStub.calledOnce).to.equal(true); expect(getFunctionVersionsStub.calledAfter(getFunctionsStub)).to.equal(true); expect(displayFunctionsStub.calledAfter(getFunctionVersionsStub)).to.equal(true); - }) - ); + })); }); describe('#getFunctions()', () => { @@ -155,7 +148,7 @@ describe('AwsDeployList', () => { { FunctionName: 'listDeployments-dev-func2' }, ]; - return awsDeployList.getFunctions().then((result) => { + return awsDeployList.getFunctions().then(result => { expect(listFunctionsStub.callCount).to.equal(2); expect(result).to.deep.equal(expectedResult); }); @@ -168,16 +161,12 @@ describe('AwsDeployList', () => { .stub(awsDeployList.provider, 'request') .onFirstCall() .resolves({ - Versions: [ - { FunctionName: 'listDeployments-dev-func', Version: '1' }, - ], + Versions: [{ FunctionName: 'listDeployments-dev-func', Version: '1' }], NextMarker: '123', }) .onSecondCall() .resolves({ - Versions: [ - { FunctionName: 'listDeployments-dev-func', Version: '2' }, - ], + Versions: [{ FunctionName: 'listDeployments-dev-func', Version: '2' }], }); }); @@ -190,7 +179,7 @@ describe('AwsDeployList', () => { FunctionName: 'listDeployments-dev-func', }; - return awsDeployList.getFunctionPaginatedVersions(params).then((result) => { + return awsDeployList.getFunctionPaginatedVersions(params).then(result => { const expectedResult = { Versions: [ { FunctionName: 'listDeployments-dev-func', Version: '1' }, @@ -207,13 +196,9 @@ describe('AwsDeployList', () => { let listVersionsByFunctionStub; beforeEach(() => { - listVersionsByFunctionStub = sinon - .stub(awsDeployList.provider, 'request') - .resolves({ - Versions: [ - { FunctionName: 'listDeployments-dev-func', Version: '$LATEST' }, - ], - }); + listVersionsByFunctionStub = sinon.stub(awsDeployList.provider, 'request').resolves({ + Versions: [{ FunctionName: 'listDeployments-dev-func', Version: '$LATEST' }], + }); }); afterEach(() => { @@ -226,17 +211,13 @@ describe('AwsDeployList', () => { { FunctionName: 'listDeployments-dev-func2' }, ]; - return awsDeployList.getFunctionVersions(funcs).then((result) => { + return awsDeployList.getFunctionVersions(funcs).then(result => { const expectedResult = [ { - Versions: [ - { FunctionName: 'listDeployments-dev-func', Version: '$LATEST' }, - ], + Versions: [{ FunctionName: 'listDeployments-dev-func', Version: '$LATEST' }], }, { - Versions: [ - { FunctionName: 'listDeployments-dev-func', Version: '$LATEST' }, - ], + Versions: [{ FunctionName: 'listDeployments-dev-func', Version: '$LATEST' }], }, ]; @@ -249,9 +230,7 @@ describe('AwsDeployList', () => { describe('#displayFunctions()', () => { const funcs = [ { - Versions: [ - { FunctionName: 'listDeployments-dev-func-1', Version: '1337' }, - ], + Versions: [{ FunctionName: 'listDeployments-dev-func-1', Version: '1337' }], }, { Versions: [ @@ -269,8 +248,9 @@ describe('AwsDeployList', () => { const log = awsDeployList.serverless.cli.log; return awsDeployList.displayFunctions(funcs).then(() => { - expect(log.calledWithExactly('Listing functions and their last 5 versions:')) - .to.be.equal(true); + expect(log.calledWithExactly('Listing functions and their last 5 versions:')).to.be.equal( + true + ); expect(log.calledWithExactly('-------------')).to.be.equal(true); expect(log.calledWithExactly('func-1: 1337')).to.be.equal(true); diff --git a/lib/plugins/aws/info/display.js b/lib/plugins/aws/info/display.js index dbf915752..5b60239e2 100644 --- a/lib/plugins/aws/info/display.js +++ b/lib/plugins/aws/info/display.js @@ -32,7 +32,7 @@ module.exports = { let apiKeysMessage = `${chalk.yellow('api keys:')}`; if (info.apiKeys && info.apiKeys.length > 0) { - info.apiKeys.forEach((apiKeyInfo) => { + info.apiKeys.forEach(apiKeyInfo => { const description = apiKeyInfo.description ? ` - ${apiKeyInfo.description}` : ''; if (conceal) { apiKeysMessage += `\n ${apiKeyInfo.name}${description}`; @@ -53,10 +53,10 @@ module.exports = { let endpointsMessage = `${chalk.yellow('endpoints:')}`; if (info.endpoints && info.endpoints.length) { - _.forEach(info.endpoints, (endpoint) => { + _.forEach(info.endpoints, endpoint => { // if the endpoint is of type http(s) if (!endpoint.startsWith('wss://')) { - _.forEach(this.serverless.service.functions, (functionObject) => { + _.forEach(this.serverless.service.functions, functionObject => { functionObject.events.forEach(event => { if (event.http) { let method; @@ -69,7 +69,13 @@ module.exports = { method = event.http.split(' ')[0].toUpperCase(); path = event.http.split(' ')[1]; } - path = path !== '/' ? `/${path.split('/').filter(p => p !== '').join('/')}` : ''; + path = + path !== '/' + ? `/${path + .split('/') + .filter(p => p !== '') + .join('/')}` + : ''; endpointsMessage += `\n ${method} - ${endpoint}${path}`; } }); @@ -92,7 +98,7 @@ module.exports = { let functionsMessage = `${chalk.yellow('functions:')}`; if (info.functions && info.functions.length > 0) { - info.functions.forEach((f) => { + info.functions.forEach(f => { functionsMessage += `\n ${f.name}: ${f.deployedName}`; }); } else { @@ -108,7 +114,7 @@ module.exports = { let layersMessage = `${chalk.yellow('layers:')}`; if (info.layers && info.layers.length > 0) { - info.layers.forEach((l) => { + info.layers.forEach(l => { layersMessage += `\n ${l.name}: ${l.arn}`; }); } else { @@ -123,7 +129,7 @@ module.exports = { let message = ''; if (this.options.verbose) { message = `${chalk.yellow.underline('\nStack Outputs\n')}`; - _.forEach(this.gatheredData.outputs, (output) => { + _.forEach(this.gatheredData.outputs, output => { message += `${chalk.yellow(output.OutputKey)}: ${output.OutputValue}\n`; }); diff --git a/lib/plugins/aws/info/display.test.js b/lib/plugins/aws/info/display.test.js index 76e5bdb9d..e7f470c4e 100644 --- a/lib/plugins/aws/info/display.test.js +++ b/lib/plugins/aws/info/display.test.js @@ -69,7 +69,8 @@ describe('#display()', () => { expectedMessage += `\n${chalk.red('WARNING:')}\n`; expectedMessage += ' You have 150 resources in your service.\n'; expectedMessage += ' CloudFormation has a hard limit of 200 resources in a service.\n'; - expectedMessage += ' For advice on avoiding this limit, check out this link: http://bit.ly/2IiYB38.'; + expectedMessage += + ' For advice on avoiding this limit, check out this link: http://bit.ly/2IiYB38.'; awsInfo.gatheredData.info.resourceCount = 150; @@ -79,11 +80,13 @@ describe('#display()', () => { }); it('should display API keys if given', () => { - awsInfo.gatheredData.info.apiKeys = [{ - name: 'keyOne', - value: '1234', - description: 'keyOne description', - }]; + awsInfo.gatheredData.info.apiKeys = [ + { + name: 'keyOne', + value: '1234', + description: 'keyOne description', + }, + ]; let expectedMessage = ''; @@ -103,11 +106,13 @@ describe('#display()', () => { it('should hide API keys values when `--conceal` is given', () => { awsInfo.options.conceal = true; - awsInfo.gatheredData.info.apiKeys = [{ - name: 'keyOne', - value: '1234', - description: 'keyOne description', - }]; + awsInfo.gatheredData.info.apiKeys = [ + { + name: 'keyOne', + value: '1234', + description: 'keyOne description', + }, + ]; let expectedMessage = ''; @@ -182,7 +187,9 @@ describe('#display()', () => { }); it('should display wss endpoint if given', () => { - awsInfo.gatheredData.info.endpoints = ['wss://ab12cd34ef.execute-api.us-east-1.amazonaws.com/dev']; + awsInfo.gatheredData.info.endpoints = [ + 'wss://ab12cd34ef.execute-api.us-east-1.amazonaws.com/dev', + ]; let expectedMessage = ''; diff --git a/lib/plugins/aws/info/getApiKeyValues.js b/lib/plugins/aws/info/getApiKeyValues.js index a95ba9e47..fdfa3f9ff 100644 --- a/lib/plugins/aws/info/getApiKeyValues.js +++ b/lib/plugins/aws/info/getApiKeyValues.js @@ -12,11 +12,11 @@ module.exports = { const apiKeyDefinitions = this.serverless.service.provider.apiKeys; const apiKeyNames = []; if (_.isArray(apiKeyDefinitions) && apiKeyDefinitions.length) { - _.forEach(apiKeyDefinitions, (definition) => { + _.forEach(apiKeyDefinitions, definition => { // different API key types are nested in separate arrays if (_.isObject(definition)) { const keyTypeName = Object.keys(definition)[0]; - _.forEach(definition[keyTypeName], (keyName) => apiKeyNames.push(keyName)); + _.forEach(definition[keyTypeName], keyName => apiKeyNames.push(keyName)); } else if (_.isString(definition)) { // plain strings are simple, non-nested API keys apiKeyNames.push(definition); @@ -26,8 +26,9 @@ module.exports = { if (apiKeyNames.length) { return this.provider - .request('CloudFormation', - 'describeStackResources', { StackName: this.provider.naming.getStackName() }) + .request('CloudFormation', 'describeStackResources', { + StackName: this.provider.naming.getStackName(), + }) .then(resources => { const apiKeys = _(resources.StackResources) .filter(resource => resource.ResourceType === 'AWS::ApiGateway::ApiKey') @@ -44,9 +45,9 @@ module.exports = { }) .then(apiKeys => { if (apiKeys && apiKeys.length) { - info.apiKeys = - _.map(apiKeys, apiKey => - _.pick(apiKey, ['name', 'value', 'description'])); + info.apiKeys = _.map(apiKeys, apiKey => + _.pick(apiKey, ['name', 'value', 'description']) + ); } return BbPromise.resolve(); }); diff --git a/lib/plugins/aws/info/getResourceCount.js b/lib/plugins/aws/info/getResourceCount.js index 632887db9..abe00ac4c 100644 --- a/lib/plugins/aws/info/getResourceCount.js +++ b/lib/plugins/aws/info/getResourceCount.js @@ -7,14 +7,13 @@ module.exports = { getResourceCount() { const stackName = this.provider.naming.getStackName(); - return this.provider.request('CloudFormation', - 'listStackResources', - { StackName: stackName }) - .then(result => { - if (!_.isEmpty(result)) { - this.gatheredData.info.resourceCount = result.StackResourceSummaries.length; - } - return BbPromise.resolve(); - }); + return this.provider + .request('CloudFormation', 'listStackResources', { StackName: stackName }) + .then(result => { + if (!_.isEmpty(result)) { + this.gatheredData.info.resourceCount = result.StackResourceSummaries.length; + } + return BbPromise.resolve(); + }); }, }; diff --git a/lib/plugins/aws/info/getResourceCount.test.js b/lib/plugins/aws/info/getResourceCount.test.js index ba150a4ff..2dfa0baba 100644 --- a/lib/plugins/aws/info/getResourceCount.test.js +++ b/lib/plugins/aws/info/getResourceCount.test.js @@ -37,57 +37,79 @@ describe('#getResourceCount()', () => { it('attach resourceCount to this.gatheredData after listStackResources call', () => { const listStackResourcesResponse = { ResponseMetadata: { RequestId: '81386aed-258b-11e8-b3e8-a937105b7db3' }, - StackResourceSummaries: - [{ LogicalResourceId: 'ApiGatewayDeployment1520814106863', - PhysicalResourceId: 'eoa2a2', - ResourceType: 'AWS::ApiGateway::Deployment', - LastUpdatedTimestamp: '2018-03-12T00:22:40.680Z', - ResourceStatus: 'CREATE_COMPLETE' }, - { LogicalResourceId: 'ApiGatewayMethodHelloGet', - PhysicalResourceId: 'hello-ApiGa-11R27BUE48W38', - ResourceType: 'AWS::ApiGateway::Method', - LastUpdatedTimestamp: '2018-03-12T00:22:37.478Z', - ResourceStatus: 'CREATE_COMPLETE' }, - { LogicalResourceId: 'ApiGatewayResourceHello', - PhysicalResourceId: 'az5f7l', - ResourceType: 'AWS::ApiGateway::Resource', - LastUpdatedTimestamp: '2018-03-12T00:22:22.916Z', - ResourceStatus: 'CREATE_COMPLETE' }, - { LogicalResourceId: 'ApiGatewayRestApi', - PhysicalResourceId: 'n1uk4p7kl0', - ResourceType: 'AWS::ApiGateway::RestApi', - LastUpdatedTimestamp: '2018-03-12T00:22:19.768Z', - ResourceStatus: 'CREATE_COMPLETE' }, - { LogicalResourceId: 'HelloLambdaFunction', - PhysicalResourceId: 'hello-world-2-dev-hello', - ResourceType: 'AWS::Lambda::Function', - LastUpdatedTimestamp: '2018-03-12T00:22:34.095Z', - ResourceStatus: 'CREATE_COMPLETE' }, - { LogicalResourceId: 'HelloLambdaPermissionApiGateway', - PhysicalResourceId: 'hello-world-2-dev-HelloLambdaPermissionApiGateway-18KKZXJG1DPF5', - ResourceType: 'AWS::Lambda::Permission', - LastUpdatedTimestamp: '2018-03-12T00:22:46.950Z', - ResourceStatus: 'CREATE_COMPLETE' }, - { LogicalResourceId: 'HelloLambdaVersiongZDaMtQjEhvXacHdpTLnQ61zDCdI2IWVYCbuE50pj8', - PhysicalResourceId: 'arn:aws:lambda:us-east-1:111111111:function:hello-world-2-dev-hello:2', - ResourceType: 'AWS::Lambda::Version', - LastUpdatedTimestamp: '2018-03-12T00:22:37.256Z', - ResourceStatus: 'CREATE_COMPLETE' }, - { LogicalResourceId: 'HelloLogGroup', - PhysicalResourceId: '/aws/lambda/hello-world-2-dev-hello', - ResourceType: 'AWS::Logs::LogGroup', - LastUpdatedTimestamp: '2018-03-12T00:22:20.095Z', - ResourceStatus: 'CREATE_COMPLETE' }, - { LogicalResourceId: 'IamRoleLambdaExecution', - PhysicalResourceId: 'hello-world-2-dev-us-east-1-lambdaRole', - ResourceType: 'AWS::IAM::Role', - LastUpdatedTimestamp: '2018-03-12T00:22:30.995Z', - ResourceStatus: 'CREATE_COMPLETE' }, - { LogicalResourceId: 'ServerlessDeploymentBucket', - PhysicalResourceId: 'hello-world-2-dev-serverlessdeploymentbucket-1e3l68m8zaz7i', - ResourceType: 'AWS::S3::Bucket', - LastUpdatedTimestamp: '2018-03-12T00:22:11.380Z', - ResourceStatus: 'CREATE_COMPLETE' }], + StackResourceSummaries: [ + { + LogicalResourceId: 'ApiGatewayDeployment1520814106863', + PhysicalResourceId: 'eoa2a2', + ResourceType: 'AWS::ApiGateway::Deployment', + LastUpdatedTimestamp: '2018-03-12T00:22:40.680Z', + ResourceStatus: 'CREATE_COMPLETE', + }, + { + LogicalResourceId: 'ApiGatewayMethodHelloGet', + PhysicalResourceId: 'hello-ApiGa-11R27BUE48W38', + ResourceType: 'AWS::ApiGateway::Method', + LastUpdatedTimestamp: '2018-03-12T00:22:37.478Z', + ResourceStatus: 'CREATE_COMPLETE', + }, + { + LogicalResourceId: 'ApiGatewayResourceHello', + PhysicalResourceId: 'az5f7l', + ResourceType: 'AWS::ApiGateway::Resource', + LastUpdatedTimestamp: '2018-03-12T00:22:22.916Z', + ResourceStatus: 'CREATE_COMPLETE', + }, + { + LogicalResourceId: 'ApiGatewayRestApi', + PhysicalResourceId: 'n1uk4p7kl0', + ResourceType: 'AWS::ApiGateway::RestApi', + LastUpdatedTimestamp: '2018-03-12T00:22:19.768Z', + ResourceStatus: 'CREATE_COMPLETE', + }, + { + LogicalResourceId: 'HelloLambdaFunction', + PhysicalResourceId: 'hello-world-2-dev-hello', + ResourceType: 'AWS::Lambda::Function', + LastUpdatedTimestamp: '2018-03-12T00:22:34.095Z', + ResourceStatus: 'CREATE_COMPLETE', + }, + { + LogicalResourceId: 'HelloLambdaPermissionApiGateway', + PhysicalResourceId: 'hello-world-2-dev-HelloLambdaPermissionApiGateway-18KKZXJG1DPF5', + ResourceType: 'AWS::Lambda::Permission', + LastUpdatedTimestamp: '2018-03-12T00:22:46.950Z', + ResourceStatus: 'CREATE_COMPLETE', + }, + { + LogicalResourceId: 'HelloLambdaVersiongZDaMtQjEhvXacHdpTLnQ61zDCdI2IWVYCbuE50pj8', + PhysicalResourceId: + 'arn:aws:lambda:us-east-1:111111111:function:hello-world-2-dev-hello:2', + ResourceType: 'AWS::Lambda::Version', + LastUpdatedTimestamp: '2018-03-12T00:22:37.256Z', + ResourceStatus: 'CREATE_COMPLETE', + }, + { + LogicalResourceId: 'HelloLogGroup', + PhysicalResourceId: '/aws/lambda/hello-world-2-dev-hello', + ResourceType: 'AWS::Logs::LogGroup', + LastUpdatedTimestamp: '2018-03-12T00:22:20.095Z', + ResourceStatus: 'CREATE_COMPLETE', + }, + { + LogicalResourceId: 'IamRoleLambdaExecution', + PhysicalResourceId: 'hello-world-2-dev-us-east-1-lambdaRole', + ResourceType: 'AWS::IAM::Role', + LastUpdatedTimestamp: '2018-03-12T00:22:30.995Z', + ResourceStatus: 'CREATE_COMPLETE', + }, + { + LogicalResourceId: 'ServerlessDeploymentBucket', + PhysicalResourceId: 'hello-world-2-dev-serverlessdeploymentbucket-1e3l68m8zaz7i', + ResourceType: 'AWS::S3::Bucket', + LastUpdatedTimestamp: '2018-03-12T00:22:11.380Z', + ResourceStatus: 'CREATE_COMPLETE', + }, + ], }; listStackResourcesStub.resolves(listStackResourcesResponse); @@ -104,15 +126,13 @@ describe('#getResourceCount()', () => { outputs: [], }; - expect(awsInfo.getResourceCount()).to.be.fulfilled.then(() => { + return expect(awsInfo.getResourceCount()).to.be.fulfilled.then(() => { expect(listStackResourcesStub.calledOnce).to.equal(true); - expect(listStackResourcesStub.calledWithExactly( - 'CloudFormation', - 'listStackResources', - { + expect( + listStackResourcesStub.calledWithExactly('CloudFormation', 'listStackResources', { StackName: awsInfo.provider.naming.getStackName(), - } - )).to.equal(true); + }) + ).to.equal(true); expect(awsInfo.gatheredData.info.resourceCount).to.equal(10); }); diff --git a/lib/plugins/aws/info/getStackInfo.js b/lib/plugins/aws/info/getStackInfo.js index 71f803fbe..4161275a8 100644 --- a/lib/plugins/aws/info/getStackInfo.js +++ b/lib/plugins/aws/info/getStackInfo.js @@ -21,55 +21,58 @@ module.exports = { const stackName = this.provider.naming.getStackName(); // Get info from CloudFormation Outputs - return this.provider.request('CloudFormation', - 'describeStacks', - { StackName: stackName }) - .then((result) => { - let outputs; + return this.provider + .request('CloudFormation', 'describeStacks', { StackName: stackName }) + .then(result => { + let outputs; - if (result) { - outputs = result.Stacks[0].Outputs; + if (result) { + outputs = result.Stacks[0].Outputs; - const serviceEndpointOutputRegex = this.provider.naming - .getServiceEndpointRegex(); + const serviceEndpointOutputRegex = this.provider.naming.getServiceEndpointRegex(); - // Outputs - this.gatheredData.outputs = outputs; + // Outputs + this.gatheredData.outputs = outputs; - // Functions - this.serverless.service.getAllFunctions().forEach((func) => { - const functionInfo = {}; - functionInfo.name = func; - functionInfo.deployedName = this.serverless.service.getFunction(func).name; - this.gatheredData.info.functions.push(functionInfo); - }); - - // Layers - this.serverless.service.getAllLayers().forEach((layer) => { - const layerInfo = {}; - layerInfo.name = layer; - const layerOutputId = this.provider.naming.getLambdaLayerOutputLogicalId(layer); - for (const output of outputs) { - if (output.OutputKey === layerOutputId) { - layerInfo.arn = output.OutputValue; - break; - } - } - this.gatheredData.info.layers.push(layerInfo); - }); - - // Endpoints - outputs.filter(x => x.OutputKey.match(serviceEndpointOutputRegex)) - .forEach(x => { - this.gatheredData.info.endpoints.push(x.OutputValue); - if (this.serverless.service.deployment && - this.serverless.service.deployment.deploymentId) { - this.serverless.service.deployment.apiId = x.OutputValue.split('//')[1].split('.')[0]; - } + // Functions + this.serverless.service.getAllFunctions().forEach(func => { + const functionInfo = {}; + functionInfo.name = func; + functionInfo.deployedName = this.serverless.service.getFunction(func).name; + this.gatheredData.info.functions.push(functionInfo); }); - } - return BbPromise.resolve(); - }); + // Layers + this.serverless.service.getAllLayers().forEach(layer => { + const layerInfo = {}; + layerInfo.name = layer; + const layerOutputId = this.provider.naming.getLambdaLayerOutputLogicalId(layer); + for (const output of outputs) { + if (output.OutputKey === layerOutputId) { + layerInfo.arn = output.OutputValue; + break; + } + } + this.gatheredData.info.layers.push(layerInfo); + }); + + // Endpoints + outputs + .filter(x => x.OutputKey.match(serviceEndpointOutputRegex)) + .forEach(x => { + this.gatheredData.info.endpoints.push(x.OutputValue); + if ( + this.serverless.service.deployment && + this.serverless.service.deployment.deploymentId + ) { + this.serverless.service.deployment.apiId = x.OutputValue.split('//')[1].split( + '.' + )[0]; + } + }); + } + + return BbPromise.resolve(); + }); }, }; diff --git a/lib/plugins/aws/info/getStackInfo.test.js b/lib/plugins/aws/info/getStackInfo.test.js index 096e71ea5..7629879c1 100644 --- a/lib/plugins/aws/info/getStackInfo.test.js +++ b/lib/plugins/aws/info/getStackInfo.test.js @@ -37,9 +37,11 @@ describe('#getStackInfo()', () => { const describeStacksResponse = { Stacks: [ { - StackId: 'arn:aws:cloudformation:us-east-1:123456789012:' + + StackId: + 'arn:aws:cloudformation:us-east-1:123456789012:' + 'stack/myteststack/466df9e0-0dff-08e3-8e2f-5088487c4896', - Description: 'AWS CloudFormation Sample Template S3_Bucket: ' + + Description: + 'AWS CloudFormation Sample Template S3_Bucket: ' + 'Sample template showing how to create a publicly accessible S3 bucket.', Tags: [], Outputs: [ @@ -126,13 +128,11 @@ describe('#getStackInfo()', () => { return awsInfo.getStackInfo().then(() => { expect(describeStacksStub.calledOnce).to.equal(true); - expect(describeStacksStub.calledWithExactly( - 'CloudFormation', - 'describeStacks', - { + expect( + describeStacksStub.calledWithExactly('CloudFormation', 'describeStacks', { StackName: awsInfo.provider.naming.getStackName(), - } - )).to.equal(true); + }) + ).to.equal(true); expect(awsInfo.gatheredData).to.deep.equal(expectedGatheredDataObj); }); @@ -158,13 +158,11 @@ describe('#getStackInfo()', () => { return awsInfo.getStackInfo().then(() => { expect(describeStacksStub.calledOnce).to.equal(true); - expect(describeStacksStub.calledWithExactly( - 'CloudFormation', - 'describeStacks', - { + expect( + describeStacksStub.calledWithExactly('CloudFormation', 'describeStacks', { StackName: awsInfo.provider.naming.getStackName(), - } - )).to.equal(true); + }) + ).to.equal(true); expect(awsInfo.gatheredData).to.deep.equal(expectedGatheredDataObj); }); diff --git a/lib/plugins/aws/info/index.js b/lib/plugins/aws/info/index.js index 09e7887ba..5f8625118 100644 --- a/lib/plugins/aws/info/index.js +++ b/lib/plugins/aws/info/index.js @@ -12,14 +12,7 @@ class AwsInfo { this.serverless = serverless; this.provider = this.serverless.getProvider('aws'); this.options = options || {}; - Object.assign( - this, - validate, - getStackInfo, - getResourceCount, - getApiKeyValues, - display - ); + Object.assign(this, validate, getStackInfo, getResourceCount, getApiKeyValues, display); this.commands = { aws: { @@ -44,39 +37,33 @@ class AwsInfo { this.hooks = { 'info:info': () => this.serverless.pluginManager.spawn('aws:info'), - 'deploy:deploy': () => BbPromise.bind(this) - .then(() => { + 'deploy:deploy': () => + BbPromise.bind(this).then(() => { if (this.options.noDeploy) { return BbPromise.resolve(); } return this.serverless.pluginManager.spawn('aws:info'); }), - 'aws:info:validate': () => BbPromise.bind(this) - .then(this.validate), + 'aws:info:validate': () => BbPromise.bind(this).then(this.validate), - 'aws:info:gatherData': () => BbPromise.bind(this) - .then(this.getStackInfo) - .then(this.getResourceCount) - .then(this.getApiKeyValues), + 'aws:info:gatherData': () => + BbPromise.bind(this) + .then(this.getStackInfo) + .then(this.getResourceCount) + .then(this.getApiKeyValues), - 'aws:info:displayServiceInfo': () => BbPromise.bind(this) - .then(this.displayServiceInfo), + 'aws:info:displayServiceInfo': () => BbPromise.bind(this).then(this.displayServiceInfo), - 'aws:info:displayApiKeys': () => BbPromise.bind(this) - .then(this.displayApiKeys), + 'aws:info:displayApiKeys': () => BbPromise.bind(this).then(this.displayApiKeys), - 'aws:info:displayEndpoints': () => BbPromise.bind(this) - .then(this.displayEndpoints), + 'aws:info:displayEndpoints': () => BbPromise.bind(this).then(this.displayEndpoints), - 'aws:info:displayFunctions': () => BbPromise.bind(this) - .then(this.displayFunctions), + 'aws:info:displayFunctions': () => BbPromise.bind(this).then(this.displayFunctions), - 'aws:info:displayLayers': () => BbPromise.bind(this) - .then(this.displayLayers), + 'aws:info:displayLayers': () => BbPromise.bind(this).then(this.displayLayers), - 'aws:info:displayStackOutputs': () => BbPromise.bind(this) - .then(this.displayStackOutputs), + 'aws:info:displayStackOutputs': () => BbPromise.bind(this).then(this.displayStackOutputs), }; } } diff --git a/lib/plugins/aws/info/index.test.js b/lib/plugins/aws/info/index.test.js index aeac8bd23..4dc56ae98 100644 --- a/lib/plugins/aws/info/index.test.js +++ b/lib/plugins/aws/info/index.test.js @@ -34,26 +34,16 @@ describe('AwsInfo', () => { // Load commands and hooks into pluginManager serverless.pluginManager.loadCommands(awsInfo); serverless.pluginManager.loadHooks(awsInfo); - validateStub = sinon - .stub(awsInfo, 'validate').resolves(); - getStackInfoStub = sinon - .stub(awsInfo, 'getStackInfo').resolves(); - getResourceCountStub = sinon - .stub(awsInfo, 'getResourceCount').resolves(); - getApiKeyValuesStub = sinon - .stub(awsInfo, 'getApiKeyValues').resolves(); - displayServiceInfoStub = sinon - .stub(awsInfo, 'displayServiceInfo').resolves(); - displayApiKeysStub = sinon - .stub(awsInfo, 'displayApiKeys').resolves(); - displayEndpointsStub = sinon - .stub(awsInfo, 'displayEndpoints').resolves(); - displayFunctionsStub = sinon - .stub(awsInfo, 'displayFunctions').resolves(); - displayLayersStub = sinon - .stub(awsInfo, 'displayLayers').resolves(); - displayStackOutputsStub = sinon - .stub(awsInfo, 'displayStackOutputs').resolves(); + validateStub = sinon.stub(awsInfo, 'validate').resolves(); + getStackInfoStub = sinon.stub(awsInfo, 'getStackInfo').resolves(); + getResourceCountStub = sinon.stub(awsInfo, 'getResourceCount').resolves(); + getApiKeyValuesStub = sinon.stub(awsInfo, 'getApiKeyValues').resolves(); + displayServiceInfoStub = sinon.stub(awsInfo, 'displayServiceInfo').resolves(); + displayApiKeysStub = sinon.stub(awsInfo, 'displayApiKeys').resolves(); + displayEndpointsStub = sinon.stub(awsInfo, 'displayEndpoints').resolves(); + displayFunctionsStub = sinon.stub(awsInfo, 'displayFunctions').resolves(); + displayLayersStub = sinon.stub(awsInfo, 'displayLayers').resolves(); + displayStackOutputsStub = sinon.stub(awsInfo, 'displayStackOutputs').resolves(); }); afterEach(() => { @@ -91,8 +81,7 @@ describe('AwsInfo', () => { expect(displayFunctionsStub.calledAfter(getApiKeyValuesStub)).to.equal(true); expect(displayLayersStub.calledAfter(getApiKeyValuesStub)).to.equal(true); expect(displayStackOutputsStub.calledAfter(getApiKeyValuesStub)).to.equal(true); - }) - ); + })); describe('when running "deploy:deploy" hook', () => { it('should run promise chain in order if no deploy is not set', () => @@ -106,8 +95,7 @@ describe('AwsInfo', () => { expect(displayFunctionsStub.calledAfter(getApiKeyValuesStub)).to.equal(true); expect(displayLayersStub.calledAfter(getApiKeyValuesStub)).to.equal(true); expect(displayStackOutputsStub.calledAfter(getApiKeyValuesStub)).to.equal(true); - }) - ); + })); }); }); }); diff --git a/lib/plugins/aws/invoke/index.js b/lib/plugins/aws/invoke/index.js index 2354a1d28..4bfef7ed1 100644 --- a/lib/plugins/aws/invoke/index.js +++ b/lib/plugins/aws/invoke/index.js @@ -16,51 +16,49 @@ class AwsInvoke { Object.assign(this, validate); this.hooks = { - 'invoke:invoke': () => BbPromise.bind(this) - .then(this.extendedValidate) - .then(this.invoke) - .then(this.log), + 'invoke:invoke': () => + BbPromise.bind(this) + .then(this.extendedValidate) + .then(this.invoke) + .then(this.log), }; } extendedValidate() { - this.validate(); + return this.validate().then(() => { + // validate function exists in service + this.options.functionObj = this.serverless.service.getFunction(this.options.function); + this.options.data = this.options.data || ''; - // validate function exists in service - this.options.functionObj = this.serverless.service.getFunction(this.options.function); - this.options.data = this.options.data || ''; - - return new BbPromise(resolve => { - if (this.options.data) { - resolve(); - } else if (this.options.path) { - const absolutePath = path.isAbsolute(this.options.path) ? - this.options.path : - path.join(this.serverless.config.servicePath, this.options.path); - if (!this.serverless.utils.fileExistsSync(absolutePath)) { - throw new this.serverless.classes.Error('The file you provided does not exist.'); + return new BbPromise(resolve => { + if (this.options.data) { + return resolve(); + } else if (this.options.path) { + const absolutePath = path.isAbsolute(this.options.path) + ? this.options.path + : path.join(this.serverless.config.servicePath, this.options.path); + if (!this.serverless.utils.fileExistsSync(absolutePath)) { + throw new this.serverless.classes.Error('The file you provided does not exist.'); + } + this.options.data = this.serverless.utils.readFileSync(absolutePath); + return resolve(); } - this.options.data = this.serverless.utils.readFileSync(absolutePath); - resolve(); - } else { - try { - stdin().then(input => { + + return stdin() + .then(input => { this.options.data = input; - resolve(); - }); + return resolve(); + }) + .catch(() => resolve()); + }).then(() => { + try { + if (!this.options.raw) { + this.options.data = JSON.parse(this.options.data); + } } catch (exception) { - // resolve if no stdin was provided - resolve(); + // do nothing if it's a simple string or object already } - } - }).then(() => { - try { - if (!this.options.raw) { - this.options.data = JSON.parse(this.options.data); - } - } catch (exception) { - // do nothing if it's a simple string or object already - } + }); }); } @@ -83,7 +81,7 @@ class AwsInvoke { } log(invocationReply) { - const color = !invocationReply.FunctionError ? (x => x) : chalk.red; + const color = !invocationReply.FunctionError ? x => x : chalk.red; if (invocationReply.Payload) { const response = JSON.parse(invocationReply.Payload); @@ -92,8 +90,9 @@ class AwsInvoke { } if (invocationReply.LogResult) { - this.consoleLog(chalk - .gray('--------------------------------------------------------------------')); + this.consoleLog( + chalk.gray('--------------------------------------------------------------------') + ); const logResult = new Buffer(invocationReply.LogResult, 'base64').toString(); logResult.split('\n').forEach(line => this.consoleLog(formatLambdaLogEvent(line))); } diff --git a/lib/plugins/aws/invoke/index.test.js b/lib/plugins/aws/invoke/index.test.js index 915911695..08d7febeb 100644 --- a/lib/plugins/aws/invoke/index.test.js +++ b/lib/plugins/aws/invoke/index.test.js @@ -1,36 +1,48 @@ 'use strict'; -const expect = require('chai').expect; +const chai = require('chai'); const sinon = require('sinon'); const path = require('path'); -const AwsInvoke = require('./index'); +const proxyquire = require('proxyquire'); const AwsProvider = require('../provider/awsProvider'); const Serverless = require('../../../Serverless'); -const testUtils = require('../../../../tests/utils'); +const { getTmpDirPath } = require('../../../../tests/utils/fs'); + +chai.use(require('chai-as-promised')); + +const expect = chai.expect; describe('AwsInvoke', () => { + let AwsInvoke; + let awsInvoke; + let serverless; + let stdinStub; const options = { stage: 'dev', region: 'us-east-1', function: 'first', }; - const serverless = new Serverless(); - serverless.setProvider('aws', new AwsProvider(serverless, options)); - const awsInvoke = new AwsInvoke(serverless, options); + + beforeEach(() => { + stdinStub = sinon.stub().resolves(''); + AwsInvoke = proxyquire('./index', { + 'get-stdin': stdinStub, + }); + serverless = new Serverless(); + serverless.setProvider('aws', new AwsProvider(serverless, options)); + awsInvoke = new AwsInvoke(serverless, options); + }); describe('#constructor()', () => { it('should have hooks', () => expect(awsInvoke.hooks).to.be.not.empty); - it('should set the provider variable to an instance of AwsProvider', - () => expect(awsInvoke.provider).to.be.instanceof(AwsProvider)); + it('should set the provider variable to an instance of AwsProvider', () => + expect(awsInvoke.provider).to.be.instanceof(AwsProvider)); it('should run promise chain in order', () => { - const validateStub = sinon - .stub(awsInvoke, 'extendedValidate').resolves(); - const invokeStub = sinon - .stub(awsInvoke, 'invoke').resolves(); - const logStub = sinon - .stub(awsInvoke, 'log').resolves(); + const validateStub = sinon.stub(awsInvoke, 'extendedValidate').resolves(); + const invokeStub = sinon.stub(awsInvoke, 'invoke').resolves(); + const logStub = sinon.stub(awsInvoke, 'log').resolves(); return awsInvoke.hooks['invoke:invoke']().then(() => { expect(validateStub.calledOnce).to.be.equal(true); @@ -51,6 +63,7 @@ describe('AwsInvoke', () => { }); describe('#extendedValidate()', () => { + let backupIsTTY; beforeEach(() => { serverless.config.servicePath = true; serverless.service.environment = { @@ -73,17 +86,26 @@ describe('AwsInvoke', () => { }; awsInvoke.options.data = null; awsInvoke.options.path = false; + + // Ensure there's no attempt to read path from stdin + backupIsTTY = process.stdin.isTTY; + process.stdin.isTTY = true; + }); + + afterEach(() => { + if (backupIsTTY) process.stdin.isTTY = backupIsTTY; + delete process.stdin.isTTY; }); it('it should throw error if function is not provided', () => { serverless.service.functions = null; - expect(() => awsInvoke.extendedValidate()).to.throw(Error); + return expect(awsInvoke.extendedValidate()).to.be.rejected; }); - it('should not throw error when there are no input data', () => { + it('should not throw error when there is no input data', () => { awsInvoke.options.data = undefined; - return awsInvoke.extendedValidate().then(() => { + return expect(awsInvoke.extendedValidate()).to.be.fulfilled.then(() => { expect(awsInvoke.options.data).to.equal(''); }); }); @@ -91,7 +113,7 @@ describe('AwsInvoke', () => { it('should keep data if it is a simple string', () => { awsInvoke.options.data = 'simple-string'; - return awsInvoke.extendedValidate().then(() => { + return expect(awsInvoke.extendedValidate()).to.be.fulfilled.then(() => { expect(awsInvoke.options.data).to.equal('simple-string'); }); }); @@ -99,7 +121,7 @@ describe('AwsInvoke', () => { it('should parse data if it is a json string', () => { awsInvoke.options.data = '{"key": "value"}'; - return awsInvoke.extendedValidate().then(() => { + return expect(awsInvoke.extendedValidate()).to.be.fulfilled.then(() => { expect(awsInvoke.options.data).to.deep.equal({ key: 'value' }); }); }); @@ -108,27 +130,29 @@ describe('AwsInvoke', () => { awsInvoke.options.data = '{"key": "value"}'; awsInvoke.options.raw = true; - return awsInvoke.extendedValidate().then(() => { + return expect(awsInvoke.extendedValidate()).to.be.fulfilled.then(() => { expect(awsInvoke.options.data).to.deep.equal('{"key": "value"}'); }); }); it('it should parse file if relative file path is provided', () => { - serverless.config.servicePath = testUtils.getTmpDirPath(); + serverless.config.servicePath = getTmpDirPath(); const data = { testProp: 'testValue', }; - serverless.utils.writeFileSync(path - .join(serverless.config.servicePath, 'data.json'), JSON.stringify(data)); + serverless.utils.writeFileSync( + path.join(serverless.config.servicePath, 'data.json'), + JSON.stringify(data) + ); awsInvoke.options.path = 'data.json'; - return awsInvoke.extendedValidate().then(() => { + return expect(awsInvoke.extendedValidate()).to.be.fulfilled.then(() => { expect(awsInvoke.options.data).to.deep.equal(data); }); }); it('it should parse file if absolute file path is provided', () => { - serverless.config.servicePath = testUtils.getTmpDirPath(); + serverless.config.servicePath = getTmpDirPath(); const data = { testProp: 'testValue', }; @@ -136,20 +160,22 @@ describe('AwsInvoke', () => { serverless.utils.writeFileSync(dataFile, JSON.stringify(data)); awsInvoke.options.path = dataFile; - return awsInvoke.extendedValidate().then(() => { + return expect(awsInvoke.extendedValidate()).to.be.fulfilled.then(() => { expect(awsInvoke.options.data).to.deep.equal(data); }); }); it('it should parse a yaml file if file path is provided', () => { - serverless.config.servicePath = testUtils.getTmpDirPath(); + serverless.config.servicePath = getTmpDirPath(); const yamlContent = 'testProp: testValue'; - serverless.utils.writeFileSync(path - .join(serverless.config.servicePath, 'data.yml'), yamlContent); + serverless.utils.writeFileSync( + path.join(serverless.config.servicePath, 'data.yml'), + yamlContent + ); awsInvoke.options.path = 'data.yml'; - return awsInvoke.extendedValidate().then(() => { + return expect(awsInvoke.extendedValidate()).to.be.fulfilled.then(() => { expect(awsInvoke.options.data).to.deep.equal({ testProp: 'testValue', }); @@ -158,23 +184,21 @@ describe('AwsInvoke', () => { it('it should throw error if service path is not set', () => { serverless.config.servicePath = false; - expect(() => awsInvoke.extendedValidate()).to.throw(Error); + return expect(awsInvoke.extendedValidate()).to.be.rejected; }); it('it should throw error if file path does not exist', () => { - serverless.config.servicePath = testUtils.getTmpDirPath(); + serverless.config.servicePath = getTmpDirPath(); awsInvoke.options.path = 'some/path'; - return awsInvoke.extendedValidate().catch((err) => { - expect(err).to.be.an.instanceOf(Error); - expect(err.message).to.equal('The file you provided does not exist.'); - }); + return expect(awsInvoke.extendedValidate()).to.be.rejectedWith( + 'The file you provided does not exist.' + ); }); - it('should resolve if path is not given', (done) => { + it('should resolve if path is not given', () => { awsInvoke.options.path = false; - - awsInvoke.extendedValidate().then(() => done()); + return expect(awsInvoke.extendedValidate()).to.be.fulfilled; }); }); @@ -192,38 +216,33 @@ describe('AwsInvoke', () => { }; }); - it('should invoke with correct params', () => awsInvoke.invoke() - .then(() => { + it('should invoke with correct params', () => + awsInvoke.invoke().then(() => { expect(invokeStub.calledOnce).to.be.equal(true); - expect(invokeStub.calledWithExactly( - 'Lambda', - 'invoke', - { + expect( + invokeStub.calledWithExactly('Lambda', 'invoke', { FunctionName: 'customName', InvocationType: 'RequestResponse', LogType: 'None', Payload: new Buffer(JSON.stringify({})), - } - )).to.be.equal(true); + }) + ).to.be.equal(true); awsInvoke.provider.request.restore(); - }) - ); + })); it('should invoke and log', () => { awsInvoke.options.log = true; return awsInvoke.invoke().then(() => { expect(invokeStub.calledOnce).to.be.equal(true); - expect(invokeStub.calledWithExactly( - 'Lambda', - 'invoke', - { + expect( + invokeStub.calledWithExactly('Lambda', 'invoke', { FunctionName: 'customName', InvocationType: 'RequestResponse', LogType: 'Tail', Payload: new Buffer(JSON.stringify({})), - } - )).to.be.equal(true); + }) + ).to.be.equal(true); awsInvoke.provider.request.restore(); }); }); @@ -233,16 +252,14 @@ describe('AwsInvoke', () => { return awsInvoke.invoke().then(() => { expect(invokeStub.calledOnce).to.be.equal(true); - expect(invokeStub.calledWithExactly( - 'Lambda', - 'invoke', - { + expect( + invokeStub.calledWithExactly('Lambda', 'invoke', { FunctionName: 'customName', InvocationType: 'OtherType', LogType: 'None', Payload: new Buffer(JSON.stringify({})), - } - )).to.be.equal(true); + }) + ).to.be.equal(true); awsInvoke.provider.request.restore(); }); }); @@ -288,8 +305,8 @@ describe('AwsInvoke', () => { }; return awsInvoke.log(invocationReplyMock).catch(err => { - expect(err).to - .and.be.instanceof(Error) + expect(err) + .to.and.be.instanceof(Error) .and.have.property('message', 'Invoked function failed'); }); }); diff --git a/lib/plugins/aws/invokeLocal/fixture/handler.py b/lib/plugins/aws/invokeLocal/fixture/handler.py index 3a36db642..13ffcf9f9 100644 --- a/lib/plugins/aws/invokeLocal/fixture/handler.py +++ b/lib/plugins/aws/invokeLocal/fixture/handler.py @@ -1,5 +1,6 @@ from time import sleep + def withRemainingTime(event, context): start = context.get_remaining_time_in_millis() sleep(0.001) @@ -8,4 +9,4 @@ def withRemainingTime(event, context): return { "start": start, "stop": stop - } \ No newline at end of file + } diff --git a/lib/plugins/aws/invokeLocal/index.js b/lib/plugins/aws/invokeLocal/index.js index cc75d1dad..f7fdcfd5e 100644 --- a/lib/plugins/aws/invokeLocal/index.js +++ b/lib/plugins/aws/invokeLocal/index.js @@ -27,24 +27,24 @@ class AwsInvokeLocal { Object.assign(this, validate); this.hooks = { - 'before:invoke:local:loadEnvVars': () => BbPromise.bind(this) - .then(this.extendedValidate) - .then(this.loadEnvVars), - 'invoke:local:invoke': () => BbPromise.bind(this) - .then(this.invokeLocal), + 'before:invoke:local:loadEnvVars': () => + BbPromise.bind(this) + .then(this.extendedValidate) + .then(this.loadEnvVars), + 'invoke:local:invoke': () => BbPromise.bind(this).then(this.invokeLocal), }; } getRuntime() { - return this.options.functionObj.runtime - || this.serverless.service.provider.runtime - || 'nodejs4.3'; + return ( + this.options.functionObj.runtime || this.serverless.service.provider.runtime || 'nodejs10.x' + ); } validateFile(filePath, key) { - const absolutePath = path.isAbsolute(filePath) ? - filePath : - path.join(this.serverless.config.servicePath, filePath); + const absolutePath = path.isAbsolute(filePath) + ? filePath + : path.join(this.serverless.config.servicePath, filePath); if (!this.serverless.utils.fileExistsSync(absolutePath)) { throw new this.serverless.classes.Error('The file you provided does not exist.'); } @@ -58,48 +58,44 @@ class AwsInvokeLocal { } extendedValidate() { - this.validate(); + return this.validate().then(() => { + // validate function exists in service + this.options.functionObj = this.serverless.service.getFunction(this.options.function); + this.options.data = this.options.data || ''; - // validate function exists in service - this.options.functionObj = this.serverless.service.getFunction(this.options.function); - this.options.data = this.options.data || ''; + return new BbPromise(resolve => { + if (this.options.contextPath) { + this.validateFile(this.options.contextPath, 'context'); + } - return new BbPromise(resolve => { - if (this.options.contextPath) { - this.validateFile(this.options.contextPath, 'context'); - } + if (this.options.data) { + return resolve(); + } else if (this.options.path) { + this.validateFile(this.options.path, 'data'); + return resolve(); + } - if (this.options.data) { - resolve(); - } else if (this.options.path) { - this.validateFile(this.options.path, 'data'); - - resolve(); - } else { - try { - stdin().then(input => { + return stdin() + .then(input => { this.options.data = input; - resolve(); - }); + return resolve(); + }) + .catch(() => resolve()); + }).then(() => { + try { + // unless asked to preserve raw input, attempt to parse any provided objects + if (!this.options.raw) { + if (this.options.data) { + this.options.data = JSON.parse(this.options.data); + } + if (this.options.context) { + this.options.context = JSON.parse(this.options.context); + } + } } catch (exception) { - // resolve if no stdin was provided - resolve(); + // do nothing if it's a simple string or object already } - } - }).then(() => { - try { - // unless asked to preserve raw input, attempt to parse any provided objects - if (!this.options.raw) { - if (this.options.data) { - this.options.data = JSON.parse(this.options.data); - } - if (this.options.context) { - this.options.context = JSON.parse(this.options.context); - } - } - } catch (exception) { - // do nothing if it's a simple string or object already - } + }); }); } @@ -111,13 +107,15 @@ class AwsInvokeLocal { loadEnvVars() { const lambdaName = this.options.functionObj.name; - const memorySize = Number(this.options.functionObj.memorySize) - || Number(this.serverless.service.provider.memorySize) - || 1024; + const memorySize = + Number(this.options.functionObj.memorySize) || + Number(this.serverless.service.provider.memorySize) || + 1024; const lambdaDefaultEnvVars = { LANG: 'en_US.UTF-8', - LD_LIBRARY_PATH: '/usr/local/lib64/node-v4.3.x/lib:/lib64:/usr/lib64:/var/runtime:/var/runtime/lib:/var/task:/var/task/lib', // eslint-disable-line max-len + LD_LIBRARY_PATH: + '/usr/local/lib64/node-v4.3.x/lib:/lib64:/usr/lib64:/var/runtime:/var/runtime/lib:/var/task:/var/task/lib', // eslint-disable-line max-len LAMBDA_TASK_ROOT: '/var/task', LAMBDA_RUNTIME_DIR: '/var/runtime', AWS_REGION: this.provider.getRegion(), @@ -158,7 +156,8 @@ class AwsInvokeLocal { handlerPath, handlerName, this.options.data, - this.options.context); + this.options.context + ); } if (_.includes(['python2.7', 'python3.6', 'python3.7'], runtime)) { @@ -170,7 +169,8 @@ class AwsInvokeLocal { handlerPath, handlerName, this.options.data, - this.options.context); + this.options.context + ); } if (runtime === 'java8') { @@ -182,7 +182,8 @@ class AwsInvokeLocal { handlerName, this.serverless.service.package.artifact, this.options.data, - this.options.context); + this.options.context + ); } if (runtime === 'ruby2.5') { @@ -194,7 +195,8 @@ class AwsInvokeLocal { handlerPath, handlerName, this.options.data, - this.options.context); + this.options.context + ); } return this.invokeLocalDocker(); @@ -218,7 +220,9 @@ class AwsInvokeLocal { return new BbPromise((resolve, reject) => { const docker = spawn('docker', ['images', '-q', `lambci/lambda:${runtime}`]); let stdout = ''; - docker.stdout.on('data', (buf) => { stdout += buf.toString(); }); + docker.stdout.on('data', buf => { + stdout += buf.toString(); + }); docker.on('exit', error => (error ? reject(error) : resolve(Boolean(stdout.trim())))); }); } @@ -235,24 +239,21 @@ class AwsInvokeLocal { } getLayerPaths() { - const layers = _.mapKeys( - this.serverless.service.layers, - (value, key) => this.provider.naming.getLambdaLayerLogicalId(key) + const layers = _.mapKeys(this.serverless.service.layers, (value, key) => + this.provider.naming.getLambdaLayerLogicalId(key) ); return BbPromise.all( - (this.options.functionObj.layers || this.serverless.service.provider.layers || []) - .map(layer => { + (this.options.functionObj.layers || this.serverless.service.provider.layers || []).map( + layer => { if (layer.Ref) { return layers[layer.Ref].path; } const arnParts = layer.split(':'); const layerArn = arnParts.slice(0, -1).join(':'); const layerVersion = Number(arnParts.slice(-1)[0]); - const layerContentsPath = path.join( - '.serverless', 'layers', arnParts[6], arnParts[7]); - const layerContentsCachePath = path.join( - cachePath, 'layers', arnParts[6], arnParts[7]); + const layerContentsPath = path.join('.serverless', 'layers', arnParts[6], arnParts[7]); + const layerContentsCachePath = path.join(cachePath, 'layers', arnParts[6], arnParts[7]); if (fs.existsSync(layerContentsPath)) { return layerContentsPath; } @@ -260,23 +261,25 @@ class AwsInvokeLocal { if (!fs.existsSync(layerContentsCachePath)) { this.serverless.cli.log(`Downloading layer ${layer}...`); mkdirp.sync(path.join(layerContentsCachePath)); - downloadPromise = this.provider.request( - 'Lambda', 'getLayerVersion', { LayerName: layerArn, VersionNumber: layerVersion }) - .then(layerInfo => download( - layerInfo.Content.Location, - layerContentsCachePath, - { extract: true })); + downloadPromise = this.provider + .request('Lambda', 'getLayerVersion', { + LayerName: layerArn, + VersionNumber: layerVersion, + }) + .then(layerInfo => + download(layerInfo.Content.Location, layerContentsCachePath, { extract: true }) + ); } return downloadPromise .then(() => fse.copySync(layerContentsCachePath, layerContentsPath)) .then(() => layerContentsPath); - })); + } + ) + ); } buildDockerImage(layerPaths) { const runtime = this.getRuntime(); - - const imageName = 'sls-docker'; return new BbPromise((resolve, reject) => { @@ -288,43 +291,60 @@ class AwsInvokeLocal { const dockerfilePath = path.join('.serverless', 'invokeLocal', 'Dockerfile'); fs.writeFileSync(dockerfilePath, dockerfile); this.serverless.cli.log('Building Docker image...'); - const docker = spawn('docker', ['build', '-t', imageName, - `${this.serverless.config.servicePath}`, '-f', dockerfilePath]); + const docker = spawn('docker', [ + 'build', + '-t', + imageName, + `${this.serverless.config.servicePath}`, + '-f', + dockerfilePath, + ]); docker.on('exit', error => (error ? reject(error) : resolve(imageName))); }); } extractArtifact() { - const artifact = _.get(this.options.functionObj, 'package.artifact', _.get( - this.serverless.service, 'package.artifact' - )); + const artifact = _.get( + this.options.functionObj, + 'package.artifact', + _.get(this.serverless.service, 'package.artifact') + ); if (!artifact) { return this.serverless.config.servicePath; } - return fs.readFileAsync(artifact) + return fs + .readFileAsync(artifact) .then(jszip.loadAsync) - .then(zip => BbPromise.all( - Object.keys(zip.files) - .map(filename => zip.files[filename].async('nodebuffer').then(fileData => { - if (filename.endsWith(path.sep)) { - return BbPromise.resolve(); - } - mkdirp.sync(path.join( - '.serverless', 'invokeLocal', 'artifact', path.dirname(filename))); - return fs.writeFileAsync(path.join( - '.serverless', 'invokeLocal', 'artifact', filename), fileData, { - mode: zip.files[filename].unixPermissions, - }); - })))) - .then(() => path.join( - this.serverless.config.servicePath, '.serverless', 'invokeLocal', 'artifact')); + .then(zip => + BbPromise.all( + Object.keys(zip.files).map(filename => + zip.files[filename].async('nodebuffer').then(fileData => { + if (filename.endsWith(path.sep)) { + return BbPromise.resolve(); + } + mkdirp.sync( + path.join('.serverless', 'invokeLocal', 'artifact', path.dirname(filename)) + ); + return fs.writeFileAsync( + path.join('.serverless', 'invokeLocal', 'artifact', filename), + fileData, + { + mode: zip.files[filename].unixPermissions, + } + ); + }) + ) + ) + ) + .then(() => + path.join(this.serverless.config.servicePath, '.serverless', 'invokeLocal', 'artifact') + ); } getEnvVarsFromOptions() { const envVarsFromOptions = {}; // Get the env vars from command line options in the form of --env KEY=value - _.concat(this.options.env || []) - .forEach(itm => { + _.concat(this.options.env || []).forEach(itm => { const splitItm = _.split(itm, '='); envVarsFromOptions[splitItm[0]] = splitItm.slice(1, splitItm.length).join('=') || ''; }); @@ -340,33 +360,37 @@ class AwsInvokeLocal { this.checkDockerImage().then(exists => (exists ? {} : this.pullDockerImage())), this.getLayerPaths().then(layerPaths => this.buildDockerImage(layerPaths)), this.extractArtifact(), - ]) - .then((results) => new BbPromise((resolve, reject) => { - const imageName = results[2]; - const artifactPath = results[3]; - const configuredEnvVars = this.getConfiguredEnvVars(); - const envVarsFromOptions = this.getEnvVarsFromOptions(); - const envVars = _.merge(configuredEnvVars, envVarsFromOptions); - const envVarsDockerArgs = _.flatMap(envVars, - (value, key) => ['--env', `${key}=${value}`] - ); - const dockerArgsFromOptions = this.getDockerArgsFromOptions(); - const dockerArgs = _.concat([ - 'run', '--rm', '-v', `${artifactPath}:/var/task`, - ], envVarsDockerArgs, dockerArgsFromOptions, [ - imageName, handler, JSON.stringify(this.options.data), - ]); - const docker = spawn('docker', dockerArgs); - docker.stdout.on('data', (buf) => this.serverless.cli.consoleLog(buf.toString())); - docker.stderr.on('data', (buf) => this.serverless.cli.consoleLog(buf.toString())); - docker.on('exit', error => (error ? reject(error) : resolve(imageName))); - })) + ]).then( + results => + new BbPromise((resolve, reject) => { + const imageName = results[2]; + const artifactPath = results[3]; + const configuredEnvVars = this.getConfiguredEnvVars(); + const envVarsFromOptions = this.getEnvVarsFromOptions(); + const envVars = _.merge(configuredEnvVars, envVarsFromOptions); + const envVarsDockerArgs = _.flatMap(envVars, (value, key) => [ + '--env', + `${key}=${value}`, + ]); + const dockerArgsFromOptions = this.getDockerArgsFromOptions(); + const dockerArgs = _.concat( + ['run', '--rm', '-v', `${artifactPath}:/var/task`], + envVarsDockerArgs, + dockerArgsFromOptions, + [imageName, handler, JSON.stringify(this.options.data)] + ); + const docker = spawn('docker', dockerArgs); + docker.stdout.on('data', buf => this.serverless.cli.consoleLog(buf.toString())); + docker.stderr.on('data', buf => this.serverless.cli.consoleLog(buf.toString())); + docker.on('exit', error => (error ? reject(error) : resolve(imageName))); + }) + ) ); } getDockerArgsFromOptions() { const dockerArgOptions = this.options['docker-arg']; - const dockerArgsFromOptions = _.flatMap(_.concat(dockerArgOptions || []), (dockerArgOption) => { + const dockerArgsFromOptions = _.flatMap(_.concat(dockerArgOptions || []), dockerArgOption => { const splitItems = dockerArgOption.split(' '); return [splitItems[0], splitItems.slice(1).join(' ')]; }); @@ -381,9 +405,10 @@ class AwsInvokeLocal { name: this.options.functionObj.name, version: 'LATEST', logGroupName: this.provider.naming.getLogGroupName(this.options.functionObj.name), - timeout: Number(this.options.functionObj.timeout) - || Number(this.serverless.service.provider.timeout) - || 6, + timeout: + Number(this.options.functionObj.timeout) || + Number(this.serverless.service.provider.timeout) || + 6, }, context ), @@ -398,47 +423,80 @@ class AwsInvokeLocal { ].join(''); } - return new BbPromise(resolve => { - const python = spawn(runtime.split('.')[0], + return new BbPromise((resolve, reject) => { + const python = spawn( + runtime.split('.')[0], ['-u', path.join(__dirname, 'invoke.py'), handlerPath, handlerName], - { env: process.env }, { shell: true }); - python.stdout.on('data', (buf) => this.serverless.cli.consoleLog(buf.toString())); - python.stderr.on('data', (buf) => this.serverless.cli.consoleLog(buf.toString())); - python.stdin.write(input); - python.stdin.end(); + { env: process.env }, + { shell: true } + ); + python.stdout.on('data', buf => this.serverless.cli.consoleLog(buf.toString())); + python.stderr.on('data', buf => this.serverless.cli.consoleLog(buf.toString())); python.on('close', () => resolve()); + let isRejected = false; + python.on('error', error => { + isRejected = true; + reject(error); + }); + + process.nextTick(() => { + if (isRejected) return; // Runtime not available + python.stdin.write(input); + python.stdin.end(); + }); }); } callJavaBridge(artifactPath, className, handlerName, input) { - return new BbPromise((resolve) => fs.statAsync(artifactPath).then(() => { - const java = spawn('java', [ - `-DartifactPath=${artifactPath}`, - `-DclassName=${className}`, - `-DhandlerName=${handlerName}`, - '-jar', - path.join(__dirname, 'java', 'target', 'invoke-bridge-1.0.1.jar'), - ], { shell: true }); + return new BbPromise((resolve, reject) => + fs.statAsync(artifactPath).then( + () => { + const java = spawn( + 'java', + [ + `-DartifactPath=${artifactPath}`, + `-DclassName=${className}`, + `-DhandlerName=${handlerName}`, + '-jar', + path.join(__dirname, 'java', 'target', 'invoke-bridge-1.0.1.jar'), + ], + { shell: true } + ); - this.serverless.cli.log([ - 'In order to get human-readable output,', - ' please implement "toString()" method of your "ApiGatewayResponse" object.', - ].join('')); + this.serverless.cli.log( + [ + 'In order to get human-readable output,', + ' please implement "toString()" method of your "ApiGatewayResponse" object.', + ].join('') + ); - java.stdout.on('data', (buf) => this.serverless.cli.consoleLog(buf.toString())); - java.stderr.on('data', (buf) => this.serverless.cli.consoleLog(buf.toString())); - java.stdin.write(input); - java.stdin.end(); - java.on('close', () => resolve()); - }).catch(() => { - throw new Error(`Artifact ${artifactPath} doesn't exists, please compile it first.`); - })); + java.stdout.on('data', buf => this.serverless.cli.consoleLog(buf.toString())); + java.stderr.on('data', buf => this.serverless.cli.consoleLog(buf.toString())); + java.on('close', () => resolve()); + let isRejected = false; + java.on('error', error => { + isRejected = true; + reject(error); + }); + + process.nextTick(() => { + if (isRejected) return; // Runtime not available + java.stdin.write(input); + java.stdin.end(); + }); + }, + () => { + throw new Error(`Artifact ${artifactPath} doesn't exists, please compile it first.`); + } + ) + ); } invokeLocalJava(runtime, className, handlerName, artifactPath, event, customContext) { - const timeout = Number(this.options.functionObj.timeout) - || Number(this.serverless.service.provider.timeout) - || 6; + const timeout = + Number(this.options.functionObj.timeout) || + Number(this.serverless.service.provider.timeout) || + 6; const context = { name: this.options.functionObj.name, version: 'LATEST', @@ -453,25 +511,36 @@ class AwsInvokeLocal { const javaBridgePath = path.join(__dirname, 'java'); const executablePath = path.join(javaBridgePath, 'target'); - return new BbPromise(resolve => fs.statAsync(executablePath) - .then(() => this.callJavaBridge(artifactPath, className, handlerName, input)) - .then(resolve) - .catch(() => { - const mvn = spawn('mvn', [ - 'package', - '-f', - path.join(javaBridgePath, 'pom.xml'), - ], { shell: true }); + return new BbPromise((resolve, reject) => + fs + .statAsync(executablePath) + .then(() => this.callJavaBridge(artifactPath, className, handlerName, input)) + .then(resolve, () => { + const mvn = spawn('mvn', ['package', '-f', path.join(javaBridgePath, 'pom.xml')], { + shell: true, + }); - this.serverless.cli - .log('Building Java bridge, first invocation might take a bit longer.'); + this.serverless.cli.log( + 'Building Java bridge, first invocation might take a bit longer.' + ); - mvn.stderr.on('data', (buf) => this.serverless.cli.consoleLog(`mvn - ${buf.toString()}`)); - mvn.stdin.end(); + mvn.stderr.on('data', buf => this.serverless.cli.consoleLog(`mvn - ${buf.toString()}`)); - mvn.on('close', () => this.callJavaBridge(artifactPath, className, handlerName, input) - .then(resolve)); - })); + mvn.on('close', () => + this.callJavaBridge(artifactPath, className, handlerName, input).then(resolve) + ); + let isRejected = false; + mvn.on('error', error => { + isRejected = true; + reject(error); + }); + + process.nextTick(() => { + if (isRejected) return; // Runtime not available + mvn.stdin.end(); + }); + }) + ); } invokeLocalRuby(runtime, handlerPath, handlerName, event, context) { @@ -480,15 +549,27 @@ class AwsInvokeLocal { context, }); - return new BbPromise(resolve => { - const ruby = spawn(runtime, + return new BbPromise((resolve, reject) => { + const ruby = spawn( + runtime, [path.join(__dirname, 'invoke.rb'), handlerPath, handlerName], - { env: process.env }, { shell: true }); - ruby.stdout.on('data', (buf) => this.serverless.cli.consoleLog(buf.toString())); - ruby.stderr.on('data', (buf) => this.serverless.cli.consoleLog(buf.toString())); - ruby.stdin.write(input); - ruby.stdin.end(); + { env: process.env }, + { shell: true } + ); + ruby.stdout.on('data', buf => this.serverless.cli.consoleLog(buf.toString())); + ruby.stderr.on('data', buf => this.serverless.cli.consoleLog(buf.toString())); ruby.on('close', () => resolve()); + let isRejected = false; + ruby.on('error', error => { + isRejected = true; + reject(error); + }); + + process.nextTick(() => { + if (isRejected) return; // Runtime not available + ruby.stdin.write(input); + ruby.stdin.end(); + }); }); } @@ -506,9 +587,8 @@ class AwsInvokeLocal { this.options.extraServicePath || '', handlerPath ); - const handlersContainer = require( // eslint-disable-line global-require - pathToHandler - ); + const handlersContainer = require(// eslint-disable-line global-require + pathToHandler); lambda = handlersContainer[handlerName]; } catch (error) { this.serverless.cli.consoleLog(chalk.red(inspect(error))); @@ -521,7 +601,7 @@ class AwsInvokeLocal { errorResult = { errorMessage: err.message, errorType: err.constructor.name, - stackTrace: err.stack.split('\n'), + stackTrace: err.stack && err.stack.split('\n'), }; } else { errorResult = { @@ -552,7 +632,7 @@ class AwsInvokeLocal { this.serverless.cli.consoleLog(JSON.stringify(result, null, 4)); } - return new BbPromise((resolve) => { + return new BbPromise(resolve => { const callback = (err, result) => { if (!hasResponded) { hasResponded = true; @@ -566,9 +646,10 @@ class AwsInvokeLocal { }; const startTime = new Date(); - const timeout = Number(this.options.functionObj.timeout) - || Number(this.serverless.service.provider.timeout) - || 6; + const timeout = + Number(this.options.functionObj.timeout) || + Number(this.serverless.service.provider.timeout) || + 6; let context = { awsRequestId: 'id', invokeid: 'id', @@ -590,7 +671,7 @@ class AwsInvokeLocal { return callback(error, result); }, getRemainingTimeInMillis() { - return Math.max((timeout * 1000) - ((new Date()).valueOf() - startTime.valueOf()), 0); + return Math.max(timeout * 1000 - (new Date().valueOf() - startTime.valueOf()), 0); }, }; @@ -600,11 +681,10 @@ class AwsInvokeLocal { const maybeThennable = lambda(event, context, callback); if (!_.isUndefined(maybeThennable)) { - return BbPromise.resolve(maybeThennable) - .then( - callback.bind(this, null), - callback.bind(this) - ); + return BbPromise.resolve(maybeThennable).then( + callback.bind(this, null), + callback.bind(this) + ); } return maybeThennable; diff --git a/lib/plugins/aws/invokeLocal/index.test.js b/lib/plugins/aws/invokeLocal/index.test.js index f2919a400..7d3577f8a 100644 --- a/lib/plugins/aws/invokeLocal/index.test.js +++ b/lib/plugins/aws/invokeLocal/index.test.js @@ -7,12 +7,14 @@ const path = require('path'); const mockRequire = require('mock-require'); const EventEmitter = require('events'); const fse = require('fs-extra'); +const proxyquire = require('proxyquire'); const stripAnsi = require('strip-ansi'); -const AwsInvokeLocal = require('./index'); +const overrideEnv = require('process-utils/override-env'); const AwsProvider = require('../provider/awsProvider'); const Serverless = require('../../../Serverless'); const CLI = require('../../../classes/CLI'); -const testUtils = require('../../../../tests/utils'); +const { getTmpDirPath } = require('../../../../tests/utils/fs'); +const { skipWithNotice } = require('../../../../tests/utils/misc'); chai.use(require('chai-as-promised')); @@ -21,10 +23,12 @@ chai.should(); const expect = chai.expect; describe('AwsInvokeLocal', () => { + let AwsInvokeLocal; + let awsInvokeLocal; let options; let serverless; let provider; - let awsInvokeLocal; + let stdinStub; beforeEach(() => { options = { @@ -32,6 +36,10 @@ describe('AwsInvokeLocal', () => { region: 'us-east-1', function: 'first', }; + stdinStub = sinon.stub().resolves(''); + AwsInvokeLocal = proxyquire('./index', { + 'get-stdin': stdinStub, + }); serverless = new Serverless(); serverless.config.servicePath = 'servicePath'; serverless.cli = new CLI(serverless); @@ -48,9 +56,7 @@ describe('AwsInvokeLocal', () => { expect(awsInvokeLocal.provider).to.be.instanceof(AwsProvider)); it('should run invoke:local:invoke promise chain in order', () => { - const invokeLocalStub = sinon - .stub(awsInvokeLocal, 'invokeLocal').resolves(); - + const invokeLocalStub = sinon.stub(awsInvokeLocal, 'invokeLocal').resolves(); return awsInvokeLocal.hooks['invoke:local:invoke']().then(() => { expect(invokeLocalStub.callCount).to.be.equal(1); @@ -60,11 +66,8 @@ describe('AwsInvokeLocal', () => { }); it('should run before:invoke:local:loadEnvVars promise chain in order', () => { - const validateStub = sinon - .stub(awsInvokeLocal, 'extendedValidate').resolves(); - const loadEnvVarsStub = sinon - .stub(awsInvokeLocal, 'loadEnvVars').resolves(); - + const validateStub = sinon.stub(awsInvokeLocal, 'extendedValidate').resolves(); + const loadEnvVarsStub = sinon.stub(awsInvokeLocal, 'loadEnvVars').resolves(); return awsInvokeLocal.hooks['before:invoke:local:loadEnvVars']().then(() => { expect(validateStub.callCount).to.be.equal(1); @@ -83,6 +86,7 @@ describe('AwsInvokeLocal', () => { }); describe('#extendedValidate()', () => { + let backupIsTTY; beforeEach(() => { serverless.config.servicePath = true; serverless.service.environment = { @@ -105,25 +109,34 @@ describe('AwsInvokeLocal', () => { }; awsInvokeLocal.options.data = null; awsInvokeLocal.options.path = false; + + // Ensure there's no attempt to read path from stdin + backupIsTTY = process.stdin.isTTY; + process.stdin.isTTY = true; + }); + + afterEach(() => { + if (backupIsTTY) process.stdin.isTTY = backupIsTTY; + delete process.stdin.isTTY; }); it('should not throw error when there are no input data', () => { awsInvokeLocal.options.data = undefined; - return awsInvokeLocal.extendedValidate().then(() => { + return expect(awsInvokeLocal.extendedValidate()).to.be.fulfilled.then(() => { expect(awsInvokeLocal.options.data).to.equal(''); }); }); it('it should throw error if function is not provided', () => { serverless.service.functions = null; - expect(() => awsInvokeLocal.extendedValidate()).to.throw(Error); + return expect(awsInvokeLocal.extendedValidate()).to.be.rejected; }); it('should keep data if it is a simple string', () => { awsInvokeLocal.options.data = 'simple-string'; - return awsInvokeLocal.extendedValidate().then(() => { + return expect(awsInvokeLocal.extendedValidate()).to.be.fulfilled.then(() => { expect(awsInvokeLocal.options.data).to.equal('simple-string'); }); }); @@ -131,7 +144,7 @@ describe('AwsInvokeLocal', () => { it('should parse data if it is a json string', () => { awsInvokeLocal.options.data = '{"key": "value"}'; - return awsInvokeLocal.extendedValidate().then(() => { + return expect(awsInvokeLocal.extendedValidate()).to.be.fulfilled.then(() => { expect(awsInvokeLocal.options.data).to.deep.equal({ key: 'value' }); }); }); @@ -140,7 +153,7 @@ describe('AwsInvokeLocal', () => { awsInvokeLocal.options.data = '{"key": "value"}'; awsInvokeLocal.options.raw = true; - return awsInvokeLocal.extendedValidate().then(() => { + return expect(awsInvokeLocal.extendedValidate()).to.be.fulfilled.then(() => { expect(awsInvokeLocal.options.data).to.deep.equal('{"key": "value"}'); }); }); @@ -148,7 +161,7 @@ describe('AwsInvokeLocal', () => { it('should parse context if it is a json string', () => { awsInvokeLocal.options.context = '{"key": "value"}'; - return awsInvokeLocal.extendedValidate().then(() => { + return expect(awsInvokeLocal.extendedValidate()).to.be.fulfilled.then(() => { expect(awsInvokeLocal.options.context).to.deep.equal({ key: 'value' }); }); }); @@ -157,27 +170,29 @@ describe('AwsInvokeLocal', () => { awsInvokeLocal.options.context = '{"key": "value"}'; awsInvokeLocal.options.raw = true; - return awsInvokeLocal.extendedValidate().then(() => { + return expect(awsInvokeLocal.extendedValidate()).to.be.fulfilled.then(() => { expect(awsInvokeLocal.options.context).to.deep.equal('{"key": "value"}'); }); }); it('it should parse file if relative file path is provided', () => { - serverless.config.servicePath = testUtils.getTmpDirPath(); + serverless.config.servicePath = getTmpDirPath(); const data = { testProp: 'testValue', }; - serverless.utils.writeFileSync(path - .join(serverless.config.servicePath, 'data.json'), JSON.stringify(data)); + serverless.utils.writeFileSync( + path.join(serverless.config.servicePath, 'data.json'), + JSON.stringify(data) + ); awsInvokeLocal.options.contextPath = 'data.json'; - return awsInvokeLocal.extendedValidate().then(() => { + return expect(awsInvokeLocal.extendedValidate()).to.be.fulfilled.then(() => { expect(awsInvokeLocal.options.context).to.deep.equal(data); }); }); it('it should parse file if absolute file path is provided', () => { - serverless.config.servicePath = testUtils.getTmpDirPath(); + serverless.config.servicePath = getTmpDirPath(); const data = { event: { testProp: 'testValue', @@ -188,26 +203,28 @@ describe('AwsInvokeLocal', () => { awsInvokeLocal.options.path = dataFile; awsInvokeLocal.options.contextPath = false; - return awsInvokeLocal.extendedValidate().then(() => { + return expect(awsInvokeLocal.extendedValidate()).to.be.fulfilled.then(() => { expect(awsInvokeLocal.options.data).to.deep.equal(data); }); }); it('it should parse a yaml file if file path is provided', () => { - serverless.config.servicePath = testUtils.getTmpDirPath(); + serverless.config.servicePath = getTmpDirPath(); const yamlContent = 'event: data'; - serverless.utils.writeFileSync(path - .join(serverless.config.servicePath, 'data.yml'), yamlContent); + serverless.utils.writeFileSync( + path.join(serverless.config.servicePath, 'data.yml'), + yamlContent + ); awsInvokeLocal.options.path = 'data.yml'; - return awsInvokeLocal.extendedValidate().then(() => { + return expect(awsInvokeLocal.extendedValidate()).to.be.fulfilled.then(() => { expect(awsInvokeLocal.options.data).to.deep.equal({ event: 'data' }); }); }); it('it should require a js file if file path is provided', () => { - serverless.config.servicePath = testUtils.getTmpDirPath(); + serverless.config.servicePath = getTmpDirPath(); const jsContent = [ 'module.exports = {', ' headers: { "Content-Type" : "application/json" },', @@ -215,11 +232,13 @@ describe('AwsInvokeLocal', () => { '}', ].join('\n'); - serverless.utils.writeFileSync(path - .join(serverless.config.servicePath, 'data.js'), jsContent); + serverless.utils.writeFileSync( + path.join(serverless.config.servicePath, 'data.js'), + jsContent + ); awsInvokeLocal.options.path = 'data.js'; - return awsInvokeLocal.extendedValidate().then(() => { + return expect(awsInvokeLocal.extendedValidate()).to.be.fulfilled.then(() => { expect(awsInvokeLocal.options.data).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, body: '[100,200]', @@ -227,30 +246,28 @@ describe('AwsInvokeLocal', () => { }); }); - it('it should throw error if service path is not set', () => { serverless.config.servicePath = false; - expect(() => awsInvokeLocal.extendedValidate()).to.throw(Error); + return expect(awsInvokeLocal.extendedValidate()).to.be.rejected; }); it('it should reject error if file path does not exist', () => { - serverless.config.servicePath = testUtils.getTmpDirPath(); + serverless.config.servicePath = getTmpDirPath(); awsInvokeLocal.options.path = 'some/path'; - return awsInvokeLocal.extendedValidate().catch((err) => { - expect(err).to.be.instanceOf(Error); - }); + return expect(awsInvokeLocal.extendedValidate()).to.be.rejected; }); - it('should resolve if path is not given', (done) => { + it('should resolve if path is not given', () => { awsInvokeLocal.options.path = false; - - awsInvokeLocal.extendedValidate().then(() => done()); + return expect(awsInvokeLocal.extendedValidate()).to.be.fulfilled; }); }); describe('#loadEnvVars()', () => { + let restoreEnv; beforeEach(() => { + ({ restoreEnv } = overrideEnv()); serverless.config.servicePath = true; serverless.service.provider = { environment: { @@ -269,15 +286,12 @@ describe('AwsInvokeLocal', () => { }; }); - afterEach(() => { - delete process.env.AWS_PROFILE; - }); + afterEach(() => restoreEnv()); - it('it should load provider env vars', () => awsInvokeLocal - .loadEnvVars().then(() => { + it('it should load provider env vars', () => + awsInvokeLocal.loadEnvVars().then(() => { expect(process.env.providerVar).to.be.equal('providerValue'); - }) - ); + })); it('it should load provider profile env', () => { serverless.service.provider.profile = 'jdoe'; @@ -286,31 +300,30 @@ describe('AwsInvokeLocal', () => { }); }); - it('it should load function env vars', () => awsInvokeLocal - .loadEnvVars().then(() => { + it('it should load function env vars', () => + awsInvokeLocal.loadEnvVars().then(() => { expect(process.env.functionVar).to.be.equal('functionValue'); - }) - ); + })); - it('it should load default lambda env vars', () => awsInvokeLocal - .loadEnvVars().then(() => { + it('it should load default lambda env vars', () => + awsInvokeLocal.loadEnvVars().then(() => { expect(process.env.LANG).to.equal('en_US.UTF-8'); - expect(process.env.LD_LIBRARY_PATH) - .to.equal('/usr/local/lib64/node-v4.3.x/lib:/lib64:/usr/lib64:/var/runtime:/var/runtime/lib:/var/task:/var/task/lib'); // eslint-disable-line max-len + expect(process.env.LD_LIBRARY_PATH).to.equal( + '/usr/local/lib64/node-v4.3.x/lib:/lib64:/usr/lib64:/var/runtime:/var/runtime/lib:/var/task:/var/task/lib' + ); // eslint-disable-line max-len expect(process.env.LAMBDA_TASK_ROOT).to.equal('/var/task'); expect(process.env.LAMBDA_RUNTIME_DIR).to.equal('/var/runtime'); expect(process.env.AWS_REGION).to.equal('us-east-1'); expect(process.env.AWS_DEFAULT_REGION).to.equal('us-east-1'); expect(process.env.AWS_LAMBDA_LOG_GROUP_NAME).to.equal('/aws/lambda/serviceName-dev-hello'); - expect(process.env.AWS_LAMBDA_LOG_STREAM_NAME) - .to.equal('2016/12/02/[$LATEST]f77ff5e4026c45bda9a9ebcec6bc9cad'); + expect(process.env.AWS_LAMBDA_LOG_STREAM_NAME).to.equal( + '2016/12/02/[$LATEST]f77ff5e4026c45bda9a9ebcec6bc9cad' + ); expect(process.env.AWS_LAMBDA_FUNCTION_NAME).to.equal('serviceName-dev-hello'); expect(process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE).to.equal('1024'); expect(process.env.AWS_LAMBDA_FUNCTION_VERSION).to.equal('$LATEST'); expect(process.env.NODE_PATH).to.equal('/var/runtime:/var/task:/var/runtime/node_modules'); - }) - ); - + })); it('should fallback to service provider configuration when options are not available', () => { awsInvokeLocal.provider.options.region = null; @@ -339,16 +352,11 @@ describe('AwsInvokeLocal', () => { let invokeLocalDockerStub; beforeEach(() => { - invokeLocalNodeJsStub = - sinon.stub(awsInvokeLocal, 'invokeLocalNodeJs').resolves(); - invokeLocalPythonStub = - sinon.stub(awsInvokeLocal, 'invokeLocalPython').resolves(); - invokeLocalJavaStub = - sinon.stub(awsInvokeLocal, 'invokeLocalJava').resolves(); - invokeLocalRubyStub = - sinon.stub(awsInvokeLocal, 'invokeLocalRuby').resolves(); - invokeLocalDockerStub = - sinon.stub(awsInvokeLocal, 'invokeLocalDocker').resolves(); + invokeLocalNodeJsStub = sinon.stub(awsInvokeLocal, 'invokeLocalNodeJs').resolves(); + invokeLocalPythonStub = sinon.stub(awsInvokeLocal, 'invokeLocalPython').resolves(); + invokeLocalJavaStub = sinon.stub(awsInvokeLocal, 'invokeLocalJava').resolves(); + invokeLocalRubyStub = sinon.stub(awsInvokeLocal, 'invokeLocalRuby').resolves(); + invokeLocalDockerStub = sinon.stub(awsInvokeLocal, 'invokeLocalDocker').resolves(); awsInvokeLocal.serverless.service.service = 'new-service'; awsInvokeLocal.provider.options.stage = 'dev'; @@ -369,43 +377,32 @@ describe('AwsInvokeLocal', () => { awsInvokeLocal.invokeLocalRuby.restore(); }); - it('should call invokeLocalNodeJs when no runtime is set', () => awsInvokeLocal.invokeLocal() - .then(() => { + it('should call invokeLocalNodeJs when no runtime is set', () => + awsInvokeLocal.invokeLocal().then(() => { expect(invokeLocalNodeJsStub.calledOnce).to.be.equal(true); - expect(invokeLocalNodeJsStub.calledWithExactly( - 'handler', - 'hello', - {}, - undefined - )).to.be.equal(true); - }) - ); + expect( + invokeLocalNodeJsStub.calledWithExactly('handler', 'hello', {}, undefined) + ).to.be.equal(true); + })); it('should call invokeLocalNodeJs for any node.js runtime version', () => { - awsInvokeLocal.options.functionObj.runtime = 'nodejs6.10'; + awsInvokeLocal.options.functionObj.runtime = 'nodejs10.x'; return awsInvokeLocal.invokeLocal().then(() => { expect(invokeLocalNodeJsStub.calledOnce).to.be.equal(true); - expect(invokeLocalNodeJsStub.calledWithExactly( - 'handler', - 'hello', - {}, - undefined - )).to.be.equal(true); + expect( + invokeLocalNodeJsStub.calledWithExactly('handler', 'hello', {}, undefined) + ).to.be.equal(true); }); }); it('should call invokeLocalNodeJs with custom context if provided', () => { awsInvokeLocal.options.context = 'custom context'; - return awsInvokeLocal.invokeLocal() - .then(() => { - expect(invokeLocalNodeJsStub.calledOnce).to.be.equal(true); - expect(invokeLocalNodeJsStub.calledWithExactly( - 'handler', - 'hello', - {}, - 'custom context' - )).to.be.equal(true); - }); + return awsInvokeLocal.invokeLocal().then(() => { + expect(invokeLocalNodeJsStub.calledOnce).to.be.equal(true); + expect( + invokeLocalNodeJsStub.calledWithExactly('handler', 'hello', {}, 'custom context') + ).to.be.equal(true); + }); }); it('should call invokeLocalPython when python2.7 runtime is set', () => { @@ -414,30 +411,27 @@ describe('AwsInvokeLocal', () => { // NOTE: this is important so that tests on Windows won't fail const runtime = process.platform === 'win32' ? 'python.exe' : 'python2.7'; expect(invokeLocalPythonStub.calledOnce).to.be.equal(true); - expect(invokeLocalPythonStub.calledWithExactly( - runtime, - 'handler', - 'hello', - {}, - undefined - )).to.be.equal(true); + expect( + invokeLocalPythonStub.calledWithExactly(runtime, 'handler', 'hello', {}, undefined) + ).to.be.equal(true); }); }); it('should call invokeLocalJava when java8 runtime is set', () => { awsInvokeLocal.options.functionObj.runtime = 'java8'; - return awsInvokeLocal.invokeLocal() - .then(() => { - expect(invokeLocalJavaStub.calledOnce).to.be.equal(true); - expect(invokeLocalJavaStub.calledWithExactly( + return awsInvokeLocal.invokeLocal().then(() => { + expect(invokeLocalJavaStub.calledOnce).to.be.equal(true); + expect( + invokeLocalJavaStub.calledWithExactly( 'java', 'handler.hello', 'handleRequest', undefined, {}, undefined - )).to.be.equal(true); - }); + ) + ).to.be.equal(true); + }); }); it('should call invokeLocalRuby when ruby2.5 runtime is set', () => { @@ -446,13 +440,9 @@ describe('AwsInvokeLocal', () => { // NOTE: this is important so that tests on Windows won't fail const runtime = process.platform === 'win32' ? 'ruby.exe' : 'ruby'; expect(invokeLocalRubyStub.calledOnce).to.be.equal(true); - expect(invokeLocalRubyStub.calledWithExactly( - runtime, - 'handler', - 'hello', - {}, - undefined - )).to.be.equal(true); + expect( + invokeLocalRubyStub.calledWithExactly(runtime, 'handler', 'hello', {}, undefined) + ).to.be.equal(true); }); }); @@ -463,13 +453,15 @@ describe('AwsInvokeLocal', () => { // NOTE: this is important so that tests on Windows won't fail const runtime = process.platform === 'win32' ? 'ruby.exe' : 'ruby'; expect(invokeLocalRubyStub.calledOnce).to.be.equal(true); - expect(invokeLocalRubyStub.calledWithExactly( - runtime, - 'handler', - 'MyModule::MyClass.hello', - {}, - undefined - )).to.be.equal(true); + expect( + invokeLocalRubyStub.calledWithExactly( + runtime, + 'handler', + 'MyModule::MyClass.hello', + {}, + undefined + ) + ).to.be.equal(true); }); }); @@ -482,8 +474,8 @@ describe('AwsInvokeLocal', () => { }); }); - it('should call invokeLocalDocker if using --docker option with nodejs8.10', () => { - awsInvokeLocal.options.functionObj.runtime = 'nodejs8.10'; + it('should call invokeLocalDocker if using --docker option with nodejs10.x', () => { + awsInvokeLocal.options.functionObj.runtime = 'nodejs10.x'; awsInvokeLocal.options.functionObj.handler = 'handler.foobar'; awsInvokeLocal.options.docker = true; return awsInvokeLocal.invokeLocal().then(() => { @@ -512,7 +504,8 @@ describe('AwsInvokeLocal', () => { it('should succeed if succeed', () => { awsInvokeLocal.serverless.config.servicePath = __dirname; - return awsInvokeLocal.invokeLocalNodeJs('fixture/handlerWithSuccess', 'withMessageByReturn') + return awsInvokeLocal + .invokeLocalNodeJs('fixture/handlerWithSuccess', 'withMessageByReturn') .then(() => expect(serverless.cli.consoleLog.lastCall.args[0]).to.contain('"Succeed"')); }); }); @@ -541,7 +534,9 @@ describe('AwsInvokeLocal', () => { awsInvokeLocal.invokeLocalNodeJs('fixture/handlerWithSuccess', 'withMessageByLambdaProxy'); - expect(serverless.cli.consoleLog.lastCall.args[0]).to.contain('{\n "statusCode": 200,\n "headers": {\n "Content-Type": "application/json"\n },\n "body": {\n "result": true,\n "message": "Whatever"\n }\n}'); // eslint-disable-line + expect(serverless.cli.consoleLog.lastCall.args[0]).to.contain( + '{\n "statusCode": 200,\n "headers": {\n "Content-Type": "application/json"\n },\n "body": {\n "result": true,\n "message": "Whatever"\n }\n}' + ); // eslint-disable-line }); }); @@ -583,7 +578,9 @@ describe('AwsInvokeLocal', () => { awsInvokeLocal.invokeLocalNodeJs('handlerWithSuccess', 'withMessageByLambdaProxy'); - expect(serverless.cli.consoleLog.lastCall.args[0]).to.contain('{\n "statusCode": 200,\n "headers": {\n "Content-Type": "application/json"\n },\n "body": {\n "result": true,\n "message": "Whatever"\n }\n}'); // eslint-disable-line + expect(serverless.cli.consoleLog.lastCall.args[0]).to.contain( + '{\n "statusCode": 200,\n "headers": {\n "Content-Type": "application/json"\n },\n "body": {\n "result": true,\n "message": "Whatever"\n }\n}' + ); // eslint-disable-line }); }); @@ -631,8 +628,9 @@ describe('AwsInvokeLocal', () => { it('should throw when module loading error', () => { awsInvokeLocal.serverless.config.servicePath = __dirname; - expect(() => awsInvokeLocal.invokeLocalNodeJs('fixture/handlerWithLoadingError', - 'anyMethod')).to.throw(/Exception encountered when loading/); + expect(() => + awsInvokeLocal.invokeLocalNodeJs('fixture/handlerWithLoadingError', 'anyMethod') + ).to.throw(/Exception encountered when loading/); }); }); @@ -654,10 +652,8 @@ describe('AwsInvokeLocal', () => { describe('with return', () => { it('should exit with error exit code', () => { awsInvokeLocal.serverless.config.servicePath = __dirname; - return awsInvokeLocal.invokeLocalNodeJs( - 'fixture/asyncHandlerWithSuccess', - 'withError' - ) + return awsInvokeLocal + .invokeLocalNodeJs('fixture/asyncHandlerWithSuccess', 'withError') .then(() => { expect(process.exitCode).to.be.equal(1); }); @@ -666,10 +662,8 @@ describe('AwsInvokeLocal', () => { it('should succeed if succeed', () => { awsInvokeLocal.serverless.config.servicePath = __dirname; - return awsInvokeLocal.invokeLocalNodeJs( - 'fixture/asyncHandlerWithSuccess', - 'withMessage' - ) + return awsInvokeLocal + .invokeLocalNodeJs('fixture/asyncHandlerWithSuccess', 'withMessage') .then(() => { expect(serverless.cli.consoleLog.lastCall.args[0]).to.contain('"Succeed"'); }); @@ -680,15 +674,16 @@ describe('AwsInvokeLocal', () => { it('success should trigger one response', () => { awsInvokeLocal.serverless.config.servicePath = __dirname; - return awsInvokeLocal.invokeLocalNodeJs( - 'fixture/asyncHandlerWithSuccess', - 'withMessageByDone' - ) + return awsInvokeLocal + .invokeLocalNodeJs('fixture/asyncHandlerWithSuccess', 'withMessageByDone') .then(() => { expect(serverless.cli.consoleLog.lastCall.args[0]).to.contain('"Succeed"'); - const calls = serverless.cli.consoleLog.getCalls().reduce((acc, call) => ( - _.includes(call.args[0], 'Succeed') ? [call].concat(acc) : acc - ), []); + const calls = serverless.cli.consoleLog + .getCalls() + .reduce( + (acc, call) => (_.includes(call.args[0], 'Succeed') ? [call].concat(acc) : acc), + [] + ); expect(calls.length).to.equal(1); }); }); @@ -696,10 +691,8 @@ describe('AwsInvokeLocal', () => { it('error should trigger one response', () => { awsInvokeLocal.serverless.config.servicePath = __dirname; - return awsInvokeLocal.invokeLocalNodeJs( - 'fixture/asyncHandlerWithSuccess', - 'withErrorByDone' - ) + return awsInvokeLocal + .invokeLocalNodeJs('fixture/asyncHandlerWithSuccess', 'withErrorByDone') .then(() => { expect(serverless.cli.consoleLog.lastCall.args[0]).to.contain('"failed"'); expect(process.exitCode).to.be.equal(1); @@ -711,15 +704,16 @@ describe('AwsInvokeLocal', () => { it('should succeed once if succeed if by callback', () => { awsInvokeLocal.serverless.config.servicePath = __dirname; - return awsInvokeLocal.invokeLocalNodeJs( - 'fixture/asyncHandlerWithSuccess', - 'withMessageByCallback' - ) + return awsInvokeLocal + .invokeLocalNodeJs('fixture/asyncHandlerWithSuccess', 'withMessageByCallback') .then(() => { expect(serverless.cli.consoleLog.lastCall.args[0]).to.contain('"Succeed"'); - const calls = serverless.cli.consoleLog.getCalls().reduce((acc, call) => ( - _.includes(call.args[0], 'Succeed') ? [call].concat(acc) : acc - ), []); + const calls = serverless.cli.consoleLog + .getCalls() + .reduce( + (acc, call) => (_.includes(call.args[0], 'Succeed') ? [call].concat(acc) : acc), + [] + ); expect(calls.length).to.equal(1); }); }); @@ -729,15 +723,16 @@ describe('AwsInvokeLocal', () => { it('should succeed once if succeed if by callback', () => { awsInvokeLocal.serverless.config.servicePath = __dirname; - return awsInvokeLocal.invokeLocalNodeJs( - 'fixture/asyncHandlerWithSuccess', - 'withMessageAndDelayByCallback' - ) + return awsInvokeLocal + .invokeLocalNodeJs('fixture/asyncHandlerWithSuccess', 'withMessageAndDelayByCallback') .then(() => { expect(serverless.cli.consoleLog.lastCall.args[0]).to.contain('"Succeed"'); - const calls = serverless.cli.consoleLog.getCalls().reduce((acc, call) => ( - _.includes(call.args[0], 'Succeed') ? [call].concat(acc) : acc - ), []); + const calls = serverless.cli.consoleLog + .getCalls() + .reduce( + (acc, call) => (_.includes(call.args[0], 'Succeed') ? [call].concat(acc) : acc), + [] + ); expect(calls.length).to.equal(1); }); }); @@ -747,12 +742,12 @@ describe('AwsInvokeLocal', () => { it('should succeed if succeed', () => { awsInvokeLocal.serverless.config.servicePath = __dirname; - return awsInvokeLocal.invokeLocalNodeJs( - 'fixture/asyncHandlerWithSuccess', - 'withMessageByLambdaProxy' - ) + return awsInvokeLocal + .invokeLocalNodeJs('fixture/asyncHandlerWithSuccess', 'withMessageByLambdaProxy') .then(() => { - expect(serverless.cli.consoleLog.lastCall.args[0]).to.contain('{\n "statusCode": 200,\n "headers": {\n "Content-Type": "application/json"\n },\n "body": {\n "result": true,\n "message": "Whatever"\n }\n}'); // eslint-disable-line + expect(serverless.cli.consoleLog.lastCall.args[0]).to.contain( + '{\n "statusCode": 200,\n "headers": {\n "Content-Type": "application/json"\n },\n "body": {\n "result": true,\n "message": "Whatever"\n }\n}' + ); // eslint-disable-line }); }); }); @@ -761,10 +756,8 @@ describe('AwsInvokeLocal', () => { it('should become lower over time', () => { awsInvokeLocal.serverless.config.servicePath = __dirname; - return awsInvokeLocal.invokeLocalNodeJs( - 'fixture/asyncHandlerWithSuccess', - 'withRemainingTime' - ) + return awsInvokeLocal + .invokeLocalNodeJs('fixture/asyncHandlerWithSuccess', 'withRemainingTime') .then(() => { const remainingTimes = JSON.parse(serverless.cli.consoleLog.lastCall.args[0]); expect(remainingTimes.start).to.be.above(remainingTimes.stop); @@ -775,10 +768,8 @@ describe('AwsInvokeLocal', () => { awsInvokeLocal.serverless.config.servicePath = __dirname; awsInvokeLocal.serverless.service.provider.timeout = 5; - return awsInvokeLocal.invokeLocalNodeJs( - 'fixture/asyncHandlerWithSuccess', - 'withRemainingTime' - ) + return awsInvokeLocal + .invokeLocalNodeJs('fixture/asyncHandlerWithSuccess', 'withRemainingTime') .then(() => { const remainingTimes = JSON.parse(serverless.cli.consoleLog.lastCall.args[0]); expect(remainingTimes.start).to.match(/\d+/); @@ -789,7 +780,8 @@ describe('AwsInvokeLocal', () => { awsInvokeLocal.serverless.config.servicePath = __dirname; awsInvokeLocal.serverless.service.provider.timeout = 0.00001; - awsInvokeLocal.invokeLocalNodeJs('fixture/asyncHandlerWithSuccess', 'withRemainingTime') + return awsInvokeLocal + .invokeLocalNodeJs('fixture/asyncHandlerWithSuccess', 'withRemainingTime') .then(() => { const remainingTimes = JSON.parse(serverless.cli.consoleLog.lastCall.args[0]); expect(remainingTimes.stop).to.eql(0); @@ -802,12 +794,12 @@ describe('AwsInvokeLocal', () => { awsInvokeLocal.serverless.config.servicePath = __dirname; awsInvokeLocal.options.extraServicePath = 'fixture'; - return awsInvokeLocal.invokeLocalNodeJs( - 'asyncHandlerWithSuccess', - 'withMessageByLambdaProxy' - ) + return awsInvokeLocal + .invokeLocalNodeJs('asyncHandlerWithSuccess', 'withMessageByLambdaProxy') .then(() => { - expect(serverless.cli.consoleLog.lastCall.args[0]).to.contain('{\n "statusCode": 200,\n "headers": {\n "Content-Type": "application/json"\n },\n "body": {\n "result": true,\n "message": "Whatever"\n }\n}'); // eslint-disable-line + expect(serverless.cli.consoleLog.lastCall.args[0]).to.contain( + '{\n "statusCode": 200,\n "headers": {\n "Content-Type": "application/json"\n },\n "body": {\n "result": true,\n "message": "Whatever"\n }\n}' + ); // eslint-disable-line }); }); }); @@ -815,7 +807,8 @@ describe('AwsInvokeLocal', () => { it('should exit with error exit code', () => { awsInvokeLocal.serverless.config.servicePath = __dirname; - return awsInvokeLocal.invokeLocalNodeJs('fixture/asyncHandlerWithError', 'withError') + return awsInvokeLocal + .invokeLocalNodeJs('fixture/asyncHandlerWithError', 'withError') .then(() => { expect(process.exitCode).to.be.equal(1); }); @@ -824,7 +817,8 @@ describe('AwsInvokeLocal', () => { it('should log Error instance when called back', () => { awsInvokeLocal.serverless.config.servicePath = __dirname; - return awsInvokeLocal.invokeLocalNodeJs('fixture/asyncHandlerWithError', 'withError') + return awsInvokeLocal + .invokeLocalNodeJs('fixture/asyncHandlerWithError', 'withError') .then(() => { expect(serverless.cli.consoleLog.lastCall.args[0]).to.contain('"errorMessage": "failed"'); expect(serverless.cli.consoleLog.lastCall.args[0]).to.contain('"errorType": "Error"'); @@ -834,7 +828,8 @@ describe('AwsInvokeLocal', () => { it('should log error', () => { awsInvokeLocal.serverless.config.servicePath = __dirname; - return awsInvokeLocal.invokeLocalNodeJs('fixture/asyncHandlerWithError', 'withMessage') + return awsInvokeLocal + .invokeLocalNodeJs('fixture/asyncHandlerWithError', 'withMessage') .then(() => { expect(serverless.cli.consoleLog.lastCall.args[0]).to.contain('"errorMessage": "failed"'); }); @@ -843,16 +838,15 @@ describe('AwsInvokeLocal', () => { it('should log error when error is returned', () => { awsInvokeLocal.serverless.config.servicePath = __dirname; - return awsInvokeLocal.invokeLocalNodeJs('fixture/asyncHandlerWithError', 'returnsError') + return awsInvokeLocal + .invokeLocalNodeJs('fixture/asyncHandlerWithError', 'returnsError') .then(() => { expect(serverless.cli.consoleLog.lastCall.args[0]).to.contain('"errorMessage": "failed"'); }); }); }); - // Ignored because it fails in CI - // See https://github.com/serverless/serverless/pull/4047#issuecomment-320460285 - describe.skip('#invokeLocalPython', () => { + describe('#invokeLocalPython', () => { beforeEach(() => { awsInvokeLocal.options = { functionObj: { @@ -863,25 +857,105 @@ describe('AwsInvokeLocal', () => { sinon.stub(serverless.cli, 'consoleLog'); }); - afterEach(() => { + const afterEachCallback = () => { serverless.cli.consoleLog.restore(); - }); + }; + afterEach(afterEachCallback); describe('context.remainingTimeInMillis', () => { - it('should become lower over time', () => { + it('should become lower over time', function() { + // skipping in CI for now due to handler loading issues + // in the Windows machine on Travis CI + if (process.env.CI) { + this.skip(); + } + awsInvokeLocal.serverless.config.servicePath = __dirname; - return awsInvokeLocal.invokeLocalPython( - 'python2.7', - 'fixture/handler', - 'withRemainingTime').then(() => { - const remainingTimes = JSON.parse(serverless.cli.consoleLog.lastCall.args[0]); - expect(remainingTimes.start).to.be.above(remainingTimes.stop); - }); + return awsInvokeLocal + .invokeLocalPython('python2.7', 'fixture/handler', 'withRemainingTime') + .then( + () => { + const remainingTimes = JSON.parse(serverless.cli.consoleLog.lastCall.args[0]); + expect(remainingTimes.start).to.be.above(remainingTimes.stop); + }, + error => { + if (error.code === 'ENOENT' && error.path === 'python2') { + skipWithNotice(this, 'Python runtime is not installed', afterEachCallback); + } + throw error; + } + ); }); }); }); + describe('#invokeLocalRuby', () => { + let curdir; + + beforeEach(() => { + curdir = process.cwd(); + process.chdir(__dirname); + awsInvokeLocal.options = { + functionObj: { + name: '', + }, + }; + + sinon.stub(serverless.cli, 'consoleLog'); + }); + + const afterEachCallback = () => { + serverless.cli.consoleLog.restore(); + process.chdir(curdir); + }; + afterEach(afterEachCallback); + + describe('context.remainingTimeInMillis', () => { + it('should become lower over time', function() { + awsInvokeLocal.serverless.config.servicePath = __dirname; + + return awsInvokeLocal.invokeLocalRuby('ruby', 'fixture/handler', 'withRemainingTime').then( + () => { + const remainingTimes = JSON.parse(serverless.cli.consoleLog.lastCall.args[0]); + expect(remainingTimes.start).to.be.above(remainingTimes.stop); + }, + error => { + if (error.code === 'ENOENT' && error.path === 'ruby') { + skipWithNotice(this, 'Ruby runtime is not installed', afterEachCallback); + } + throw error; + } + ); + }); + }); + + describe('calling a class method', () => { + it('should execute', function() { + awsInvokeLocal.serverless.config.servicePath = __dirname; + + return awsInvokeLocal + .invokeLocalRuby('ruby', 'fixture/handler', 'MyModule::MyClass.my_class_method') + .then( + () => { + const result = JSON.parse(serverless.cli.consoleLog.lastCall.args[0]); + expect(result.foo).to.eq('bar'); + }, + error => { + if (error.code === 'ENOENT' && error.path === 'ruby') { + skipWithNotice(this, 'Ruby runtime is not installed', afterEachCallback); + } + throw error; + } + ); + }); + }); + }); + + // ------------- + // TODO: the following tests need to be refactored since they use mockRequire in a breaking way + // ------------- + describe('#callJavaBridge()', () => { let awsInvokeLocalMocked; let writeChildStub; @@ -899,7 +973,9 @@ describe('AwsInvokeLocal', () => { write: writeChildStub, end: endChildStub, }, - on: (key, callback) => callback(), + on: (key, callback) => { + if (key === 'close') process.nextTick(callback); + }, }), }); @@ -931,17 +1007,13 @@ describe('AwsInvokeLocal', () => { }); it('spawns java process with correct arguments', () => - awsInvokeLocalMocked.callJavaBridge( - __dirname, - 'com.serverless.Handler', - 'handleRequest', - '{}' - ).then(() => { - expect(writeChildStub.calledOnce).to.be.equal(true); - expect(endChildStub.calledOnce).to.be.equal(true); - expect(writeChildStub.calledWithExactly('{}')).to.be.equal(true); - }) - ); + awsInvokeLocalMocked + .callJavaBridge(__dirname, 'com.serverless.Handler', 'handleRequest', '{}') + .then(() => { + expect(writeChildStub.calledOnce).to.be.equal(true); + expect(endChildStub.calledOnce).to.be.equal(true); + expect(writeChildStub.calledWithExactly('{}')).to.be.equal(true); + })); }); describe('#invokeLocalJava()', () => { @@ -969,30 +1041,27 @@ describe('AwsInvokeLocal', () => { }); it('should invoke callJavaBridge when bridge is built', () => - awsInvokeLocal.invokeLocalJava( - 'java', - 'com.serverless.Handler', - 'handleRequest', - __dirname, - {} - ).then(() => { - expect(callJavaBridgeStub.calledOnce).to.be.equal(true); - expect(callJavaBridgeStub.calledWithExactly( - __dirname, - 'com.serverless.Handler', - 'handleRequest', - JSON.stringify({ - event: {}, - context: { - name: 'hello', - version: 'LATEST', - logGroupName: '/aws/lambda/hello', - timeout: 4, - }, - }) - )).to.be.equal(true); - }) - ); + awsInvokeLocal + .invokeLocalJava('java', 'com.serverless.Handler', 'handleRequest', __dirname, {}) + .then(() => { + expect(callJavaBridgeStub.calledOnce).to.be.equal(true); + expect( + callJavaBridgeStub.calledWithExactly( + __dirname, + 'com.serverless.Handler', + 'handleRequest', + JSON.stringify({ + event: {}, + context: { + name: 'hello', + version: 'LATEST', + logGroupName: '/aws/lambda/hello', + timeout: 4, + }, + }) + ) + ).to.be.equal(true); + })); describe('when attempting to build the Java bridge', () => { let awsInvokeLocalMocked; @@ -1039,79 +1108,28 @@ describe('AwsInvokeLocal', () => { delete require.cache[require.resolve('child_process')]; }); - it('if it\'s not present yet', () => - awsInvokeLocalMocked.invokeLocalJava( - 'java', - 'com.serverless.Handler', - 'handleRequest', - __dirname, - {} - ).then(() => { - expect(callJavaBridgeMockedStub.calledOnce).to.be.equal(true); - expect(callJavaBridgeMockedStub.calledWithExactly( - __dirname, - 'com.serverless.Handler', - 'handleRequest', - JSON.stringify({ - event: {}, - context: { - name: 'hello', - version: 'LATEST', - logGroupName: '/aws/lambda/hello', - timeout: 4, - }, - }) - )).to.be.equal(true); - }) - ); - }); - }); - - describe('#invokeLocalRuby', () => { - let curdir; - beforeEach(() => { - curdir = process.cwd(); - process.chdir(__dirname); - awsInvokeLocal.options = { - functionObj: { - name: '', - }, - }; - - sinon.stub(serverless.cli, 'consoleLog'); - }); - - afterEach(() => { - serverless.cli.consoleLog.restore(); - process.chdir(curdir); - }); - - describe('context.remainingTimeInMillis', () => { - it('should become lower over time', () => { - awsInvokeLocal.serverless.config.servicePath = __dirname; - - return awsInvokeLocal.invokeLocalRuby( - 'ruby', - 'fixture/handler', - 'withRemainingTime').then(() => { - const remainingTimes = JSON.parse(serverless.cli.consoleLog.lastCall.args[0]); - expect(remainingTimes.start).to.be.above(remainingTimes.stop); - }); - }); - }); - - describe('calling a class method', () => { - it('should execute', () => { - awsInvokeLocal.serverless.config.servicePath = __dirname; - - return awsInvokeLocal.invokeLocalRuby( - 'ruby', - 'fixture/handler', - 'MyModule::MyClass.my_class_method').then(() => { - const result = JSON.parse(serverless.cli.consoleLog.lastCall.args[0]); - expect(result.foo).to.eq('bar'); - }); - }); + it("if it's not present yet", () => + awsInvokeLocalMocked + .invokeLocalJava('java', 'com.serverless.Handler', 'handleRequest', __dirname, {}) + .then(() => { + expect(callJavaBridgeMockedStub.calledOnce).to.be.equal(true); + expect( + callJavaBridgeMockedStub.calledWithExactly( + __dirname, + 'com.serverless.Handler', + 'handleRequest', + JSON.stringify({ + event: {}, + context: { + name: 'hello', + version: 'LATEST', + logGroupName: '/aws/lambda/hello', + timeout: 4, + }, + }) + ) + ).to.be.equal(true); + })); }); }); @@ -1157,69 +1175,72 @@ describe('AwsInvokeLocal', () => { providerVar: 'providerValue', }; awsInvokeLocalMocked.options = { - stage: 'dev', - function: 'first', - functionObj: { + 'stage': 'dev', + 'function': 'first', + 'functionObj': { handler: 'handler.hello', name: 'hello', timeout: 4, - runtime: 'nodejs8.10', + runtime: 'nodejs10.x', environment: { functionVar: 'functionValue', }, }, - data: {}, - env: 'commandLineEnvVar=commandLineEnvVarValue', + 'data': {}, + 'env': 'commandLineEnvVar=commandLineEnvVarValue', 'docker-arg': '-p 9292:9292', }; - pluginMangerSpawnStub = sinon - .stub(serverless.pluginManager, 'spawn'); + pluginMangerSpawnStub = sinon.stub(serverless.pluginManager, 'spawn'); pluginMangerSpawnPackageStub = pluginMangerSpawnStub.withArgs('package').resolves(); }); - afterEach(() => { delete require.cache[require.resolve('./index')]; delete require.cache[require.resolve('child_process')]; serverless.pluginManager.spawn.restore(); + fse.removeSync('.serverless'); }); it('calls docker with packaged artifact', () => awsInvokeLocalMocked.invokeLocalDocker().then(() => { + const dockerfilePath = path.join('.serverless', 'invokeLocal', 'Dockerfile'); + expect(pluginMangerSpawnPackageStub.calledOnce).to.equal(true); expect(spawnStub.getCall(0).args).to.deep.equal(['docker', ['version']]); - expect(spawnStub.getCall(1).args).to.deep.equal(['docker', - ['images', '-q', 'lambci/lambda:nodejs8.10']]); - expect(spawnStub.getCall(2).args).to.deep.equal(['docker', - ['pull', 'lambci/lambda:nodejs8.10']]); - expect(spawnStub.getCall(3).args).to.deep.equal(['docker', [ - 'build', - '-t', - 'sls-docker', - 'servicePath', - '-f', - '.serverless/invokeLocal/Dockerfile', - ]]); - expect(spawnStub.getCall(4).args).to.deep.equal(['docker', [ - 'run', - '--rm', - '-v', - 'servicePath:/var/task', - '--env', - 'providerVar=providerValue', - '--env', - 'functionVar=functionValue', - '--env', - 'commandLineEnvVar=commandLineEnvVarValue', - '-p', - '9292:9292', - 'sls-docker', - 'handler.hello', - '{}', - ]]); - }) - ); + expect(spawnStub.getCall(1).args).to.deep.equal([ + 'docker', + ['images', '-q', 'lambci/lambda:nodejs10.x'], + ]); + expect(spawnStub.getCall(2).args).to.deep.equal([ + 'docker', + ['pull', 'lambci/lambda:nodejs10.x'], + ]); + expect(spawnStub.getCall(3).args).to.deep.equal([ + 'docker', + ['build', '-t', 'sls-docker', 'servicePath', '-f', dockerfilePath], + ]); + expect(spawnStub.getCall(4).args).to.deep.equal([ + 'docker', + [ + 'run', + '--rm', + '-v', + 'servicePath:/var/task', + '--env', + 'providerVar=providerValue', + '--env', + 'functionVar=functionValue', + '--env', + 'commandLineEnvVar=commandLineEnvVarValue', + '-p', + '9292:9292', + 'sls-docker', + 'handler.hello', + '{}', + ], + ]); + })); }); describe('#getEnvVarsFromOptions', () => { diff --git a/lib/plugins/aws/lib/getServiceState.js b/lib/plugins/aws/lib/getServiceState.js index 0feefd37a..fe5b95b01 100644 --- a/lib/plugins/aws/lib/getServiceState.js +++ b/lib/plugins/aws/lib/getServiceState.js @@ -1,3 +1,5 @@ +'use strict'; + const path = require('path'); module.exports = { diff --git a/lib/plugins/aws/lib/getServiceState.test.js b/lib/plugins/aws/lib/getServiceState.test.js index 72dc72462..f7540287f 100644 --- a/lib/plugins/aws/lib/getServiceState.test.js +++ b/lib/plugins/aws/lib/getServiceState.test.js @@ -1,11 +1,15 @@ 'use strict'; -const expect = require('chai').expect; +const path = require('path'); +const chai = require('chai'); const sinon = require('sinon'); const Serverless = require('../../../Serverless'); const AwsProvider = require('../provider/awsProvider'); const getServiceState = require('./getServiceState'); +const expect = chai.expect; +chai.use(require('sinon-chai')); + describe('#getServiceState()', () => { let serverless; let readFileSyncStub; @@ -20,8 +24,7 @@ describe('#getServiceState()', () => { awsPlugin.options = options; Object.assign(awsPlugin, getServiceState); - readFileSyncStub = sinon - .stub(awsPlugin.serverless.utils, 'readFileSync').returns(); + readFileSyncStub = sinon.stub(awsPlugin.serverless.utils, 'readFileSync').returns(); }); afterEach(() => { @@ -29,16 +32,17 @@ describe('#getServiceState()', () => { }); it('should use the default state file path if the "package" option is not used', () => { + const stateFilePath = path.join('my-service', '.serverless', 'serverless-state.json'); awsPlugin.getServiceState(); - expect(readFileSyncStub).to.be - .calledWithExactly('my-service/.serverless/serverless-state.json'); + + expect(readFileSyncStub).to.be.calledWithExactly(stateFilePath); }); it('should use the argument-based state file path if the "package" option is used ', () => { + const stateFilePath = path.join('my-service', 'some-package-path', 'serverless-state.json'); options.package = 'some-package-path'; awsPlugin.getServiceState(); - expect(readFileSyncStub).to.be - .calledWithExactly('my-service/some-package-path/serverless-state.json'); + expect(readFileSyncStub).to.be.calledWithExactly(stateFilePath); }); }); diff --git a/lib/plugins/aws/lib/monitorStack.js b/lib/plugins/aws/lib/monitorStack.js index 5f7186d90..332741527 100644 --- a/lib/plugins/aws/lib/monitorStack.js +++ b/lib/plugins/aws/lib/monitorStack.js @@ -10,11 +10,7 @@ module.exports = { if (cfData === 'alreadyCreated') return BbPromise.bind(this).then(BbPromise.resolve()); // Monitor stack creation/update/removal - const validStatuses = [ - 'CREATE_COMPLETE', - 'UPDATE_COMPLETE', - 'DELETE_COMPLETE', - ]; + const validStatuses = ['CREATE_COMPLETE', 'UPDATE_COMPLETE', 'DELETE_COMPLETE']; const loggedEvents = []; const region = this.provider.getRegion(); const baseCfUrl = `https://${region}.console.aws.amazon.com/cloudformation/home`; @@ -30,30 +26,31 @@ module.exports = { return new BbPromise((resolve, reject) => { async.whilst( - () => (validStatuses.indexOf(stackStatus) === -1), - (callback) => { + () => validStatuses.indexOf(stackStatus) === -1, + callback => { setTimeout(() => { const params = { StackName: cfData.StackId, }; - return this.provider.request('CloudFormation', - 'describeStackEvents', - params) - .then((data) => { + return this.provider + .request('CloudFormation', 'describeStackEvents', params) + .then(data => { const stackEvents = data.StackEvents; // look through all the stack events and find the first relevant // event which is a "Stack" event and has a CREATE, UPDATE or DELETE status - const firstRelevantEvent = stackEvents.find((event) => { + const firstRelevantEvent = stackEvents.find(event => { const isStack = 'AWS::CloudFormation::Stack'; const updateIsInProgress = 'UPDATE_IN_PROGRESS'; const createIsInProgress = 'CREATE_IN_PROGRESS'; const deleteIsInProgress = 'DELETE_IN_PROGRESS'; - return event.ResourceType === isStack - && (event.ResourceStatus === updateIsInProgress - || event.ResourceStatus === createIsInProgress - || event.ResourceStatus === deleteIsInProgress); + return ( + event.ResourceType === isStack && + (event.ResourceStatus === updateIsInProgress || + event.ResourceStatus === createIsInProgress || + event.ResourceStatus === deleteIsInProgress) + ); }); // set the date some time before the first found @@ -65,21 +62,25 @@ module.exports = { } // Loop through stack events - stackEvents.reverse().forEach((event) => { - const eventInRange = (monitoredSince <= event.Timestamp); - const eventNotLogged = (loggedEvents.indexOf(event.EventId) === -1); + stackEvents.reverse().forEach(event => { + const eventInRange = monitoredSince <= event.Timestamp; + const eventNotLogged = loggedEvents.indexOf(event.EventId) === -1; let eventStatus = event.ResourceStatus || null; if (eventInRange && eventNotLogged) { // Keep track of stack status - if (event.ResourceType === 'AWS::CloudFormation::Stack' - && event.StackName === event.LogicalResourceId) { + if ( + event.ResourceType === 'AWS::CloudFormation::Stack' && + event.StackName === event.LogicalResourceId + ) { stackStatus = eventStatus; } // Keep track of first failed event - if (eventStatus - && (eventStatus.endsWith('FAILED') - || eventStatus === 'UPDATE_ROLLBACK_IN_PROGRESS') - && stackLatestError === null) { + if ( + eventStatus && + (eventStatus.endsWith('FAILED') || + eventStatus === 'UPDATE_ROLLBACK_IN_PROGRESS') && + stackLatestError === null + ) { stackLatestError = event; } // Log stack events @@ -103,11 +104,13 @@ module.exports = { } }); // Handle stack create/update/delete failures - if ((stackLatestError && !this.options.verbose) - || (stackStatus - && (stackStatus.endsWith('ROLLBACK_COMPLETE') - || stackStatus === 'DELETE_FAILED') - && this.options.verbose)) { + if ( + (stackLatestError && !this.options.verbose) || + (stackStatus && + (stackStatus.endsWith('ROLLBACK_COMPLETE') || + stackStatus === 'DELETE_FAILED') && + this.options.verbose) + ) { // empty console.log for a prettier output if (!this.options.verbose) this.serverless.cli.consoleLog(''); this.serverless.cli.log('Operation failed!'); @@ -120,7 +123,7 @@ module.exports = { // Trigger next monitoring action return callback(); }) - .catch((e) => { + .catch(e => { if (action === 'removal' && e.message.endsWith('does not exist')) { // empty console.log for a prettier output if (!this.options.verbose) this.serverless.cli.consoleLog(''); @@ -137,7 +140,8 @@ module.exports = { if (!this.options.verbose) this.serverless.cli.consoleLog(''); this.serverless.cli.log(`Stack ${action} finished...`); resolve(stackStatus); - }); + } + ); }); }, }; diff --git a/lib/plugins/aws/lib/monitorStack.test.js b/lib/plugins/aws/lib/monitorStack.test.js index 858851929..427631acb 100644 --- a/lib/plugins/aws/lib/monitorStack.test.js +++ b/lib/plugins/aws/lib/monitorStack.test.js @@ -67,15 +67,13 @@ describe('monitorStack', () => { describeStackEventsStub.onCall(0).resolves(updateStartEvent); describeStackEventsStub.onCall(1).resolves(updateFinishedEvent); - return awsPlugin.monitorStack('create', cfDataMock, 10).then((stackStatus) => { + return awsPlugin.monitorStack('create', cfDataMock, 10).then(stackStatus => { expect(describeStackEventsStub.callCount).to.be.equal(2); - expect(describeStackEventsStub.calledWithExactly( - 'CloudFormation', - 'describeStackEvents', - { + expect( + describeStackEventsStub.calledWithExactly('CloudFormation', 'describeStackEvents', { StackName: cfDataMock.StackId, - } - )).to.be.equal(true); + }) + ).to.be.equal(true); expect(stackStatus).to.be.equal('CREATE_COMPLETE'); awsPlugin.provider.request.restore(); }); @@ -114,15 +112,13 @@ describe('monitorStack', () => { describeStackEventsStub.onCall(0).resolves(updateStartEvent); describeStackEventsStub.onCall(1).resolves(updateFinishedEvent); - return awsPlugin.monitorStack('update', cfDataMock, 10).then((stackStatus) => { + return awsPlugin.monitorStack('update', cfDataMock, 10).then(stackStatus => { expect(describeStackEventsStub.callCount).to.be.equal(2); - expect(describeStackEventsStub.calledWithExactly( - 'CloudFormation', - 'describeStackEvents', - { + expect( + describeStackEventsStub.calledWithExactly('CloudFormation', 'describeStackEvents', { StackName: cfDataMock.StackId, - } - )).to.be.equal(true); + }) + ).to.be.equal(true); expect(stackStatus).to.be.equal('UPDATE_COMPLETE'); awsPlugin.provider.request.restore(); }); @@ -161,15 +157,13 @@ describe('monitorStack', () => { describeStackEventsStub.onCall(0).resolves(updateStartEvent); describeStackEventsStub.onCall(1).resolves(updateFinishedEvent); - return awsPlugin.monitorStack('removal', cfDataMock, 10).then((stackStatus) => { + return awsPlugin.monitorStack('removal', cfDataMock, 10).then(stackStatus => { expect(describeStackEventsStub.callCount).to.be.equal(2); - expect(describeStackEventsStub.calledWithExactly( - 'CloudFormation', - 'describeStackEvents', - { + expect( + describeStackEventsStub.calledWithExactly('CloudFormation', 'describeStackEvents', { StackName: cfDataMock.StackId, - } - )).to.be.equal(true); + }) + ).to.be.equal(true); expect(stackStatus).to.be.equal('DELETE_COMPLETE'); awsPlugin.provider.request.restore(); }); @@ -221,15 +215,13 @@ describe('monitorStack', () => { describeStackEventsStub.onCall(1).resolves(nestedStackEvent); describeStackEventsStub.onCall(2).resolves(updateFinishedEvent); - return awsPlugin.monitorStack('create', cfDataMock, 10).then((stackStatus) => { + return awsPlugin.monitorStack('create', cfDataMock, 10).then(stackStatus => { expect(describeStackEventsStub.callCount).to.be.equal(3); - expect(describeStackEventsStub.calledWithExactly( - 'CloudFormation', - 'describeStackEvents', - { + expect( + describeStackEventsStub.calledWithExactly('CloudFormation', 'describeStackEvents', { StackName: cfDataMock.StackId, - } - )).to.be.equal(true); + }) + ).to.be.equal(true); expect(stackStatus).to.be.equal('CREATE_COMPLETE'); awsPlugin.provider.request.restore(); }); @@ -281,15 +273,13 @@ describe('monitorStack', () => { describeStackEventsStub.onCall(1).resolves(nestedStackEvent); describeStackEventsStub.onCall(2).resolves(updateFinishedEvent); - return awsPlugin.monitorStack('update', cfDataMock, 10).then((stackStatus) => { + return awsPlugin.monitorStack('update', cfDataMock, 10).then(stackStatus => { expect(describeStackEventsStub.callCount).to.be.equal(3); - expect(describeStackEventsStub.calledWithExactly( - 'CloudFormation', - 'describeStackEvents', - { + expect( + describeStackEventsStub.calledWithExactly('CloudFormation', 'describeStackEvents', { StackName: cfDataMock.StackId, - } - )).to.be.equal(true); + }) + ).to.be.equal(true); expect(stackStatus).to.be.equal('UPDATE_COMPLETE'); awsPlugin.provider.request.restore(); }); @@ -341,15 +331,13 @@ describe('monitorStack', () => { describeStackEventsStub.onCall(1).resolves(nestedStackEvent); describeStackEventsStub.onCall(2).resolves(updateFinishedEvent); - return awsPlugin.monitorStack('removal', cfDataMock, 10).then((stackStatus) => { + return awsPlugin.monitorStack('removal', cfDataMock, 10).then(stackStatus => { expect(describeStackEventsStub.callCount).to.be.equal(3); - expect(describeStackEventsStub.calledWithExactly( - 'CloudFormation', - 'describeStackEvents', - { + expect( + describeStackEventsStub.calledWithExactly('CloudFormation', 'describeStackEvents', { StackName: cfDataMock.StackId, - } - )).to.be.equal(true); + }) + ).to.be.equal(true); expect(stackStatus).to.be.equal('DELETE_COMPLETE'); awsPlugin.provider.request.restore(); }); @@ -379,15 +367,13 @@ describe('monitorStack', () => { describeStackEventsStub.onCall(0).resolves(updateStartEvent); describeStackEventsStub.onCall(1).rejects(stackNotFoundError); - return awsPlugin.monitorStack('removal', cfDataMock, 10).then((stackStatus) => { + return awsPlugin.monitorStack('removal', cfDataMock, 10).then(stackStatus => { expect(describeStackEventsStub.callCount).to.be.equal(2); - expect(describeStackEventsStub.calledWithExactly( - 'CloudFormation', - 'describeStackEvents', - { + expect( + describeStackEventsStub.calledWithExactly('CloudFormation', 'describeStackEvents', { StackName: cfDataMock.StackId, - } - )).to.be.equal(true); + }) + ).to.be.equal(true); expect(stackStatus).to.be.equal('DELETE_COMPLETE'); awsPlugin.provider.request.restore(); }); @@ -453,19 +439,17 @@ describe('monitorStack', () => { describeStackEventsStub.onCall(2).resolves(updateRollbackEvent); describeStackEventsStub.onCall(3).resolves(updateRollbackComplete); - return awsPlugin.monitorStack('update', cfDataMock, 10).catch((e) => { + return awsPlugin.monitorStack('update', cfDataMock, 10).catch(e => { let errorMessage = 'An error occurred: '; errorMessage += 'mochaS3 - Bucket already exists.'; expect(e.name).to.be.equal('ServerlessError'); expect(e.message).to.be.equal(errorMessage); expect(describeStackEventsStub.callCount).to.be.equal(4); - expect(describeStackEventsStub.calledWithExactly( - 'CloudFormation', - 'describeStackEvents', - { + expect( + describeStackEventsStub.calledWithExactly('CloudFormation', 'describeStackEvents', { StackName: cfDataMock.StackId, - } - )).to.be.equal(true); + }) + ).to.be.equal(true); awsPlugin.provider.request.restore(); }); }); @@ -516,13 +500,11 @@ describe('monitorStack', () => { return awsPlugin.monitorStack('update', cfDataMock, 10).then(() => { expect(describeStackEventsStub.callCount).to.be.equal(3); - expect(describeStackEventsStub.calledWithExactly( - 'CloudFormation', - 'describeStackEvents', - { + expect( + describeStackEventsStub.calledWithExactly('CloudFormation', 'describeStackEvents', { StackName: cfDataMock.StackId, - } - )).to.be.equal(true); + }) + ).to.be.equal(true); awsPlugin.provider.request.restore(); }); }); @@ -538,16 +520,14 @@ describe('monitorStack', () => { describeStackEventsStub.onCall(0).rejects(failedDescribeStackEvents); - return awsPlugin.monitorStack('update', cfDataMock, 10).catch((e) => { + return awsPlugin.monitorStack('update', cfDataMock, 10).catch(e => { expect(e.message).to.be.equal('Something went wrong.'); expect(describeStackEventsStub.callCount).to.be.equal(1); - expect(describeStackEventsStub.calledWithExactly( - 'CloudFormation', - 'describeStackEvents', - { + expect( + describeStackEventsStub.calledWithExactly('CloudFormation', 'describeStackEvents', { StackName: cfDataMock.StackId, - } - )).to.be.equal(true); + }) + ).to.be.equal(true); awsPlugin.provider.request.restore(); }); }); @@ -608,20 +588,18 @@ describe('monitorStack', () => { describeStackEventsStub.onCall(2).resolves(updateRollbackEvent); describeStackEventsStub.onCall(3).resolves(updateRollbackFailedEvent); - return awsPlugin.monitorStack('update', cfDataMock, 10).catch((e) => { + return awsPlugin.monitorStack('update', cfDataMock, 10).catch(e => { let errorMessage = 'An error occurred: '; errorMessage += 'mochaS3 - Bucket already exists.'; expect(e.name).to.be.equal('ServerlessError'); expect(e.message).to.be.equal(errorMessage); // callCount is 2 because Serverless will immediately exits and shows the error expect(describeStackEventsStub.callCount).to.be.equal(2); - expect(describeStackEventsStub.calledWithExactly( - 'CloudFormation', - 'describeStackEvents', - { + expect( + describeStackEventsStub.calledWithExactly('CloudFormation', 'describeStackEvents', { StackName: cfDataMock.StackId, - } - )).to.be.equal(true); + }) + ).to.be.equal(true); awsPlugin.provider.request.restore(); }); }); @@ -686,103 +664,102 @@ describe('monitorStack', () => { describeStackEventsStub.onCall(2).resolves(deleteItemFailedEvent); describeStackEventsStub.onCall(3).resolves(deleteFailedEvent); - return awsPlugin.monitorStack('removal', cfDataMock, 10).catch((e) => { + return awsPlugin.monitorStack('removal', cfDataMock, 10).catch(e => { let errorMessage = 'An error occurred: '; errorMessage += 'mochaLambda - You are not authorized to perform this operation.'; expect(e.name).to.be.equal('ServerlessError'); expect(e.message).to.be.equal(errorMessage); // callCount is 2 because Serverless will immediately exits and shows the error expect(describeStackEventsStub.callCount).to.be.equal(3); - expect(describeStackEventsStub.calledWithExactly( - 'CloudFormation', - 'describeStackEvents', - { + expect( + describeStackEventsStub.calledWithExactly('CloudFormation', 'describeStackEvents', { StackName: cfDataMock.StackId, - } - )).to.be.equal(true); + }) + ).to.be.equal(true); awsPlugin.provider.request.restore(); }); }); - it('should throw an error if stack status is DELETE_FAILED and should output all ' + - 'stack events information with the --verbose option', () => { - awsPlugin.options.verbose = true; - const describeStackEventsStub = sinon.stub(awsPlugin.provider, 'request'); - const cfDataMock = { - StackId: 'new-service-dev', - }; - const deleteStartEvent = { - StackEvents: [ - { - EventId: '1a2b3c4d', - StackName: 'new-service-dev', - LogicalResourceId: 'new-service-dev', - ResourceType: 'AWS::CloudFormation::Stack', - Timestamp: new Date(), - ResourceStatus: 'DELETE_IN_PROGRESS', - }, - ], - }; - const deleteItemEvent = { - StackEvents: [ - { - EventId: '1e2f3g4h', - StackName: 'new-service-dev', - LogicalResourceId: 'mochaLambda', - ResourceType: 'AWS::Lambda::Function', - Timestamp: new Date(), - ResourceStatus: 'DELETE_IN_PROGRESS', - }, - ], - }; - const deleteItemFailedEvent = { - StackEvents: [ - { - EventId: '1i2j3k4l', - StackName: 'new-service-dev', - LogicalResourceId: 'mochaLambda', - ResourceType: 'AWS::Lambda::Function', - Timestamp: new Date(), - ResourceStatus: 'DELETE_FAILED', - ResourceStatusReason: 'You are not authorized to perform this operation', - }, - ], - }; - const deleteFailedEvent = { - StackEvents: [ - { - EventId: '1m2n3o4p', - StackName: 'new-service-dev', - LogicalResourceId: 'new-service-dev', - ResourceType: 'AWS::CloudFormation::Stack', - Timestamp: new Date(), - ResourceStatus: 'DELETE_FAILED', - }, - ], - }; + it( + 'should throw an error if stack status is DELETE_FAILED and should output all ' + + 'stack events information with the --verbose option', + () => { + awsPlugin.options.verbose = true; + const describeStackEventsStub = sinon.stub(awsPlugin.provider, 'request'); + const cfDataMock = { + StackId: 'new-service-dev', + }; + const deleteStartEvent = { + StackEvents: [ + { + EventId: '1a2b3c4d', + StackName: 'new-service-dev', + LogicalResourceId: 'new-service-dev', + ResourceType: 'AWS::CloudFormation::Stack', + Timestamp: new Date(), + ResourceStatus: 'DELETE_IN_PROGRESS', + }, + ], + }; + const deleteItemEvent = { + StackEvents: [ + { + EventId: '1e2f3g4h', + StackName: 'new-service-dev', + LogicalResourceId: 'mochaLambda', + ResourceType: 'AWS::Lambda::Function', + Timestamp: new Date(), + ResourceStatus: 'DELETE_IN_PROGRESS', + }, + ], + }; + const deleteItemFailedEvent = { + StackEvents: [ + { + EventId: '1i2j3k4l', + StackName: 'new-service-dev', + LogicalResourceId: 'mochaLambda', + ResourceType: 'AWS::Lambda::Function', + Timestamp: new Date(), + ResourceStatus: 'DELETE_FAILED', + ResourceStatusReason: 'You are not authorized to perform this operation', + }, + ], + }; + const deleteFailedEvent = { + StackEvents: [ + { + EventId: '1m2n3o4p', + StackName: 'new-service-dev', + LogicalResourceId: 'new-service-dev', + ResourceType: 'AWS::CloudFormation::Stack', + Timestamp: new Date(), + ResourceStatus: 'DELETE_FAILED', + }, + ], + }; - describeStackEventsStub.onCall(0).resolves(deleteStartEvent); - describeStackEventsStub.onCall(1).resolves(deleteItemEvent); - describeStackEventsStub.onCall(2).resolves(deleteItemFailedEvent); - describeStackEventsStub.onCall(3).resolves(deleteFailedEvent); + describeStackEventsStub.onCall(0).resolves(deleteStartEvent); + describeStackEventsStub.onCall(1).resolves(deleteItemEvent); + describeStackEventsStub.onCall(2).resolves(deleteItemFailedEvent); + describeStackEventsStub.onCall(3).resolves(deleteFailedEvent); - return awsPlugin.monitorStack('removal', cfDataMock, 10).catch((e) => { - let errorMessage = 'An error occurred: '; - errorMessage += 'mochaLambda - You are not authorized to perform this operation.'; - expect(e.name).to.be.equal('ServerlessError'); - expect(e.message).to.be.equal(errorMessage); - // callCount is 2 because Serverless will immediately exits and shows the error - expect(describeStackEventsStub.callCount).to.be.equal(4); - expect(describeStackEventsStub.calledWithExactly( - 'CloudFormation', - 'describeStackEvents', - { - StackName: cfDataMock.StackId, - } - )).to.be.equal(true); - awsPlugin.provider.request.restore(); - }); - }); + return awsPlugin.monitorStack('removal', cfDataMock, 10).catch(e => { + let errorMessage = 'An error occurred: '; + errorMessage += 'mochaLambda - You are not authorized to perform this operation.'; + expect(e.name).to.be.equal('ServerlessError'); + expect(e.message).to.be.equal(errorMessage); + // callCount is 2 because Serverless will immediately exits and shows the error + expect(describeStackEventsStub.callCount).to.be.equal(4); + expect( + describeStackEventsStub.calledWithExactly('CloudFormation', 'describeStackEvents', { + StackName: cfDataMock.StackId, + }) + ).to.be.equal(true); + awsPlugin.provider.request.restore(); + }); + } + ); it('should record an error and fail if status is UPDATE_ROLLBACK_IN_PROGRESS', () => { const describeStackEventsStub = sinon.stub(awsPlugin.provider, 'request'); @@ -828,20 +805,18 @@ describe('monitorStack', () => { describeStackEventsStub.onCall(1).resolves(updateRollbackEvent); describeStackEventsStub.onCall(2).resolves(updateRollbackCompleteEvent); - return awsPlugin.monitorStack('update', cfDataMock, 10).catch((e) => { + return awsPlugin.monitorStack('update', cfDataMock, 10).catch(e => { let errorMessage = 'An error occurred: '; errorMessage += 'mocha - Export is in use.'; expect(e.name).to.be.equal('ServerlessError'); expect(e.message).to.be.equal(errorMessage); // callCount is 2 because Serverless will immediately exits and shows the error expect(describeStackEventsStub.callCount).to.be.equal(2); - expect(describeStackEventsStub.calledWithExactly( - 'CloudFormation', - 'describeStackEvents', - { + expect( + describeStackEventsStub.calledWithExactly('CloudFormation', 'describeStackEvents', { StackName: cfDataMock.StackId, - } - )).to.be.equal(true); + }) + ).to.be.equal(true); awsPlugin.provider.request.restore(); }); }); diff --git a/lib/plugins/aws/lib/naming.js b/lib/plugins/aws/lib/naming.js index 7ad93bc53..fb863e60c 100644 --- a/lib/plugins/aws/lib/naming.js +++ b/lib/plugins/aws/lib/naming.js @@ -1,6 +1,7 @@ 'use strict'; const _ = require('lodash'); +const crypto = require('crypto'); /** * A centralized naming object that provides naming standards for the AWS provider plugin. The @@ -23,7 +24,6 @@ const _ = require('lodash'); * That is to say that they are easier to change together. */ module.exports = { - // General normalizeName(name) { return `${_.upperFirst(name)}`; @@ -46,8 +46,10 @@ module.exports = { // Stack getStackName() { - if (this.provider.serverless.service.provider.stackName && - _.isString(this.provider.serverless.service.provider.stackName)) { + if ( + this.provider.serverless.service.provider.stackName && + _.isString(this.provider.serverless.service.provider.stackName) + ) { return `${this.provider.serverless.service.provider.stackName}`; } return `${this.provider.serverless.service.service}-${this.provider.getStage()}`; @@ -100,7 +102,8 @@ module.exports = { // Policy getPolicyName() { - return { // TODO should probably have name ordered and altered as above - see AWS docs + return { + // TODO should probably have name ordered and altered as above - see AWS docs 'Fn::Join': [ '-', [ @@ -123,9 +126,7 @@ module.exports = { // Lambda getNormalizedFunctionName(functionName) { - return this.normalizeName(functionName - .replace(/-/g, 'Dash') - .replace(/_/g, 'Underscore')); + return this.normalizeName(functionName.replace(/-/g, 'Dash').replace(/_/g, 'Underscore')); }, extractLambdaNameFromArn(functionArn) { return functionArn.substring(functionArn.lastIndexOf(':') + 1); @@ -144,15 +145,19 @@ module.exports = { return `${this.getNormalizedFunctionName(layerName)}LambdaLayer`; }, getLambdaLayerPermissionLogicalId(layerName, account) { - return `${this.getNormalizedFunctionName(layerName)}${ - account.replace('*', 'Wild')}LambdaLayerPermission`; + return `${this.getNormalizedFunctionName(layerName)}${account.replace( + '*', + 'Wild' + )}LambdaLayerPermission`; }, getLambdaLogicalIdRegex() { return /LambdaFunction$/; }, getLambdaVersionLogicalId(functionName, sha) { - return `${this.getNormalizedFunctionName(functionName)}LambdaVersion${sha - .replace(/[^0-9a-z]/gi, '')}`; + return `${this.getNormalizedFunctionName(functionName)}LambdaVersion${sha.replace( + /[^0-9a-z]/gi, + '' + )}`; }, getLambdaVersionOutputLogicalId(functionName) { return `${this.getLambdaLogicalId(functionName)}QualifiedArn`; @@ -163,8 +168,10 @@ module.exports = { // Websockets API getWebsocketsApiName() { - if (this.provider.serverless.service.provider.websocketsApiName && - _.isString(this.provider.serverless.service.provider.websocketsApiName)) { + if ( + this.provider.serverless.service.provider.websocketsApiName && + _.isString(this.provider.serverless.service.provider.websocketsApiName) + ) { return `${this.provider.serverless.service.provider.websocketsApiName}`; } return `${this.provider.getStage()}-${this.provider.serverless.service.service}-websockets`; @@ -204,11 +211,22 @@ module.exports = { getWebsocketsAuthorizerLogicalId(functionName) { return `${this.getNormalizedAuthorizerName(functionName)}WebsocketsAuthorizer`; }, + getWebsocketsLogGroupLogicalId() { + return 'WebsocketsLogGroup'; + }, + getWebsocketsLogsRoleLogicalId() { + return 'IamRoleWebsocketsLogs'; + }, + getWebsocketsAccountLogicalId() { + return 'WebsocketsAccount'; + }, // API Gateway getApiGatewayName() { - if (this.provider.serverless.service.provider.apiName && - _.isString(this.provider.serverless.service.provider.apiName)) { + if ( + this.provider.serverless.service.provider.apiName && + _.isString(this.provider.serverless.service.provider.apiName) + ) { return `${this.provider.serverless.service.provider.apiName}`; } return `${this.provider.getStage()}-${this.provider.serverless.service.service}`; @@ -226,9 +244,10 @@ module.exports = { return `${this.getNormalizedAuthorizerName(functionName)}ApiGatewayAuthorizer`; }, normalizePath(resourcePath) { - return resourcePath.split('/').map( - this.normalizePathPart.bind(this) - ).join(''); + return resourcePath + .split('/') + .map(this.normalizePathPart.bind(this)) + .join(''); }, getResourceLogicalId(resourcePath) { return `ApiGatewayResource${this.normalizePath(resourcePath)}`; @@ -246,8 +265,10 @@ module.exports = { return `${this.getMethodLogicalId(resourceId, methodName)}Validator`; }, getModelLogicalId(resourceId, methodName, contentType) { - return `${this.getMethodLogicalId(resourceId, methodName)}${_.startCase( - contentType).replace(' ', '')}Model`; + return `${this.getMethodLogicalId(resourceId, methodName)}${_.startCase(contentType).replace( + ' ', + '' + )}Model`; }, getApiKeyLogicalId(apiKeyNumber, apiKeyName) { if (apiKeyName) { @@ -315,11 +336,9 @@ module.exports = { // Stream getStreamLogicalId(functionName, streamType, streamName) { - return `${ - this.getNormalizedFunctionName(functionName) - }EventSourceMapping${ - this.normalizeName(streamType) - }${this.normalizeNameToAlphaNumericOnly(streamName)}`; + return `${this.getNormalizedFunctionName(functionName)}EventSourceMapping${this.normalizeName( + streamType + )}${this.normalizeNameToAlphaNumericOnly(streamName)}`; }, // IoT @@ -327,8 +346,7 @@ module.exports = { return `${this.getNormalizedFunctionName(functionName)}IotTopicRule${iotIndex}`; }, getLambdaIotPermissionLogicalId(functionName, iotIndex) { - return `${this.getNormalizedFunctionName(functionName)}LambdaPermissionIotTopicRule${ - iotIndex}`; + return `${this.getNormalizedFunctionName(functionName)}LambdaPermissionIotTopicRule${iotIndex}`; }, // CloudWatch Event @@ -336,14 +354,16 @@ module.exports = { return `${functionName}CloudWatchEvent`; }, getCloudWatchEventLogicalId(functionName, cloudWatchIndex) { - return `${this - .getNormalizedFunctionName(functionName)}EventsRuleCloudWatchEvent${cloudWatchIndex}`; + return `${this.getNormalizedFunctionName( + functionName + )}EventsRuleCloudWatchEvent${cloudWatchIndex}`; }, // CloudWatch Log getCloudWatchLogLogicalId(functionName, logsIndex) { - return `${this - .getNormalizedFunctionName(functionName)}LogsSubscriptionFilterCloudWatchLog${logsIndex}`; + return `${this.getNormalizedFunctionName( + functionName + )}LogsSubscriptionFilterCloudWatchLog${logsIndex}`; }, // Cognito User Pool @@ -353,53 +373,102 @@ module.exports = { // SQS getQueueLogicalId(functionName, queueName) { + return `${this.getNormalizedFunctionName( + functionName + )}EventSourceMappingSQS${this.normalizeNameToAlphaNumericOnly(queueName)}`; + }, + + // ALB + getAlbTargetGroupLogicalId(functionName) { + return `${this.getNormalizedFunctionName(functionName)}AlbTargetGroup`; + }, + getAlbTargetGroupNameTagValue(functionName) { return `${ - this.getNormalizedFunctionName(functionName) - }EventSourceMappingSQS${ - this.normalizeNameToAlphaNumericOnly(queueName) - }`; + this.provider.serverless.service.service + }-${functionName}-${this.provider.getStage()}`; + }, + getAlbTargetGroupName(functionName) { + return crypto + .createHash('md5') + .update(this.getAlbTargetGroupNameTagValue(functionName)) + .digest('hex'); + }, + getAlbListenerRuleLogicalId(functionName, idx) { + return `${this.getNormalizedFunctionName(functionName)}AlbListenerRule${idx}`; }, // Permissions getLambdaS3PermissionLogicalId(functionName, bucketName) { - return `${this.getNormalizedFunctionName(functionName)}LambdaPermission${this - .normalizeBucketName(bucketName)}S3`; + return `${this.getNormalizedFunctionName( + functionName + )}LambdaPermission${this.normalizeBucketName(bucketName)}S3`; }, getLambdaSnsPermissionLogicalId(functionName, topicName) { - return `${this.getNormalizedFunctionName(functionName)}LambdaPermission${ - this.normalizeTopicName(topicName)}SNS`; + return `${this.getNormalizedFunctionName( + functionName + )}LambdaPermission${this.normalizeTopicName(topicName)}SNS`; }, getLambdaSnsSubscriptionLogicalId(functionName, topicName) { - return `${this.getNormalizedFunctionName(functionName)}SnsSubscription${ - this.normalizeTopicName(topicName)}`; + return `${this.getNormalizedFunctionName(functionName)}SnsSubscription${this.normalizeTopicName( + topicName + )}`; }, getLambdaSchedulePermissionLogicalId(functionName, scheduleIndex) { - return `${this.getNormalizedFunctionName(functionName)}LambdaPermissionEventsRuleSchedule${ - scheduleIndex}`; + return `${this.getNormalizedFunctionName( + functionName + )}LambdaPermissionEventsRuleSchedule${scheduleIndex}`; }, getLambdaCloudWatchEventPermissionLogicalId(functionName, cloudWatchIndex) { - return `${this - .getNormalizedFunctionName(functionName)}LambdaPermissionEventsRuleCloudWatchEvent${ - cloudWatchIndex}`; + return `${this.getNormalizedFunctionName( + functionName + )}LambdaPermissionEventsRuleCloudWatchEvent${cloudWatchIndex}`; }, getLambdaApiGatewayPermissionLogicalId(functionName) { return `${this.getNormalizedFunctionName(functionName)}LambdaPermissionApiGateway`; }, getLambdaAlexaSkillPermissionLogicalId(functionName, alexaSkillIndex) { - return `${this.getNormalizedFunctionName(functionName)}LambdaPermissionAlexaSkill${ - alexaSkillIndex || '0'}`; + return `${this.getNormalizedFunctionName( + functionName + )}LambdaPermissionAlexaSkill${alexaSkillIndex || '0'}`; }, getLambdaAlexaSmartHomePermissionLogicalId(functionName, alexaSmartHomeIndex) { - return `${this.getNormalizedFunctionName(functionName)}LambdaPermissionAlexaSmartHome${ - alexaSmartHomeIndex}`; + return `${this.getNormalizedFunctionName( + functionName + )}LambdaPermissionAlexaSmartHome${alexaSmartHomeIndex}`; }, getLambdaCloudWatchLogPermissionLogicalId(functionName) { - return `${this.getNormalizedFunctionName(functionName) - }LambdaPermissionLogsSubscriptionFilterCloudWatchLog`; + return `${this.getNormalizedFunctionName( + functionName + )}LambdaPermissionLogsSubscriptionFilterCloudWatchLog`; }, getLambdaCognitoUserPoolPermissionLogicalId(functionName, poolId, triggerSource) { - return `${this - .getNormalizedFunctionName(functionName)}LambdaPermissionCognitoUserPool${ - this.normalizeNameToAlphaNumericOnly(poolId)}TriggerSource${triggerSource}`; + return `${this.getNormalizedFunctionName( + functionName + )}LambdaPermissionCognitoUserPool${this.normalizeNameToAlphaNumericOnly( + poolId + )}TriggerSource${triggerSource}`; + }, + getLambdaAlbPermissionLogicalId(functionName) { + return `${this.getNormalizedFunctionName(functionName)}LambdaPermissionAlb`; + }, + + // Custom Resources + getCustomResourcesArtifactDirectoryName() { + return 'custom-resources'; + }, + getCustomResourcesRoleLogicalId() { + return 'IamRoleCustomResourcesLambdaExecution'; + }, + // S3 + getCustomResourceS3HandlerFunctionName() { + return 'custom-resource-existing-s3'; + }, + getCustomResourceS3HandlerFunctionLogicalId() { + return this.getLambdaLogicalId( + `${this.getNormalizedFunctionName(this.getCustomResourceS3HandlerFunctionName())}` + ); + }, + getCustomResourceS3ResourceLogicalId(functionName, idx) { + return `${this.getNormalizedFunctionName(functionName)}CustomS3${idx}`; }, }; diff --git a/lib/plugins/aws/lib/naming.test.js b/lib/plugins/aws/lib/naming.test.js index 78f9a6b23..bbc9699e2 100644 --- a/lib/plugins/aws/lib/naming.test.js +++ b/lib/plugins/aws/lib/naming.test.js @@ -35,8 +35,9 @@ describe('#naming()', () => { describe('#normalizeNameToAlphaNumericOnly()', () => { it('should strip non-alpha-numeric characters', () => { - expect(sdk.naming - .normalizeNameToAlphaNumericOnly('`!@#$%^&*()-={}|[]\\:";\'<>?,./')).to.equal(''); + expect( + sdk.naming.normalizeNameToAlphaNumericOnly('`!@#$%^&*()-={}|[]\\:";\'<>?,./') + ).to.equal(''); }); it('should apply normalizeName to the remaining characters', () => { @@ -46,58 +47,50 @@ describe('#naming()', () => { describe('#normalizePathPart()', () => { it('converts `-` to `Dash`', () => { - expect(sdk.naming.normalizePathPart( - 'a-path' - )).to.equal('ADashpath'); + expect(sdk.naming.normalizePathPart('a-path')).to.equal('ADashpath'); }); it('converts variable declarations (`${var}`) to `VariableVar`', () => { - expect(sdk.naming.normalizePathPart( - '${variable}' - )).to.equal('VariableVar'); + expect(sdk.naming.normalizePathPart('${variable}')).to.equal('VariableVar'); }); it('converts variable declarations prefixes to `VariableVarpath`', () => { - expect(sdk.naming.normalizePathPart( - '${variable}Path' - )).to.equal('VariableVarpath'); + expect(sdk.naming.normalizePathPart('${variable}Path')).to.equal('VariableVarpath'); }); it('converts variable declarations suffixes to `PathvariableVar`', () => { - expect(sdk.naming.normalizePathPart( - 'path${variable}' - )).to.equal('PathvariableVar'); + expect(sdk.naming.normalizePathPart('path${variable}')).to.equal('PathvariableVar'); }); it('converts variable declarations in center to `PathvariableVardir`', () => { - expect(sdk.naming.normalizePathPart( - 'path${variable}Dir' - )).to.equal('PathvariableVardir'); + expect(sdk.naming.normalizePathPart('path${variable}Dir')).to.equal('PathvariableVardir'); }); }); describe('#getServiceEndpointRegex()', () => { it('should match the prefix', () => { - expect(sdk.naming.getServiceEndpointRegex().test('ServiceEndpoint')) - .to.equal(true); + expect(sdk.naming.getServiceEndpointRegex().test('ServiceEndpoint')).to.equal(true); }); it('should not match a name without the prefix', () => { - expect(sdk.naming.getServiceEndpointRegex() - .test('NotThePrefixServiceEndpoint')).to.equal(false); + expect(sdk.naming.getServiceEndpointRegex().test('NotThePrefixServiceEndpoint')).to.equal( + false + ); }); it('should match a name with the prefix', () => { - expect(sdk.naming.getServiceEndpointRegex() - .test('ServiceEndpointForAService')).to.equal(true); + expect(sdk.naming.getServiceEndpointRegex().test('ServiceEndpointForAService')).to.equal( + true + ); }); }); describe('#getStackName()', () => { it('should use the service name & stage if custom stack name not provided', () => { serverless.service.service = 'myService'; - expect(sdk.naming.getStackName()).to.equal(`${serverless.service.service}-${ - sdk.naming.provider.getStage()}`); + expect(sdk.naming.getStackName()).to.equal( + `${serverless.service.service}-${sdk.naming.provider.getStage()}` + ); }); it('should use the custom stack name if provided', () => { @@ -141,14 +134,7 @@ describe('#naming()', () => { it('should use the stage and service name', () => { serverless.service.service = 'myService'; expect(sdk.naming.getPolicyName()).to.eql({ - 'Fn::Join': [ - '-', - [ - sdk.naming.provider.getStage(), - serverless.service.service, - 'lambda', - ], - ], + 'Fn::Join': ['-', [sdk.naming.provider.getStage(), serverless.service.service, 'lambda']], }); }); }); @@ -167,18 +153,15 @@ describe('#naming()', () => { describe('#getNormalizedFunctionName()', () => { it('should normalize the given functionName', () => { - expect(sdk.naming.getNormalizedFunctionName('functionName')) - .to.equal('FunctionName'); + expect(sdk.naming.getNormalizedFunctionName('functionName')).to.equal('FunctionName'); }); it('should normalize the given functionName with an underscore', () => { - expect(sdk.naming.getNormalizedFunctionName('hello_world')) - .to.equal('HelloUnderscoreworld'); + expect(sdk.naming.getNormalizedFunctionName('hello_world')).to.equal('HelloUnderscoreworld'); }); it('should normalize the given functionName with a dash', () => { - expect(sdk.naming.getNormalizedFunctionName('hello-world')) - .to.equal('HelloDashworld'); + expect(sdk.naming.getNormalizedFunctionName('hello-world')).to.equal('HelloDashworld'); }); }); @@ -198,33 +181,34 @@ describe('#naming()', () => { describe('#getLambdaLogicalId()', () => { it('should normalize the function name and add the logical suffix', () => { - expect(sdk.naming.getLambdaLogicalId('functionName')) - .to.equal('FunctionNameLambdaFunction'); + expect(sdk.naming.getLambdaLogicalId('functionName')).to.equal('FunctionNameLambdaFunction'); }); }); describe('#getLambdaLogicalIdRegex()', () => { it('should match the suffix', () => { - expect(sdk.naming.getLambdaLogicalIdRegex() - .test('LambdaFunction')).to.equal(true); + expect(sdk.naming.getLambdaLogicalIdRegex().test('LambdaFunction')).to.equal(true); }); it('should not match a name without the suffix', () => { - expect(sdk.naming.getLambdaLogicalIdRegex() - .test('LambdaFunctionNotTheSuffix')).to.equal(false); + expect(sdk.naming.getLambdaLogicalIdRegex().test('LambdaFunctionNotTheSuffix')).to.equal( + false + ); }); it('should match a name with the suffix', () => { - expect(sdk.naming.getLambdaLogicalIdRegex() - .test('AFunctionNameLambdaFunction')).to.equal(true); + expect(sdk.naming.getLambdaLogicalIdRegex().test('AFunctionNameLambdaFunction')).to.equal( + true + ); }); }); describe('#getWebsocketsApiName()', () => { it('should return the composition of stage & service name if custom name not provided', () => { serverless.service.service = 'myService'; - expect(sdk.naming.getWebsocketsApiName()) - .to.equal(`${sdk.naming.provider.getStage()}-${serverless.service.service}-websockets`); + expect(sdk.naming.getWebsocketsApiName()).to.equal( + `${sdk.naming.provider.getStage()}-${serverless.service.service}-websockets` + ); }); it('should return the custom api name if provided', () => { @@ -243,70 +227,88 @@ describe('#naming()', () => { describe('#getWebsocketsIntegrationLogicalId()', () => { it('should return the integrations logical id', () => { - expect(sdk.naming.getWebsocketsIntegrationLogicalId('myFunc')) - .to.equal('MyFuncWebsocketsIntegration'); + expect(sdk.naming.getWebsocketsIntegrationLogicalId('myFunc')).to.equal( + 'MyFuncWebsocketsIntegration' + ); }); }); describe('#getLambdaWebsocketsPermissionLogicalId()', () => { it('should return the lambda websocket permission logical id', () => { - expect(sdk.naming.getLambdaWebsocketsPermissionLogicalId('myFunc')) - .to.equal('MyFuncLambdaPermissionWebsockets'); + expect(sdk.naming.getLambdaWebsocketsPermissionLogicalId('myFunc')).to.equal( + 'MyFuncLambdaPermissionWebsockets' + ); }); }); describe('#getNormalizedWebsocketsRouteKey()', () => { it('should return a normalized version of the route key', () => { - expect(sdk.naming.getNormalizedWebsocketsRouteKey('$connect')) - .to.equal('Sconnect'); + expect(sdk.naming.getNormalizedWebsocketsRouteKey('$connect')).to.equal('Sconnect'); - expect(sdk.naming.getNormalizedWebsocketsRouteKey('foo/bar')) - .to.equal('fooSlashbar'); + expect(sdk.naming.getNormalizedWebsocketsRouteKey('foo/bar')).to.equal('fooSlashbar'); - expect(sdk.naming.getNormalizedWebsocketsRouteKey('foo-bar')) - .to.equal('fooDashbar'); + expect(sdk.naming.getNormalizedWebsocketsRouteKey('foo-bar')).to.equal('fooDashbar'); - expect(sdk.naming.getNormalizedWebsocketsRouteKey('foo_bar')) - .to.equal('fooUnderscorebar'); + expect(sdk.naming.getNormalizedWebsocketsRouteKey('foo_bar')).to.equal('fooUnderscorebar'); - expect(sdk.naming.getNormalizedWebsocketsRouteKey('foo.bar')) - .to.equal('fooPeriodbar'); + expect(sdk.naming.getNormalizedWebsocketsRouteKey('foo.bar')).to.equal('fooPeriodbar'); }); }); describe('#getWebsocketsRouteLogicalId()', () => { it('should return the websockets route logical id', () => { - expect(sdk.naming.getWebsocketsRouteLogicalId('$connect')) - .to.equal('SconnectWebsocketsRoute'); + expect(sdk.naming.getWebsocketsRouteLogicalId('$connect')).to.equal( + 'SconnectWebsocketsRoute' + ); }); }); describe('#getWebsocketsDeploymentLogicalId()', () => { it('should return the websockets deployment logical id', () => { - expect(sdk.naming.getWebsocketsDeploymentLogicalId(1234)) - .to.equal('WebsocketsDeployment1234'); + expect(sdk.naming.getWebsocketsDeploymentLogicalId(1234)).to.equal( + 'WebsocketsDeployment1234' + ); }); }); describe('#getWebsocketsStageLogicalId()', () => { it('should return the websockets stage logical id', () => { - expect(sdk.naming.getWebsocketsStageLogicalId()) - .to.equal('WebsocketsDeploymentStage'); + expect(sdk.naming.getWebsocketsStageLogicalId()).to.equal('WebsocketsDeploymentStage'); }); }); describe('#getWebsocketsAuthorizerLogicalId()', () => { it('should return the websockets authorizer logical id', () => { - expect(sdk.naming.getWebsocketsAuthorizerLogicalId('auth')) - .to.equal('AuthWebsocketsAuthorizer'); + expect(sdk.naming.getWebsocketsAuthorizerLogicalId('auth')).to.equal( + 'AuthWebsocketsAuthorizer' + ); + }); + }); + + describe('#getWebsocketsLogGroupLogicalId()', () => { + it('should return the Websockets log group logical id', () => { + expect(sdk.naming.getWebsocketsLogGroupLogicalId()).to.equal('WebsocketsLogGroup'); + }); + }); + + describe('#getWebsocketsLogsRoleLogicalId()', () => { + it('should return the Websockets logs IAM role logical id', () => { + expect(sdk.naming.getWebsocketsLogsRoleLogicalId()).to.equal('IamRoleWebsocketsLogs'); + }); + }); + + describe('#getWebsocketsAccountLogicalId()', () => { + it('should return the Websockets account logical id', () => { + expect(sdk.naming.getWebsocketsAccountLogicalId()).to.equal('WebsocketsAccount'); }); }); describe('#getApiGatewayName()', () => { it('should return the composition of stage & service name if custom name not provided', () => { serverless.service.service = 'myService'; - expect(sdk.naming.getApiGatewayName()) - .to.equal(`${sdk.naming.provider.getStage()}-${serverless.service.service}`); + expect(sdk.naming.getApiGatewayName()).to.equal( + `${sdk.naming.provider.getStage()}-${serverless.service.service}` + ); }); it('should return the custom api name if provided', () => { @@ -319,8 +321,9 @@ describe('#naming()', () => { describe('#generateApiGatewayDeploymentLogicalId()', () => { it('should return ApiGatewayDeployment with a suffix', () => { - expect(sdk.naming.generateApiGatewayDeploymentLogicalId(1234)) - .to.equal('ApiGatewayDeployment1234'); + expect(sdk.naming.generateApiGatewayDeploymentLogicalId(1234)).to.equal( + 'ApiGatewayDeployment1234' + ); }); }); @@ -332,15 +335,15 @@ describe('#naming()', () => { describe('#getNormalizedAuthorizerName()', () => { it('normalize the authorizer name', () => { - expect(sdk.naming.getNormalizedAuthorizerName('authorizerName')) - .to.equal('AuthorizerName'); + expect(sdk.naming.getNormalizedAuthorizerName('authorizerName')).to.equal('AuthorizerName'); }); }); describe('#getAuthorizerLogicalId()', () => { it('should normalize the authorizer name and add the standard suffix', () => { - expect(sdk.naming.getAuthorizerLogicalId('authorizerName')) - .to.equal('AuthorizerNameApiGatewayAuthorizer'); + expect(sdk.naming.getAuthorizerLogicalId('authorizerName')).to.equal( + 'AuthorizerNameApiGatewayAuthorizer' + ); }); }); @@ -352,27 +355,26 @@ describe('#naming()', () => { }); describe('#normalizePath()', () => { - it('should normalize each part of the resource path and remove non-alpha-numeric characters', - () => { - expect(sdk.naming.normalizePath( - 'my/path/to/a-${var}-resource' - )).to.equal('MyPathToADashvarVarDashresource'); - }); + it('should normalize each part of the resource path and remove non-alpha-numeric characters', () => { + expect(sdk.naming.normalizePath('my/path/to/a-${var}-resource')).to.equal( + 'MyPathToADashvarVarDashresource' + ); + }); }); describe('#getResourceLogicalId()', () => { it('should normalize the resource and add the standard suffix', () => { - expect(sdk.naming.getResourceLogicalId( - 'my/path/to/a-${var}-resource' - )).to.equal('ApiGatewayResourceMyPathToADashvarVarDashresource'); + expect(sdk.naming.getResourceLogicalId('my/path/to/a-${var}-resource')).to.equal( + 'ApiGatewayResourceMyPathToADashvarVarDashresource' + ); }); }); describe('#extractResourceId()', () => { it('should extract the normalized resource name', () => { - expect(sdk.naming.extractResourceId( - 'ApiGatewayResourceMyPathToADashvarVarDashResource' - )).to.equal('MyPathToADashvarVarDashResource'); + expect( + sdk.naming.extractResourceId('ApiGatewayResourceMyPathToADashvarVarDashResource') + ).to.equal('MyPathToADashvarVarDashResource'); }); }); @@ -384,25 +386,25 @@ describe('#naming()', () => { describe('#getMethodLogicalId()', () => { it('', () => { - expect(sdk.naming.getMethodLogicalId( - 'ResourceId', 'get' - )).to.equal('ApiGatewayMethodResourceIdGet'); + expect(sdk.naming.getMethodLogicalId('ResourceId', 'get')).to.equal( + 'ApiGatewayMethodResourceIdGet' + ); }); }); describe('#getValidatorLogicalId()', () => { it('', () => { - expect(sdk.naming.getValidatorLogicalId( - 'ResourceId', 'get' - )).to.equal('ApiGatewayMethodResourceIdGetValidator'); + expect(sdk.naming.getValidatorLogicalId('ResourceId', 'get')).to.equal( + 'ApiGatewayMethodResourceIdGetValidator' + ); }); }); describe('#getModelLogicalId()', () => { it('', () => { - expect(sdk.naming.getModelLogicalId( - 'ResourceId', 'get', 'application/json' - )).to.equal('ApiGatewayMethodResourceIdGetApplicationJsonModel'); + expect(sdk.naming.getModelLogicalId('ResourceId', 'get', 'application/json')).to.equal( + 'ApiGatewayMethodResourceIdGetApplicationJsonModel' + ); }); }); @@ -418,30 +420,27 @@ describe('#naming()', () => { describe('#getApiKeyLogicalIdRegex()', () => { it('should match the prefix', () => { - expect(sdk.naming.getApiKeyLogicalIdRegex() - .test('ApiGatewayApiKey')).to.equal(true); + expect(sdk.naming.getApiKeyLogicalIdRegex().test('ApiGatewayApiKey')).to.equal(true); }); it('should not match a name without the prefix', () => { - expect(sdk.naming.getApiKeyLogicalIdRegex() - .test('NotThePrefixApiGatewayApiKey')).to.equal(false); + expect(sdk.naming.getApiKeyLogicalIdRegex().test('NotThePrefixApiGatewayApiKey')).to.equal( + false + ); }); it('should match a name with the prefix', () => { - expect(sdk.naming.getApiKeyLogicalIdRegex() - .test('ApiGatewayApiKeySuffix')).to.equal(true); + expect(sdk.naming.getApiKeyLogicalIdRegex().test('ApiGatewayApiKeySuffix')).to.equal(true); }); }); describe('#getUsagePlanLogicalId()', () => { it('should return the default ApiGateway usage plan logical id', () => { - expect(sdk.naming.getUsagePlanLogicalId()) - .to.equal('ApiGatewayUsagePlan'); + expect(sdk.naming.getUsagePlanLogicalId()).to.equal('ApiGatewayUsagePlan'); }); it('should return the named ApiGateway usage plan logical id', () => { - expect(sdk.naming.getUsagePlanLogicalId('free')) - .to.equal('ApiGatewayUsagePlanFree'); + expect(sdk.naming.getUsagePlanLogicalId('free')).to.equal('ApiGatewayUsagePlanFree'); }); }); @@ -451,8 +450,9 @@ describe('#naming()', () => { }); it('should support API Key names', () => { - expect(sdk.naming.getUsagePlanKeyLogicalId(1, 'free')) - .to.equal('ApiGatewayUsagePlanKeyFree1'); + expect(sdk.naming.getUsagePlanKeyLogicalId(1, 'free')).to.equal( + 'ApiGatewayUsagePlanKeyFree1' + ); }); }); @@ -488,8 +488,9 @@ describe('#naming()', () => { describe('#getDeploymentBucketOutputLogicalId()', () => { it('should return "ServerlessDeploymentBucketName"', () => { - expect(sdk.naming.getDeploymentBucketOutputLogicalId()) - .to.equal('ServerlessDeploymentBucketName'); + expect(sdk.naming.getDeploymentBucketOutputLogicalId()).to.equal( + 'ServerlessDeploymentBucketName' + ); }); }); @@ -525,143 +526,239 @@ describe('#naming()', () => { describe('#getScheduleLogicalId()', () => { it('should normalize the function name and add the standard suffix including the index', () => { - expect(sdk.naming.getScheduleLogicalId('functionName', 0)) - .to.equal('FunctionNameEventsRuleSchedule0'); + expect(sdk.naming.getScheduleLogicalId('functionName', 0)).to.equal( + 'FunctionNameEventsRuleSchedule0' + ); }); }); describe('#getCloudWatchEventId()', () => { it('should add the standard suffix', () => { - expect(sdk.naming.getCloudWatchEventId('functionName')) - .to.equal('functionNameCloudWatchEvent'); + expect(sdk.naming.getCloudWatchEventId('functionName')).to.equal( + 'functionNameCloudWatchEvent' + ); }); }); describe('#getCloudWatchEventLogicalId()', () => { it('should normalize the function name and add the standard suffix including the index', () => { - expect(sdk.naming.getCloudWatchEventLogicalId('functionName', 0)) - .to.equal('FunctionNameEventsRuleCloudWatchEvent0'); + expect(sdk.naming.getCloudWatchEventLogicalId('functionName', 0)).to.equal( + 'FunctionNameEventsRuleCloudWatchEvent0' + ); }); }); describe('#getCloudWatchLogLogicalId()', () => { it('should normalize the function name and add the standard suffix including the index', () => { - expect(sdk.naming.getCloudWatchLogLogicalId('functionName', 0)) - .to.equal('FunctionNameLogsSubscriptionFilterCloudWatchLog0'); + expect(sdk.naming.getCloudWatchLogLogicalId('functionName', 0)).to.equal( + 'FunctionNameLogsSubscriptionFilterCloudWatchLog0' + ); }); }); describe('#getCognitoUserPoolLogicalId()', () => { it('should normalize the user pool name and add the standard prefix', () => { - expect(sdk.naming.getCognitoUserPoolLogicalId('us-east-1_v123sDAS1')) - .to.equal('CognitoUserPoolUseast1v123sDAS1'); + expect(sdk.naming.getCognitoUserPoolLogicalId('us-east-1_v123sDAS1')).to.equal( + 'CognitoUserPoolUseast1v123sDAS1' + ); }); }); describe('#getLambdaS3PermissionLogicalId()', () => { it('should normalize the function name and add the standard suffix', () => { - expect(sdk.naming.getLambdaS3PermissionLogicalId('functionName', 'bucket')) - .to.equal('FunctionNameLambdaPermissionBucketS3'); + expect(sdk.naming.getLambdaS3PermissionLogicalId('functionName', 'bucket')).to.equal( + 'FunctionNameLambdaPermissionBucketS3' + ); }); }); describe('#getLambdaSnsPermissionLogicalId()', () => { - it('should normalize the function and topic names and add them as prefix and suffix to the ' + - 'standard permission center', () => { - expect(sdk.naming.getLambdaSnsPermissionLogicalId('functionName', 'topic')) - .to.equal('FunctionNameLambdaPermissionTopicSNS'); - }); + it( + 'should normalize the function and topic names and add them as prefix and suffix to the ' + + 'standard permission center', + () => { + expect(sdk.naming.getLambdaSnsPermissionLogicalId('functionName', 'topic')).to.equal( + 'FunctionNameLambdaPermissionTopicSNS' + ); + } + ); }); describe('#getLambdaSchedulePermissionLogicalId()', () => { - it('should normalize the function name and add the standard suffix including event index', - () => { - expect(sdk.naming.getLambdaSchedulePermissionLogicalId('functionName', 0)) - .to.equal('FunctionNameLambdaPermissionEventsRuleSchedule0'); - }); + it('should normalize the function name and add the standard suffix including event index', () => { + expect(sdk.naming.getLambdaSchedulePermissionLogicalId('functionName', 0)).to.equal( + 'FunctionNameLambdaPermissionEventsRuleSchedule0' + ); + }); }); describe('#getLambdaCloudWatchEventPermissionLogicalId()', () => { - it('should normalize the function name and add the standard suffix including event index', - () => { - expect(sdk.naming.getLambdaCloudWatchEventPermissionLogicalId('functionName', 0)) - .to.equal('FunctionNameLambdaPermissionEventsRuleCloudWatchEvent0'); - }); + it('should normalize the function name and add the standard suffix including event index', () => { + expect(sdk.naming.getLambdaCloudWatchEventPermissionLogicalId('functionName', 0)).to.equal( + 'FunctionNameLambdaPermissionEventsRuleCloudWatchEvent0' + ); + }); }); describe('#getLambdaApiGatewayPermissionLogicalId()', () => { it('should normalize the function name and append the standard suffix', () => { - expect(sdk.naming.getLambdaApiGatewayPermissionLogicalId('functionName')) - .to.equal('FunctionNameLambdaPermissionApiGateway'); + expect(sdk.naming.getLambdaApiGatewayPermissionLogicalId('functionName')).to.equal( + 'FunctionNameLambdaPermissionApiGateway' + ); }); }); describe('#getIotLogicalId()', () => { it('should normalize the function name and add the standard suffix including the index', () => { - expect(sdk.naming.getIotLogicalId('functionName', 0)) - .to.equal('FunctionNameIotTopicRule0'); + expect(sdk.naming.getIotLogicalId('functionName', 0)).to.equal('FunctionNameIotTopicRule0'); }); }); describe('#getLambdaIotPermissionLogicalId()', () => { - it('should normalize the function name and add the standard suffix including event index', - () => { - expect(sdk.naming.getLambdaIotPermissionLogicalId('functionName', 0)) - .to.equal('FunctionNameLambdaPermissionIotTopicRule0'); - }); + it('should normalize the function name and add the standard suffix including event index', () => { + expect(sdk.naming.getLambdaIotPermissionLogicalId('functionName', 0)).to.equal( + 'FunctionNameLambdaPermissionIotTopicRule0' + ); + }); }); describe('#getLambdaAlexaSkillPermissionLogicalId()', () => { - it('should normalize the function name and append the standard suffix', - () => { - expect(sdk.naming.getLambdaAlexaSkillPermissionLogicalId('functionName', 2)) - .to.equal('FunctionNameLambdaPermissionAlexaSkill2'); - }); + it('should normalize the function name and append the standard suffix', () => { + expect(sdk.naming.getLambdaAlexaSkillPermissionLogicalId('functionName', 2)).to.equal( + 'FunctionNameLambdaPermissionAlexaSkill2' + ); + }); - it('should normalize the function name and append a default suffix if not defined', - () => { - expect(sdk.naming.getLambdaAlexaSkillPermissionLogicalId('functionName')) - .to.equal('FunctionNameLambdaPermissionAlexaSkill0'); - }); + it('should normalize the function name and append a default suffix if not defined', () => { + expect(sdk.naming.getLambdaAlexaSkillPermissionLogicalId('functionName')).to.equal( + 'FunctionNameLambdaPermissionAlexaSkill0' + ); + }); }); describe('#getLambdaAlexaSmartHomePermissionLogicalId()', () => { - it('should normalize the function name and append the standard suffix', - () => { - expect(sdk.naming.getLambdaAlexaSmartHomePermissionLogicalId('functionName', 0)) - .to.equal('FunctionNameLambdaPermissionAlexaSmartHome0'); - }); + 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')) - .to.equal('FunctionNameSnsSubscriptionTopicName'); + expect(sdk.naming.getLambdaSnsSubscriptionLogicalId('functionName', 'topicName')).to.equal( + 'FunctionNameSnsSubscriptionTopicName' + ); }); }); describe('#getLambdaCloudWatchLogPermissionLogicalId()', () => { - it('should normalize the function name and add the standard suffix including event index', - () => { - expect(sdk.naming.getLambdaCloudWatchLogPermissionLogicalId('functionName')) - .to.equal('FunctionNameLambdaPermissionLogsSubscriptionFilterCloudWatchLog'); - }); + it('should normalize the function name and add the standard suffix including event index', () => { + expect(sdk.naming.getLambdaCloudWatchLogPermissionLogicalId('functionName')).to.equal( + 'FunctionNameLambdaPermissionLogsSubscriptionFilterCloudWatchLog' + ); + }); }); describe('#getLambdaCognitoUserPoolPermissionLogicalId()', () => { it('should normalize the function name and add the standard suffix', () => { - expect(sdk.naming.getLambdaCognitoUserPoolPermissionLogicalId( - 'functionName', - 'Pool1', - 'CustomMessage' - )).to.equal('FunctionNameLambdaPermissionCognitoUserPoolPool1TriggerSourceCustomMessage'); + expect( + sdk.naming.getLambdaCognitoUserPoolPermissionLogicalId( + 'functionName', + 'Pool1', + 'CustomMessage' + ) + ).to.equal('FunctionNameLambdaPermissionCognitoUserPoolPool1TriggerSourceCustomMessage'); + }); + + describe('#getLambdaAlbPermissionLogicalId()', () => { + it('should normalize the function name', () => { + expect(sdk.naming.getLambdaAlbPermissionLogicalId('functionName')).to.equal( + 'FunctionNameLambdaPermissionAlb' + ); + }); }); }); describe('#getQueueLogicalId()', () => { it('should normalize the function name and add the standard suffix', () => { - expect(sdk.naming.getQueueLogicalId('functionName', 'MyQueue')) - .to.equal('FunctionNameEventSourceMappingSQSMyQueue'); + expect(sdk.naming.getQueueLogicalId('functionName', 'MyQueue')).to.equal( + 'FunctionNameEventSourceMappingSQSMyQueue' + ); + }); + }); + + describe('#getAlbTargetGroupLogicalId()', () => { + it('should normalize the function name', () => { + expect(sdk.naming.getAlbTargetGroupLogicalId('functionName')).to.equal( + 'FunctionNameAlbTargetGroup' + ); + }); + }); + + describe('#getAlbListenerRuleLogicalId()', () => { + it('should normalize the function name and add an index', () => { + expect(sdk.naming.getAlbListenerRuleLogicalId('functionName', 0)).to.equal( + 'FunctionNameAlbListenerRule0' + ); + }); + }); + + describe('#getAlbTargetGroupName()', () => { + it('should return a unique identifier based on the service name, function name and stage', () => { + serverless.service.service = 'myService'; + expect(sdk.naming.getAlbTargetGroupName('functionName')).to.equal( + '1ea0b85512f1943fbae9f40a0451d718' + ); + }); + }); + + describe('#getAlbTargetGroupNameTagValue()', () => { + it('should return the composition of service name, function name and stage', () => { + serverless.service.service = 'myService'; + expect(sdk.naming.getAlbTargetGroupNameTagValue('functionName')).to.equal( + `${serverless.service.service}-functionName-${sdk.naming.provider.getStage()}` + ); + }); + }); + + describe('#getCustomResourcesArtifactDirectoryName()', () => { + it('should return the custom resources artifact directory name', () => { + expect(sdk.naming.getCustomResourcesArtifactDirectoryName()).to.equal('custom-resources'); + }); + }); + + describe('#getCustomResourcesRoleLogicalId()', () => { + it('should return the custom resources role logical id', () => { + expect(sdk.naming.getCustomResourcesRoleLogicalId()).to.equal( + 'IamRoleCustomResourcesLambdaExecution' + ); + }); + }); + + describe('#getCustomResourceS3HandlerFunctionName()', () => { + it('should return the nane of the S3 custom resouce handler function', () => { + expect(sdk.naming.getCustomResourceS3HandlerFunctionName()).to.equal( + 'custom-resource-existing-s3' + ); + }); + }); + + describe('#getCustomResourceS3HandlerFunctionLogicalId()', () => { + it('should return the logical id of the S3 custom resouce handler function', () => { + expect(sdk.naming.getCustomResourceS3HandlerFunctionLogicalId()).to.equal( + 'CustomDashresourceDashexistingDashs3LambdaFunction' + ); + }); + }); + + describe('#getCustomResourceS3ResourceLogicalId()', () => { + it('should return the logical id of the S3 custom resouce', () => { + const functionName = 'my-function'; + const index = 1; + expect(sdk.naming.getCustomResourceS3ResourceLogicalId(functionName, index)).to.equal( + 'MyDashfunctionCustomS31' + ); }); }); }); diff --git a/lib/plugins/aws/lib/normalizeFiles.js b/lib/plugins/aws/lib/normalizeFiles.js index e0150cc18..a1c90634d 100644 --- a/lib/plugins/aws/lib/normalizeFiles.js +++ b/lib/plugins/aws/lib/normalizeFiles.js @@ -8,8 +8,9 @@ module.exports = { _.forEach(normalizedTemplate.Resources, (value, key) => { if (key.startsWith('ApiGatewayDeployment')) { - delete Object.assign(normalizedTemplate.Resources, - { ApiGatewayDeployment: normalizedTemplate.Resources[key] })[key]; + delete Object.assign(normalizedTemplate.Resources, { + ApiGatewayDeployment: normalizedTemplate.Resources[key], + })[key]; } if (value.Type && value.Type === 'AWS::Lambda::Function') { const newVal = value; diff --git a/lib/plugins/aws/lib/setBucketName.js b/lib/plugins/aws/lib/setBucketName.js index 8a4b9c28a..8a3296099 100644 --- a/lib/plugins/aws/lib/setBucketName.js +++ b/lib/plugins/aws/lib/setBucketName.js @@ -8,9 +8,8 @@ module.exports = { return BbPromise.resolve(this.bucketName); } - return this.provider.getServerlessDeploymentBucketName() - .then((bucketName) => { - this.bucketName = bucketName; - }); + return this.provider.getServerlessDeploymentBucketName().then(bucketName => { + this.bucketName = bucketName; + }); }, }; diff --git a/lib/plugins/aws/lib/setBucketName.test.js b/lib/plugins/aws/lib/setBucketName.test.js index 8d2f9f89e..b18b87291 100644 --- a/lib/plugins/aws/lib/setBucketName.test.js +++ b/lib/plugins/aws/lib/setBucketName.test.js @@ -26,19 +26,19 @@ describe('#setBucketName()', () => { .resolves('bucket-name'); }); - it('should store the name of the Serverless deployment bucket', () => awsDeploy - .setBucketName().then(() => { + it('should store the name of the Serverless deployment bucket', () => + awsDeploy.setBucketName().then(() => { expect(awsDeploy.bucketName).to.equal('bucket-name'); expect(getServerlessDeploymentBucketNameStub.calledOnce).to.be.equal(true); expect(getServerlessDeploymentBucketNameStub.calledWithExactly()).to.be.equal(true); awsDeploy.provider.getServerlessDeploymentBucketName.restore(); - }) - ); + })); it('should resolve if the bucketName is already set', () => { const bucketName = 'someBucket'; awsDeploy.bucketName = bucketName; - return awsDeploy.setBucketName() + return awsDeploy + .setBucketName() .then(() => expect(getServerlessDeploymentBucketNameStub.calledOnce).to.be.false) .then(() => expect(awsDeploy.bucketName).to.equal(bucketName)); }); diff --git a/lib/plugins/aws/lib/updateStack.js b/lib/plugins/aws/lib/updateStack.js index 4288b4425..2e1af627d 100644 --- a/lib/plugins/aws/lib/updateStack.js +++ b/lib/plugins/aws/lib/updateStack.js @@ -25,17 +25,16 @@ module.exports = { const params = { StackName: stackName, OnFailure: 'ROLLBACK', - Capabilities: [ - 'CAPABILITY_IAM', - 'CAPABILITY_NAMED_IAM', - ], + Capabilities: ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'], Parameters: [], TemplateURL: templateUrl, - Tags: Object.keys(stackTags).map((key) => ({ Key: key, Value: stackTags[key] })), + Tags: Object.keys(stackTags).map(key => ({ Key: key, Value: stackTags[key] })), }; - if (this.serverless.service.provider.compiledCloudFormationTemplate && - this.serverless.service.provider.compiledCloudFormationTemplate.Transform) { + if ( + this.serverless.service.provider.compiledCloudFormationTemplate && + this.serverless.service.provider.compiledCloudFormationTemplate.Transform + ) { params.Capabilities.push('CAPABILITY_AUTO_EXPAND'); } @@ -47,10 +46,9 @@ module.exports = { params.NotificationARNs = this.serverless.service.provider.notificationArns; } - return this.provider.request('CloudFormation', - 'createStack', - params) - .then((cfData) => this.monitorStack('create', cfData)); + return this.provider + .request('CloudFormation', 'createStack', params) + .then(cfData => this.monitorStack('create', cfData)); }, update() { @@ -69,17 +67,16 @@ module.exports = { const params = { StackName: stackName, - Capabilities: [ - 'CAPABILITY_IAM', - 'CAPABILITY_NAMED_IAM', - ], + Capabilities: ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'], Parameters: [], TemplateURL: templateUrl, - Tags: Object.keys(stackTags).map((key) => ({ Key: key, Value: stackTags[key] })), + Tags: Object.keys(stackTags).map(key => ({ Key: key, Value: stackTags[key] })), }; - if (this.serverless.service.provider.compiledCloudFormationTemplate && - this.serverless.service.provider.compiledCloudFormationTemplate.Transform) { + if ( + this.serverless.service.provider.compiledCloudFormationTemplate && + this.serverless.service.provider.compiledCloudFormationTemplate.Transform + ) { params.Capabilities.push('CAPABILITY_AUTO_EXPAND'); } @@ -92,18 +89,19 @@ module.exports = { } // Policy must have at least one statement, otherwise no updates would be possible at all - if (this.serverless.service.provider.stackPolicy && - !_.isEmpty(this.serverless.service.provider.stackPolicy)) { + if ( + this.serverless.service.provider.stackPolicy && + !_.isEmpty(this.serverless.service.provider.stackPolicy) + ) { params.StackPolicyBody = JSON.stringify({ Statement: this.serverless.service.provider.stackPolicy, }); } - return this.provider.request('CloudFormation', - 'updateStack', - params) - .then((cfData) => this.monitorStack('update', cfData)) - .catch((e) => { + return this.provider + .request('CloudFormation', 'updateStack', params) + .then(cfData => this.monitorStack('update', cfData)) + .catch(e => { if (e.message === NO_UPDATE_MESSAGE) { return BbPromise.resolve(); } @@ -112,14 +110,11 @@ module.exports = { }, updateStack() { - return BbPromise.bind(this) - .then(() => { - if (this.createLater) { - return BbPromise.bind(this) - .then(this.createFallback); - } - return BbPromise.bind(this) - .then(this.update); - }); + return BbPromise.bind(this).then(() => { + if (this.createLater) { + return BbPromise.bind(this).then(this.createFallback); + } + return BbPromise.bind(this).then(this.update); + }); }, }; diff --git a/lib/plugins/aws/lib/updateStack.test.js b/lib/plugins/aws/lib/updateStack.test.js index dbde9a841..e3c976680 100644 --- a/lib/plugins/aws/lib/updateStack.test.js +++ b/lib/plugins/aws/lib/updateStack.test.js @@ -5,12 +5,12 @@ const sinon = require('sinon'); const AwsProvider = require('../provider/awsProvider'); const AwsDeploy = require('../deploy'); const Serverless = require('../../../Serverless'); -const testUtils = require('../../../../tests/utils'); +const { getTmpDirPath } = require('../../../../tests/utils/fs'); describe('updateStack', () => { let serverless; let awsDeploy; - const tmpDirPath = testUtils.getTmpDirPath(); + const tmpDirPath = getTmpDirPath(); beforeEach(() => { const options = { @@ -24,7 +24,7 @@ describe('updateStack', () => { awsDeploy.deployedFunctions = [{ name: 'first', zipFileKey: 'zipFileOfFirstFunction' }]; awsDeploy.bucketName = 'deployment-bucket'; - serverless.service.service = `service-${(new Date()).getTime().toString()}`; + serverless.service.service = `service-${new Date().getTime().toString()}`; serverless.config.servicePath = tmpDirPath; awsDeploy.serverless.service.package.artifactDirectoryName = 'somedir'; awsDeploy.serverless.cli = new serverless.classes.CLI(); @@ -34,28 +34,21 @@ describe('updateStack', () => { it('should create a stack with the CF template URL', () => { const compiledTemplateFileName = 'compiled-cloudformation-template.json'; - const createStackStub = sinon - .stub(awsDeploy.provider, 'request').resolves(); + const createStackStub = sinon.stub(awsDeploy.provider, 'request').resolves(); sinon.stub(awsDeploy, 'monitorStack').resolves(); return awsDeploy.createFallback().then(() => { expect(createStackStub.calledOnce).to.be.equal(true); - expect(createStackStub.calledWithExactly( - 'CloudFormation', - 'createStack', - { + expect( + createStackStub.calledWithExactly('CloudFormation', 'createStack', { StackName: awsDeploy.provider.naming.getStackName(), OnFailure: 'ROLLBACK', - Capabilities: [ - 'CAPABILITY_IAM', - 'CAPABILITY_NAMED_IAM', - ], + Capabilities: ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'], Parameters: [], - TemplateURL: `https://s3.amazonaws.com/${awsDeploy.bucketName}/${awsDeploy.serverless - .service.package.artifactDirectoryName}/${compiledTemplateFileName}`, + TemplateURL: `https://s3.amazonaws.com/${awsDeploy.bucketName}/${awsDeploy.serverless.service.package.artifactDirectoryName}/${compiledTemplateFileName}`, Tags: [{ Key: 'STAGE', Value: awsDeploy.provider.getStage() }], - } - )).to.be.equal(true); + }) + ).to.be.equal(true); awsDeploy.provider.request.restore(); awsDeploy.monitorStack.restore(); }); @@ -64,31 +57,29 @@ describe('updateStack', () => { it('should include custom stack tags', () => { awsDeploy.serverless.service.provider.stackTags = { STAGE: 'overridden', tag1: 'value1' }; - const createStackStub = sinon - .stub(awsDeploy.provider, 'request').resolves(); + const createStackStub = sinon.stub(awsDeploy.provider, 'request').resolves(); sinon.stub(awsDeploy, 'monitorStack').resolves(); return awsDeploy.createFallback().then(() => { - expect(createStackStub.args[0][2].Tags) - .to.deep.equal([ - { Key: 'STAGE', Value: 'overridden' }, - { Key: 'tag1', Value: 'value1' }, - ]); + expect(createStackStub.args[0][2].Tags).to.deep.equal([ + { Key: 'STAGE', Value: 'overridden' }, + { Key: 'tag1', Value: 'value1' }, + ]); awsDeploy.provider.request.restore(); awsDeploy.monitorStack.restore(); }); }); it('should add CAPABILITY_AUTO_EXPAND if a Transform directive is specified', () => { - awsDeploy.serverless.service.provider - .compiledCloudFormationTemplate = { Transform: 'MyMacro' }; + awsDeploy.serverless.service.provider.compiledCloudFormationTemplate = { + Transform: 'MyMacro', + }; const createStackStub = sinon.stub(awsDeploy.provider, 'request').resolves(); sinon.stub(awsDeploy, 'monitorStack').resolves(); return awsDeploy.createFallback().then(() => { - expect(createStackStub.args[0][2].Capabilities) - .to.contain('CAPABILITY_AUTO_EXPAND'); + expect(createStackStub.args[0][2].Capabilities).to.contain('CAPABILITY_AUTO_EXPAND'); awsDeploy.provider.request.restore(); awsDeploy.monitorStack.restore(); }); @@ -101,8 +92,9 @@ describe('updateStack', () => { sinon.stub(awsDeploy, 'monitorStack').resolves(); return awsDeploy.createFallback().then(() => { - expect(createStackStub.args[0][2].RoleARN) - .to.equal('arn:aws:iam::123456789012:role/myrole'); + expect(createStackStub.args[0][2].RoleARN).to.equal( + 'arn:aws:iam::123456789012:role/myrole' + ); awsDeploy.provider.request.restore(); awsDeploy.monitorStack.restore(); }); @@ -116,8 +108,7 @@ describe('updateStack', () => { sinon.stub(awsDeploy, 'monitorStack').resolves(); return awsDeploy.createFallback().then(() => { - expect(createStackStub.args[0][2].NotificationARNs) - .to.deep.equal([mytopicArn]); + expect(createStackStub.args[0][2].NotificationARNs).to.deep.equal([mytopicArn]); awsDeploy.provider.request.restore(); awsDeploy.monitorStack.restore(); }); @@ -128,8 +119,7 @@ describe('updateStack', () => { let updateStackStub; beforeEach(() => { - updateStackStub = sinon - .stub(awsDeploy.provider, 'request').resolves(); + updateStackStub = sinon.stub(awsDeploy.provider, 'request').resolves(); sinon.stub(awsDeploy, 'monitorStack').resolves(); }); @@ -138,65 +128,57 @@ describe('updateStack', () => { awsDeploy.monitorStack.restore(); }); - it('should update the stack', () => awsDeploy.update() - .then(() => { + it('should update the stack', () => + awsDeploy.update().then(() => { const compiledTemplateFileName = 'compiled-cloudformation-template.json'; expect(updateStackStub.calledOnce).to.be.equal(true); - expect(updateStackStub.calledWithExactly( - 'CloudFormation', - 'updateStack', - { + expect( + updateStackStub.calledWithExactly('CloudFormation', 'updateStack', { StackName: awsDeploy.provider.naming.getStackName(), - Capabilities: [ - 'CAPABILITY_IAM', - 'CAPABILITY_NAMED_IAM', - ], + Capabilities: ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'], Parameters: [], - TemplateURL: `https://s3.amazonaws.com/${awsDeploy.bucketName}/${awsDeploy.serverless - .service.package.artifactDirectoryName}/${compiledTemplateFileName}`, + TemplateURL: `https://s3.amazonaws.com/${awsDeploy.bucketName}/${awsDeploy.serverless.service.package.artifactDirectoryName}/${compiledTemplateFileName}`, Tags: [{ Key: 'STAGE', Value: awsDeploy.provider.getStage() }], - } - )).to.be.equal(true); - }) - ); + }) + ).to.be.equal(true); + })); it('should include custom stack tags and policy', () => { awsDeploy.serverless.service.provider.stackTags = { STAGE: 'overridden', tag1: 'value1' }; - awsDeploy.serverless.service.provider.stackPolicy = [{ - Effect: 'Allow', - Principal: '*', - Action: 'Update:*', - Resource: '*', - }]; + awsDeploy.serverless.service.provider.stackPolicy = [ + { + Effect: 'Allow', + Principal: '*', + Action: 'Update:*', + Resource: '*', + }, + ]; return awsDeploy.update().then(() => { - expect(updateStackStub.args[0][2].Tags) - .to.deep.equal([ - { Key: 'STAGE', Value: 'overridden' }, - { Key: 'tag1', Value: 'value1' }, - ]); - expect(updateStackStub.args[0][2].StackPolicyBody) - .to.equal( - '{"Statement":[{"Effect":"Allow","Principal":"*","Action":"Update:*","Resource":"*"}]}' - ); + expect(updateStackStub.args[0][2].Tags).to.deep.equal([ + { Key: 'STAGE', Value: 'overridden' }, + { Key: 'tag1', Value: 'value1' }, + ]); + expect(updateStackStub.args[0][2].StackPolicyBody).to.equal( + '{"Statement":[{"Effect":"Allow","Principal":"*","Action":"Update:*","Resource":"*"}]}' + ); }); }); it('should success if no changes to stack happened', () => { awsDeploy.monitorStack.restore(); - sinon.stub(awsDeploy, 'monitorStack') - .rejects(new Error('No updates are to be performed.')); + sinon.stub(awsDeploy, 'monitorStack').rejects(new Error('No updates are to be performed.')); return awsDeploy.update(); }); it('should add CAPABILITY_AUTO_EXPAND if a Transform directive is specified', () => { - awsDeploy.serverless.service.provider - .compiledCloudFormationTemplate = { Transform: 'MyMacro' }; + awsDeploy.serverless.service.provider.compiledCloudFormationTemplate = { + Transform: 'MyMacro', + }; return awsDeploy.update().then(() => { - expect(updateStackStub.args[0][2].Capabilities) - .to.contain('CAPABILITY_AUTO_EXPAND'); + expect(updateStackStub.args[0][2].Capabilities).to.contain('CAPABILITY_AUTO_EXPAND'); }); }); @@ -204,8 +186,9 @@ describe('updateStack', () => { awsDeploy.serverless.service.provider.cfnRole = 'arn:aws:iam::123456789012:role/myrole'; return awsDeploy.update().then(() => { - expect(updateStackStub.args[0][2].RoleARN) - .to.equal('arn:aws:iam::123456789012:role/myrole'); + expect(updateStackStub.args[0][2].RoleARN).to.equal( + 'arn:aws:iam::123456789012:role/myrole' + ); }); }); @@ -214,8 +197,7 @@ describe('updateStack', () => { awsDeploy.serverless.service.provider.notificationArns = [mytopicArn]; return awsDeploy.update().then(() => { - expect(updateStackStub.args[0][2].NotificationARNs) - .to.deep.equal([mytopicArn]); + expect(updateStackStub.args[0][2].NotificationARNs).to.deep.equal([mytopicArn]); }); }); }); @@ -223,10 +205,8 @@ describe('updateStack', () => { describe('#updateStack()', () => { it('should fallback to createStack if createLater flag exists', () => { awsDeploy.createLater = true; - const createFallbackStub = sinon - .stub(awsDeploy, 'createFallback').resolves(); - const updateStub = sinon - .stub(awsDeploy, 'update').resolves(); + const createFallbackStub = sinon.stub(awsDeploy, 'createFallback').resolves(); + const updateStub = sinon.stub(awsDeploy, 'update').resolves(); return awsDeploy.updateStack().then(() => { expect(createFallbackStub.calledOnce).to.be.equal(true); @@ -236,8 +216,7 @@ describe('updateStack', () => { }); it('should run promise chain in order', () => { - const updateStub = sinon - .stub(awsDeploy, 'update').resolves(); + const updateStub = sinon.stub(awsDeploy, 'update').resolves(); return awsDeploy.updateStack().then(() => { expect(updateStub.calledOnce).to.be.equal(true); diff --git a/lib/plugins/aws/lib/validate.js b/lib/plugins/aws/lib/validate.js index 3673348ee..31ffa85d9 100644 --- a/lib/plugins/aws/lib/validate.js +++ b/lib/plugins/aws/lib/validate.js @@ -4,14 +4,18 @@ const BbPromise = require('bluebird'); module.exports = { validate() { - if (!this.serverless.config.servicePath) { - throw new this.serverless.classes - .Error('This command can only be run inside a service directory'); - } + return new BbPromise((resolve, reject) => { + if (!this.serverless.config.servicePath) { + const error = new this.serverless.classes.Error( + 'This command can only be run inside a service directory' + ); + reject(error); + } - this.options.stage = this.provider.getStage(); - this.options.region = this.provider.getRegion(); + this.options.stage = this.provider.getStage(); + this.options.region = this.provider.getRegion(); - return BbPromise.resolve(); + return resolve(); + }); }, }; diff --git a/lib/plugins/aws/lib/validate.test.js b/lib/plugins/aws/lib/validate.test.js index 87ec6abeb..d21aa79eb 100644 --- a/lib/plugins/aws/lib/validate.test.js +++ b/lib/plugins/aws/lib/validate.test.js @@ -1,11 +1,14 @@ 'use strict'; -const expect = require('chai').expect; - +const chai = require('chai'); const AwsProvider = require('../provider/awsProvider'); const validate = require('../lib/validate'); const Serverless = require('../../../Serverless'); +chai.use(require('chai-as-promised')); + +const expect = chai.expect; + describe('#validate', () => { const serverless = new Serverless(); let provider; @@ -27,20 +30,19 @@ describe('#validate', () => { }); describe('#validate()', () => { - it('should succeed if inside service (servicePath defined)', () => { - expect(() => awsPlugin.validate()).to.not.throw(Error); - }); + it('should succeed if inside service (servicePath defined)', () => + expect(awsPlugin.validate()).to.be.fulfilled); it('should throw error if not inside service (servicePath not defined)', () => { awsPlugin.serverless.config.servicePath = false; - expect(() => awsPlugin.validate()).to.throw(Error); + return expect(awsPlugin.validate()).to.be.rejected; }); // NOTE: starting here, test order is important it('should default to "dev" if stage is not provided', () => { awsPlugin.options.stage = false; - return awsPlugin.validate().then(() => { + return expect(awsPlugin.validate()).to.be.fulfilled.then(() => { expect(awsPlugin.provider.getStage()).to.equal('dev'); }); }); @@ -51,14 +53,14 @@ describe('#validate', () => { stage: 'some-stage', }; - return awsPlugin.validate().then(() => { + return expect(awsPlugin.validate()).to.be.fulfilled.then(() => { expect(awsPlugin.provider.getStage()).to.equal('some-stage'); }); }); it('should default to "us-east-1" region if region is not provided', () => { awsPlugin.options.region = false; - return awsPlugin.validate().then(() => { + return expect(awsPlugin.validate()).to.be.fulfilled.then(() => { expect(awsPlugin.options.region).to.equal('us-east-1'); }); }); @@ -69,7 +71,7 @@ describe('#validate', () => { region: 'some-region', }; - return awsPlugin.validate().then(() => { + return expect(awsPlugin.validate()).to.be.fulfilled.then(() => { expect(awsPlugin.options.region).to.equal('some-region'); }); }); diff --git a/lib/plugins/aws/lib/validateS3BucketName.js b/lib/plugins/aws/lib/validateS3BucketName.js index 22286324e..ce3483232 100644 --- a/lib/plugins/aws/lib/validateS3BucketName.js +++ b/lib/plugins/aws/lib/validateS3BucketName.js @@ -14,33 +14,32 @@ module.exports = { * @param bucketName */ validateS3BucketName(bucketName) { - return BbPromise.resolve() - .then(() => { - let error; - if (!bucketName) { - error = 'Bucket name cannot be undefined or empty'; - } else if (bucketName.length < 3) { - error = `Bucket name is shorter than 3 characters. ${bucketName}`; - } else if (bucketName.length > 63) { - error = `Bucket name is longer than 63 characters. ${bucketName}`; - } else if (/[A-Z]/.test(bucketName)) { - error = `Bucket name cannot contain uppercase letters. ${bucketName}`; - } else if (/^[^a-z0-9]/.test(bucketName)) { - error = `Bucket name must start with a letter or number. ${bucketName}`; - } else if (/[^a-z0-9]$/.test(bucketName)) { - error = `Bucket name must end with a letter or number. ${bucketName}`; - } else if (!/^[a-z0-9][a-z.0-9-]+[a-z0-9]$/.test(bucketName)) { - error = `Bucket name contains invalid characters, [a-z.0-9-] ${bucketName}`; - } else if (/\.{2,}/.test(bucketName)) { - error = `Bucket name cannot contain consecutive periods (.) ${bucketName}`; - } else if (/^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(bucketName)) { - error = `Bucket name cannot look like an IPv4 address. ${bucketName}`; - } + return BbPromise.resolve().then(() => { + let error; + if (!bucketName) { + error = 'Bucket name cannot be undefined or empty'; + } else if (bucketName.length < 3) { + error = `Bucket name is shorter than 3 characters. ${bucketName}`; + } else if (bucketName.length > 63) { + error = `Bucket name is longer than 63 characters. ${bucketName}`; + } else if (/[A-Z]/.test(bucketName)) { + error = `Bucket name cannot contain uppercase letters. ${bucketName}`; + } else if (/^[^a-z0-9]/.test(bucketName)) { + error = `Bucket name must start with a letter or number. ${bucketName}`; + } else if (/[^a-z0-9]$/.test(bucketName)) { + error = `Bucket name must end with a letter or number. ${bucketName}`; + } else if (!/^[a-z0-9][a-z.0-9-]+[a-z0-9]$/.test(bucketName)) { + error = `Bucket name contains invalid characters, [a-z.0-9-] ${bucketName}`; + } else if (/\.{2,}/.test(bucketName)) { + error = `Bucket name cannot contain consecutive periods (.) ${bucketName}`; + } else if (/^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(bucketName)) { + error = `Bucket name cannot look like an IPv4 address. ${bucketName}`; + } - if (error) { - throw new this.serverless.classes.Error(error); - } - return true; - }); + if (error) { + throw new this.serverless.classes.Error(error); + } + return true; + }); }, }; diff --git a/lib/plugins/aws/lib/validateS3BucketName.test.js b/lib/plugins/aws/lib/validateS3BucketName.test.js index f28f12b3f..c33498a6b 100644 --- a/lib/plugins/aws/lib/validateS3BucketName.test.js +++ b/lib/plugins/aws/lib/validateS3BucketName.test.js @@ -22,16 +22,17 @@ describe('#validateS3BucketName()', () => { describe('#validateS3BucketName()', () => { it('should reject an ip address as a name', () => - awsPlugin.validateS3BucketName('127.0.0.1') + awsPlugin + .validateS3BucketName('127.0.0.1') .then(() => { throw new Error('Should not get here'); }) - .catch(err => expect(err.message).to.contain('cannot look like an IPv4 address')) - ); + .catch(err => expect(err.message).to.contain('cannot look like an IPv4 address'))); it('should reject names that are too long', () => { const bucketName = Array.from({ length: 64 }, () => 'j').join(''); - return awsPlugin.validateS3BucketName(bucketName) + return awsPlugin + .validateS3BucketName(bucketName) .then(() => { throw new Error('Should not get here'); }) @@ -39,75 +40,75 @@ describe('#validateS3BucketName()', () => { }); it('should reject names that are too short', () => - awsPlugin.validateS3BucketName('12') + awsPlugin + .validateS3BucketName('12') .then(() => { throw new Error('Should not get here'); }) - .catch(err => expect(err.message).to.contain('shorter than 3 characters')) - ); + .catch(err => expect(err.message).to.contain('shorter than 3 characters'))); it('should reject names that contain invalid characters', () => - awsPlugin.validateS3BucketName('this has b@d characters') + awsPlugin + .validateS3BucketName('this has b@d characters') .then(() => { throw new Error('Should not get here'); }) - .catch(err => expect(err.message).to.contain('contains invalid characters')) - ); + .catch(err => expect(err.message).to.contain('contains invalid characters'))); it('should reject names that have consecutive periods', () => - awsPlugin.validateS3BucketName('otherwise..valid.name') + awsPlugin + .validateS3BucketName('otherwise..valid.name') .then(() => { throw new Error('Should not get here'); }) - .catch(err => expect(err.message).to.contain('cannot contain consecutive periods')) - ); + .catch(err => expect(err.message).to.contain('cannot contain consecutive periods'))); it('should reject names that start with a dash', () => - awsPlugin.validateS3BucketName('-invalid.name') + awsPlugin + .validateS3BucketName('-invalid.name') .then(() => { throw new Error('Should not get here'); }) - .catch(err => expect(err.message).to.contain('start with a letter or number')) - ); + .catch(err => expect(err.message).to.contain('start with a letter or number'))); it('should reject names that start with a period', () => - awsPlugin.validateS3BucketName('.invalid.name') + awsPlugin + .validateS3BucketName('.invalid.name') .then(() => { throw new Error('Should not get here'); }) - .catch(err => expect(err.message).to.contain('start with a letter or number')) - ); + .catch(err => expect(err.message).to.contain('start with a letter or number'))); it('should reject names that end with a dash', () => - awsPlugin.validateS3BucketName('invalid.name-') + awsPlugin + .validateS3BucketName('invalid.name-') .then(() => { throw new Error('Should not get here'); }) - .catch(err => expect(err.message).to.contain('end with a letter or number')) - ); + .catch(err => expect(err.message).to.contain('end with a letter or number'))); it('should reject names that end with a period', () => - awsPlugin.validateS3BucketName('invalid.name.') + awsPlugin + .validateS3BucketName('invalid.name.') .then(() => { throw new Error('Should not get here'); }) - .catch(err => expect(err.message).to.contain('end with a letter or number')) - ); + .catch(err => expect(err.message).to.contain('end with a letter or number'))); it('should reject names that contain uppercase letters', () => - awsPlugin.validateS3BucketName('otherwise.Valid.name') + awsPlugin + .validateS3BucketName('otherwise.Valid.name') .then(() => { throw new Error('Should not get here'); }) - .catch(err => expect(err.message).to.contain('cannot contain uppercase letters')) - ); + .catch(err => expect(err.message).to.contain('cannot contain uppercase letters'))); it('should accept valid names', () => - awsPlugin.validateS3BucketName('1.this.is.valid.2') + awsPlugin + .validateS3BucketName('1.this.is.valid.2') .then(() => awsPlugin.validateS3BucketName('another.valid.name')) .then(() => awsPlugin.validateS3BucketName('1-2-3')) .then(() => awsPlugin.validateS3BucketName('123')) - .then(() => awsPlugin.validateS3BucketName('should.be.allowed-to-mix')) - ); + .then(() => awsPlugin.validateS3BucketName('should.be.allowed-to-mix'))); }); }); diff --git a/lib/plugins/aws/logs/index.js b/lib/plugins/aws/logs/index.js index 9f37783ac..6e24332ff 100644 --- a/lib/plugins/aws/logs/index.js +++ b/lib/plugins/aws/logs/index.js @@ -15,10 +15,11 @@ class AwsLogs { Object.assign(this, validate); this.hooks = { - 'logs:logs': () => BbPromise.bind(this) - .then(this.extendedValidate) - .then(this.getLogStreams) - .then(this.showLogs), + 'logs:logs': () => + BbPromise.bind(this) + .then(this.extendedValidate) + .then(this.getLogStreams) + .then(this.showLogs), }; } @@ -42,21 +43,16 @@ class AwsLogs { orderBy: 'LastEventTime', }; - return this.provider - .request('CloudWatchLogs', - 'describeLogStreams', - params) - .then(reply => { - if (!reply || reply.logStreams.length === 0) { - throw new this.serverless.classes - .Error('No existing streams for the function'); - } + return this.provider.request('CloudWatchLogs', 'describeLogStreams', params).then(reply => { + if (!reply || reply.logStreams.length === 0) { + throw new this.serverless.classes.Error('No existing streams for the function'); + } - return _.chain(reply.logStreams) - .filter(stream => _.includes(stream.logStreamName, '[$LATEST]')) - .map('logStreamName') - .value(); - }); + return _.chain(reply.logStreams) + .filter(stream => _.includes(stream.logStreamName, '[$LATEST]')) + .map('logStreamName') + .value(); + }); } showLogs(logStreamNames) { @@ -78,51 +74,54 @@ class AwsLogs { if (this.options.filter) params.filterPattern = this.options.filter; if (this.options.nextToken) params.nextToken = this.options.nextToken; if (this.options.startTime) { - const since = (['m', 'h', 'd'] - .indexOf(this.options.startTime[this.options.startTime.length - 1]) !== -1); + const since = + ['m', 'h', 'd'].indexOf(this.options.startTime[this.options.startTime.length - 1]) !== -1; if (since) { - params.startTime = moment().subtract(this.options - .startTime.replace(/\D/g, ''), this.options - .startTime.replace(/\d/g, '')).valueOf(); + params.startTime = moment() + .subtract( + this.options.startTime.replace(/\D/g, ''), + this.options.startTime.replace(/\d/g, '') + ) + .valueOf(); } else { params.startTime = moment.utc(this.options.startTime).valueOf(); } } else { - params.startTime = moment().subtract(10, 'm').valueOf(); + params.startTime = moment() + .subtract(10, 'm') + .valueOf(); if (this.options.tail) { - params.startTime = moment().subtract(10, 's').valueOf(); + params.startTime = moment() + .subtract(10, 's') + .valueOf(); } } - return this.provider - .request('CloudWatchLogs', - 'filterLogEvents', - params) - .then(results => { - if (results.events) { - results.events.forEach(e => { - process.stdout.write(formatLambdaLogEvent(e.message)); - }); + return this.provider.request('CloudWatchLogs', 'filterLogEvents', params).then(results => { + if (results.events) { + results.events.forEach(e => { + process.stdout.write(formatLambdaLogEvent(e.message)); + }); + } + + if (results.nextToken) { + this.options.nextToken = results.nextToken; + } else { + delete this.options.nextToken; + } + + if (this.options.tail) { + if (results.events && results.events.length) { + this.options.startTime = _.last(results.events).timestamp + 1; } - if (results.nextToken) { - this.options.nextToken = results.nextToken; - } else { - delete this.options.nextToken; - } + return BbPromise.delay(this.options.interval) + .then(this.getLogStreams.bind(this)) + .then(this.showLogs.bind(this)); + } - if (this.options.tail) { - if (results.events && results.events.length) { - this.options.startTime = _.last(results.events).timestamp + 1; - } - - return BbPromise.delay(this.options.interval) - .then(this.getLogStreams.bind(this)) - .then(this.showLogs.bind(this)); - } - - return BbPromise.resolve(); - }); + return BbPromise.resolve(); + }); } } diff --git a/lib/plugins/aws/logs/index.test.js b/lib/plugins/aws/logs/index.test.js index 1ec522647..e748a4409 100644 --- a/lib/plugins/aws/logs/index.test.js +++ b/lib/plugins/aws/logs/index.test.js @@ -35,12 +35,9 @@ describe('AwsLogs', () => { expect(awsLogs.provider).to.be.instanceof(AwsProvider)); it('should run promise chain in order', () => { - const validateStub = sinon - .stub(awsLogs, 'extendedValidate').resolves(); - const getLogStreamsStub = sinon - .stub(awsLogs, 'getLogStreams').resolves(); - const showLogsStub = sinon - .stub(awsLogs, 'showLogs').resolves(); + const validateStub = sinon.stub(awsLogs, 'extendedValidate').resolves(); + const getLogStreamsStub = sinon.stub(awsLogs, 'getLogStreams').resolves(); + const showLogsStub = sinon.stub(awsLogs, 'showLogs').resolves(); return awsLogs.hooks['logs:logs']().then(() => { expect(validateStub.calledOnce).to.be.equal(true); @@ -83,14 +80,16 @@ describe('AwsLogs', () => { expect(() => awsLogs.extendedValidate()).to.throw(Error); }); - it('it should set default options', () => awsLogs.extendedValidate().then(() => { - expect(awsLogs.options.stage).to.deep.equal('dev'); - expect(awsLogs.options.region).to.deep.equal('us-east-1'); - expect(awsLogs.options.function).to.deep.equal('first'); - expect(awsLogs.options.interval).to.be.equal(1000); - expect(awsLogs.options.logGroupName).to.deep.equal(awsLogs.provider.naming - .getLogGroupName('customName')); - })); + it('it should set default options', () => + awsLogs.extendedValidate().then(() => { + expect(awsLogs.options.stage).to.deep.equal('dev'); + expect(awsLogs.options.region).to.deep.equal('us-east-1'); + expect(awsLogs.options.function).to.deep.equal('first'); + expect(awsLogs.options.interval).to.be.equal(1000); + expect(awsLogs.options.logGroupName).to.deep.equal( + awsLogs.provider.naming.getLogGroupName('customName') + ); + })); }); describe('#getLogStreams()', () => { @@ -119,37 +118,38 @@ describe('AwsLogs', () => { }; const getLogStreamsStub = sinon.stub(awsLogs.provider, 'request').resolves(replyMock); - return awsLogs.getLogStreams() - .then(logStreamNames => { - expect(getLogStreamsStub.calledOnce).to.be.equal(true); - expect(getLogStreamsStub.calledWithExactly( - 'CloudWatchLogs', - 'describeLogStreams', - { - logGroupName: awsLogs.provider.naming.getLogGroupName('new-service-dev-first'), - descending: true, - limit: 50, - orderBy: 'LastEventTime', - } - )).to.be.equal(true); + return awsLogs.getLogStreams().then(logStreamNames => { + expect(getLogStreamsStub.calledOnce).to.be.equal(true); + expect( + getLogStreamsStub.calledWithExactly('CloudWatchLogs', 'describeLogStreams', { + logGroupName: awsLogs.provider.naming.getLogGroupName('new-service-dev-first'), + descending: true, + limit: 50, + orderBy: 'LastEventTime', + }) + ).to.be.equal(true); - expect(logStreamNames[0]) - .to.be.equal('2016/07/28/[$LATEST]83f5206ab2a8488290349b9c1fbfe2ba'); - expect(logStreamNames[1]) - .to.be.equal('2016/07/28/[$LATEST]83f5206ab2a8488290349b9c1fbfe2ba'); + expect(logStreamNames[0]).to.be.equal( + '2016/07/28/[$LATEST]83f5206ab2a8488290349b9c1fbfe2ba' + ); + expect(logStreamNames[1]).to.be.equal( + '2016/07/28/[$LATEST]83f5206ab2a8488290349b9c1fbfe2ba' + ); - awsLogs.provider.request.restore(); - }); + awsLogs.provider.request.restore(); + }); }); it('should throw error if no log streams found', () => { sinon.stub(awsLogs.provider, 'request').resolves(); - return awsLogs.getLogStreams() + return awsLogs + .getLogStreams() .then(() => { expect(1).to.equal(2); awsLogs.provider.request.restore(); - }).catch(e => { + }) + .catch(e => { expect(e.name).to.be.equal('ServerlessError'); awsLogs.provider.request.restore(); }); @@ -200,22 +200,19 @@ describe('AwsLogs', () => { filter: 'error', }; - return awsLogs.showLogs(logStreamNamesMock) - .then(() => { - expect(filterLogEventsStub.calledOnce).to.be.equal(true); - expect(filterLogEventsStub.calledWithExactly( - 'CloudWatchLogs', - 'filterLogEvents', - { - logGroupName: awsLogs.provider.naming.getLogGroupName('new-service-dev-first'), - interleaved: true, - logStreamNames: logStreamNamesMock, - filterPattern: 'error', - startTime: fakeTime - (3 * 60 * 60 * 1000), // -3h - } - )).to.be.equal(true); - awsLogs.provider.request.restore(); - }); + return awsLogs.showLogs(logStreamNamesMock).then(() => { + expect(filterLogEventsStub.calledOnce).to.be.equal(true); + expect( + filterLogEventsStub.calledWithExactly('CloudWatchLogs', 'filterLogEvents', { + logGroupName: awsLogs.provider.naming.getLogGroupName('new-service-dev-first'), + interleaved: true, + logStreamNames: logStreamNamesMock, + filterPattern: 'error', + startTime: fakeTime - 3 * 60 * 60 * 1000, // -3h + }) + ).to.be.equal(true); + awsLogs.provider.request.restore(); + }); }); it('should call filterLogEvents API with standard start time', () => { @@ -248,23 +245,20 @@ describe('AwsLogs', () => { filter: 'error', }; - return awsLogs.showLogs(logStreamNamesMock) - .then(() => { - expect(filterLogEventsStub.calledOnce).to.be.equal(true); - expect(filterLogEventsStub.calledWithExactly( - 'CloudWatchLogs', - 'filterLogEvents', - { - logGroupName: awsLogs.provider.naming.getLogGroupName('new-service-dev-first'), - interleaved: true, - logStreamNames: logStreamNamesMock, - startTime: 1287532800000, // '2010-10-20' - filterPattern: 'error', - } - )).to.be.equal(true); + return awsLogs.showLogs(logStreamNamesMock).then(() => { + expect(filterLogEventsStub.calledOnce).to.be.equal(true); + expect( + filterLogEventsStub.calledWithExactly('CloudWatchLogs', 'filterLogEvents', { + logGroupName: awsLogs.provider.naming.getLogGroupName('new-service-dev-first'), + interleaved: true, + logStreamNames: logStreamNamesMock, + startTime: 1287532800000, // '2010-10-20' + filterPattern: 'error', + }) + ).to.be.equal(true); - awsLogs.provider.request.restore(); - }); + awsLogs.provider.request.restore(); + }); }); it('should call filterLogEvents API with latest 10 minutes if startTime not given', () => { @@ -295,22 +289,19 @@ describe('AwsLogs', () => { logGroupName: awsLogs.provider.naming.getLogGroupName('new-service-dev-first'), }; - return awsLogs.showLogs(logStreamNamesMock) - .then(() => { - expect(filterLogEventsStub.calledOnce).to.be.equal(true); - expect(filterLogEventsStub.calledWithExactly( - 'CloudWatchLogs', - 'filterLogEvents', - { - logGroupName: awsLogs.provider.naming.getLogGroupName('new-service-dev-first'), - interleaved: true, - logStreamNames: logStreamNamesMock, - startTime: fakeTime - (10 * 60 * 1000), // fakeTime - 10 minutes - } - )).to.be.equal(true); + return awsLogs.showLogs(logStreamNamesMock).then(() => { + expect(filterLogEventsStub.calledOnce).to.be.equal(true); + expect( + filterLogEventsStub.calledWithExactly('CloudWatchLogs', 'filterLogEvents', { + logGroupName: awsLogs.provider.naming.getLogGroupName('new-service-dev-first'), + interleaved: true, + logStreamNames: logStreamNamesMock, + startTime: fakeTime - 10 * 60 * 1000, // fakeTime - 10 minutes + }) + ).to.be.equal(true); - awsLogs.provider.request.restore(); - }); + awsLogs.provider.request.restore(); + }); }); it('should call filterLogEvents API which starts 10 seconds in the past if tail given', () => { @@ -343,20 +334,19 @@ describe('AwsLogs', () => { }; const bbPromiseDelay = sinon.stub(bluebird, 'delay').rejects(); - return awsLogs.showLogs(logStreamNamesMock) + return awsLogs + .showLogs(logStreamNamesMock) .catch(() => true) // Promise.delay has to reject or it'll loop forever .then(() => { expect(filterLogEventsStub.calledOnce).to.be.equal(true); - expect(filterLogEventsStub.calledWithExactly( - 'CloudWatchLogs', - 'filterLogEvents', - { + expect( + filterLogEventsStub.calledWithExactly('CloudWatchLogs', 'filterLogEvents', { logGroupName: awsLogs.provider.naming.getLogGroupName('new-service-dev-first'), interleaved: true, logStreamNames: logStreamNamesMock, - startTime: fakeTime - (10 * 1000), // fakeTime - 10 minutes - } - )).to.be.equal(true); + startTime: fakeTime - 10 * 1000, // fakeTime - 10 minutes + }) + ).to.be.equal(true); awsLogs.provider.request.restore(); bbPromiseDelay.restore(); diff --git a/lib/plugins/aws/metrics/awsMetrics.js b/lib/plugins/aws/metrics/awsMetrics.js index 98e5640a9..e28c9bd11 100644 --- a/lib/plugins/aws/metrics/awsMetrics.js +++ b/lib/plugins/aws/metrics/awsMetrics.js @@ -15,10 +15,11 @@ class AwsMetrics { Object.assign(this, validate); this.hooks = { - 'metrics:metrics': () => BbPromise.bind(this) - .then(this.extendedValidate) - .then(this.getMetrics) - .then(this.showMetrics), + 'metrics:metrics': () => + BbPromise.bind(this) + .then(this.extendedValidate) + .then(this.getMetrics) + .then(this.showMetrics), }; } @@ -26,12 +27,16 @@ class AwsMetrics { this.validate(); const today = new Date(); - const yesterday = moment().subtract(1, 'day').toDate(); + const yesterday = moment() + .subtract(1, 'day') + .toDate(); if (this.options.startTime) { const sinceDateMatch = this.options.startTime.match(/(\d+)(m|h|d)/); if (sinceDateMatch) { - this.options.startTime = moment().subtract(sinceDateMatch[1], sinceDateMatch[2]).valueOf(); + this.options.startTime = moment() + .subtract(sinceDateMatch[1], sinceDateMatch[2]) + .valueOf(); } } @@ -46,12 +51,12 @@ class AwsMetrics { const StartTime = this.options.startTime; const EndTime = this.options.endTime; const hoursDiff = Math.abs(EndTime - StartTime) / 36e5; - const Period = (hoursDiff > 24) ? 3600 * 24 : 3600; + const Period = hoursDiff > 24 ? 3600 * 24 : 3600; const functions = this.options.function ? [this.serverless.service.getFunction(this.options.function).name] : this.serverless.service.getAllFunctionsNames(); - return BbPromise.map(functions, (functionName) => { + return BbPromise.map(functions, functionName => { const commonParams = { StartTime, EndTime, @@ -81,7 +86,7 @@ class AwsMetrics { Unit: 'Milliseconds', }); - const getMetrics = (params) => + const getMetrics = params => this.provider.request('CloudWatch', 'getMetricStatistics', params); return BbPromise.all([ @@ -89,7 +94,7 @@ class AwsMetrics { getMetrics(throttlesParams), getMetrics(errorsParams), getMetrics(averageDurationParams), - ]).then((metrics) => metrics); + ]).then(metrics => metrics); }); } @@ -107,7 +112,7 @@ class AwsMetrics { message += `${formattedStartTime} - ${formattedEndTime}\n\n`; if (metrics && metrics.length > 0) { - const getDatapointsByLabel = (Label) => + const getDatapointsByLabel = Label => _.chain(metrics) .flatten() .filter({ Label }) @@ -124,7 +129,7 @@ class AwsMetrics { message += `${chalk.yellow('Invocations:', invocationsCount, '\n')}`; message += `${chalk.yellow('Throttles:', throttlesCount, '\n')}`; message += `${chalk.yellow('Errors:', errorsCount, '\n')}`; - message += `${chalk.yellow('Duration (avg.):', `${Number((durationAverage).toFixed(2))}ms`)}`; + message += `${chalk.yellow('Duration (avg.):', `${Number(durationAverage.toFixed(2))}ms`)}`; } else { message += `${chalk.yellow('There are no metrics to show for these options')}`; } diff --git a/lib/plugins/aws/metrics/awsMetrics.test.js b/lib/plugins/aws/metrics/awsMetrics.test.js index a265a84c8..49fcbd206 100644 --- a/lib/plugins/aws/metrics/awsMetrics.test.js +++ b/lib/plugins/aws/metrics/awsMetrics.test.js @@ -42,12 +42,9 @@ describe('AwsMetrics', () => { }); it('should run promise chain in order for "metrics:metrics" hook', () => { - const extendedValidateStub = sinon - .stub(awsMetrics, 'extendedValidate').resolves(); - const getMetricsStub = sinon - .stub(awsMetrics, 'getMetrics').resolves(); - const showMetricsStub = sinon - .stub(awsMetrics, 'showMetrics').resolves(); + const extendedValidateStub = sinon.stub(awsMetrics, 'extendedValidate').resolves(); + const getMetricsStub = sinon.stub(awsMetrics, 'getMetrics').resolves(); + const showMetricsStub = sinon.stub(awsMetrics, 'showMetrics').resolves(); return awsMetrics.hooks['metrics:metrics']().then(() => { expect(extendedValidateStub.calledOnce).to.equal(true); @@ -70,8 +67,7 @@ describe('AwsMetrics', () => { }; awsMetrics.serverless.service.service = 'my-service'; awsMetrics.options.function = 'function1'; - validateStub = sinon - .stub(awsMetrics, 'validate').resolves(); + validateStub = sinon.stub(awsMetrics, 'validate').resolves(); }); afterEach(() => { @@ -81,8 +77,7 @@ describe('AwsMetrics', () => { it('should call the shared validate() function', () => awsMetrics.extendedValidate().then(() => { expect(validateStub.calledOnce).to.equal(true); - }) - ); + })); it('should set the startTime to yesterday as the default value if not provided', () => { awsMetrics.options.startTime = null; @@ -237,44 +232,52 @@ describe('AwsMetrics', () => { const expectedResult = [ [ - { ResponseMetadata: { RequestId: '1f50045b-b569-11e6-86c6-eb54d1aaa755-func1' }, + { + ResponseMetadata: { RequestId: '1f50045b-b569-11e6-86c6-eb54d1aaa755-func1' }, Label: 'Invocations', Datapoints: [], }, - { ResponseMetadata: { RequestId: '1f59059b-b569-11e6-aa18-c7bab68810d2-func1' }, + { + ResponseMetadata: { RequestId: '1f59059b-b569-11e6-aa18-c7bab68810d2-func1' }, Label: 'Throttles', Datapoints: [], }, - { ResponseMetadata: { RequestId: '1f50c7b1-b569-11e6-b1b6-ab86694b617b-func1' }, + { + ResponseMetadata: { RequestId: '1f50c7b1-b569-11e6-b1b6-ab86694b617b-func1' }, Label: 'Errors', Datapoints: [], }, - { ResponseMetadata: { RequestId: '1f63db14-b569-11e6-8501-d98a275ce164-func1' }, + { + ResponseMetadata: { RequestId: '1f63db14-b569-11e6-8501-d98a275ce164-func1' }, Label: 'Duration', Datapoints: [], }, ], [ - { ResponseMetadata: { RequestId: '1f50045b-b569-11e6-86c6-eb54d1aaa755-func2' }, + { + ResponseMetadata: { RequestId: '1f50045b-b569-11e6-86c6-eb54d1aaa755-func2' }, Label: 'Invocations', Datapoints: [], }, - { ResponseMetadata: { RequestId: '1f59059b-b569-11e6-aa18-c7bab68810d2-func2' }, + { + ResponseMetadata: { RequestId: '1f59059b-b569-11e6-aa18-c7bab68810d2-func2' }, Label: 'Throttles', Datapoints: [], }, - { ResponseMetadata: { RequestId: '1f50c7b1-b569-11e6-b1b6-ab86694b617b-func2' }, + { + ResponseMetadata: { RequestId: '1f50c7b1-b569-11e6-b1b6-ab86694b617b-func2' }, Label: 'Errors', Datapoints: [], }, - { ResponseMetadata: { RequestId: '1f63db14-b569-11e6-8501-d98a275ce164-func2' }, + { + ResponseMetadata: { RequestId: '1f63db14-b569-11e6-8501-d98a275ce164-func2' }, Label: 'Duration', Datapoints: [], }, ], ]; - return awsMetrics.getMetrics().then((result) => { + return awsMetrics.getMetrics().then(result => { expect(result).to.deep.equal(expectedResult); }); }); @@ -311,26 +314,30 @@ describe('AwsMetrics', () => { const expectedResult = [ [ - { ResponseMetadata: { RequestId: '1f50045b-b569-11e6-86c6-eb54d1aaa755-func1' }, + { + ResponseMetadata: { RequestId: '1f50045b-b569-11e6-86c6-eb54d1aaa755-func1' }, Label: 'Invocations', Datapoints: [], }, - { ResponseMetadata: { RequestId: '1f59059b-b569-11e6-aa18-c7bab68810d2-func1' }, + { + ResponseMetadata: { RequestId: '1f59059b-b569-11e6-aa18-c7bab68810d2-func1' }, Label: 'Throttles', Datapoints: [], }, - { ResponseMetadata: { RequestId: '1f50c7b1-b569-11e6-b1b6-ab86694b617b-func1' }, + { + ResponseMetadata: { RequestId: '1f50c7b1-b569-11e6-b1b6-ab86694b617b-func1' }, Label: 'Errors', Datapoints: [], }, - { ResponseMetadata: { RequestId: '1f63db14-b569-11e6-8501-d98a275ce164-func1' }, + { + ResponseMetadata: { RequestId: '1f63db14-b569-11e6-8501-d98a275ce164-func1' }, Label: 'Duration', Datapoints: [], }, ], ]; - return awsMetrics.getMetrics().then((result) => { + return awsMetrics.getMetrics().then(result => { expect(result).to.deep.equal(expectedResult); }); }); @@ -340,11 +347,13 @@ describe('AwsMetrics', () => { awsMetrics.options.endTime = new Date('1970-01-01T16:00'); return awsMetrics.getMetrics().then(() => { - expect(requestStub.calledWith( - sinon.match.string, - sinon.match.string, - sinon.match.has('Period', 3600) - )).to.equal(true); + expect( + requestStub.calledWith( + sinon.match.string, + sinon.match.string, + sinon.match.has('Period', 3600) + ) + ).to.equal(true); }); }); @@ -353,11 +362,13 @@ describe('AwsMetrics', () => { awsMetrics.options.endTime = new Date('1970-01-03'); return awsMetrics.getMetrics().then(() => { - expect(requestStub.calledWith( - sinon.match.string, - sinon.match.string, - sinon.match.has('Period', 24 * 3600) - )).to.equal(true); + expect( + requestStub.calledWith( + sinon.match.string, + sinon.match.string, + sinon.match.has('Period', 24 * 3600) + ) + ).to.equal(true); }); }); }); @@ -455,7 +466,7 @@ describe('AwsMetrics', () => { expectedMessage += `${chalk.yellow('Errors: 0 \n')}`; expectedMessage += `${chalk.yellow('Duration (avg.): 1000ms')}`; - return awsMetrics.showMetrics(metrics).then((message) => { + return awsMetrics.showMetrics(metrics).then(message => { expect(consoleLogStub.calledOnce).to.equal(true); expect(message).to.equal(expectedMessage); }); @@ -477,18 +488,15 @@ describe('AwsMetrics', () => { ], ]; - return awsMetrics.showMetrics(metrics).then((message) => { + return awsMetrics.showMetrics(metrics).then(message => { expect(message).to.include('Duration (avg.): 300ms'); }); }); it('should display 0 as average function duration if no data by given period', () => { - const metrics = [ - [], - [], - ]; + const metrics = [[], []]; - return awsMetrics.showMetrics(metrics).then((message) => { + return awsMetrics.showMetrics(metrics).then(message => { expect(message).to.include('Duration (avg.): 0ms'); }); }); @@ -537,7 +545,7 @@ describe('AwsMetrics', () => { expectedMessage += `${chalk.yellow('Errors: 0 \n')}`; expectedMessage += `${chalk.yellow('Duration (avg.): 1000ms')}`; - return awsMetrics.showMetrics(metrics).then((message) => { + return awsMetrics.showMetrics(metrics).then(message => { expect(consoleLogStub.calledOnce).to.equal(true); expect(message).to.equal(expectedMessage); }); @@ -551,7 +559,7 @@ describe('AwsMetrics', () => { expectedMessage += 'January 1, 1970 12:00 AM - January 2, 1970 12:00 AM\n\n'; expectedMessage += `${chalk.yellow('There are no metrics to show for these options')}`; - return awsMetrics.showMetrics().then((message) => { + return awsMetrics.showMetrics().then(message => { expect(consoleLogStub.calledOnce).to.equal(true); expect(message).to.equal(expectedMessage); }); diff --git a/lib/plugins/aws/package/compile/events/alb/index.js b/lib/plugins/aws/package/compile/events/alb/index.js new file mode 100644 index 000000000..b7b4eb30b --- /dev/null +++ b/lib/plugins/aws/package/compile/events/alb/index.js @@ -0,0 +1,33 @@ +'use strict'; + +const BbPromise = require('bluebird'); + +const validate = require('./lib/validate'); +const compileTargetGroups = require('./lib/targetGroups'); +const compileListenerRules = require('./lib/listenerRules'); +const compilePermissions = require('./lib/permissions'); + +class AwsCompileAlbEvents { + constructor(serverless, options) { + this.serverless = serverless; + this.options = options; + this.provider = this.serverless.getProvider('aws'); + + Object.assign(this, validate, compileTargetGroups, compileListenerRules, compilePermissions); + + this.hooks = { + 'package:compileEvents': () => { + return BbPromise.try(() => { + this.validated = this.validate(); + if (this.validated.events.length === 0) return; + + this.compileTargetGroups(); + this.compileListenerRules(); + this.compilePermissions(); + }); + }, + }; + } +} + +module.exports = AwsCompileAlbEvents; diff --git a/lib/plugins/aws/package/compile/events/alb/index.test.js b/lib/plugins/aws/package/compile/events/alb/index.test.js new file mode 100644 index 000000000..d3f4815c0 --- /dev/null +++ b/lib/plugins/aws/package/compile/events/alb/index.test.js @@ -0,0 +1,82 @@ +'use strict'; + +const expect = require('chai').expect; +const sinon = require('sinon'); +const AwsProvider = require('../../../../provider/awsProvider'); +const AwsCompileAlbEvents = require('./index'); +const Serverless = require('../../../../../../Serverless'); + +describe('AwsCompileAlbEvents', () => { + let awsCompileAlbEvents; + + beforeEach(() => { + const serverless = new Serverless(); + const options = { + stage: 'dev', + region: 'us-east-1', + }; + serverless.setProvider('aws', new AwsProvider(serverless, options)); + awsCompileAlbEvents = new AwsCompileAlbEvents(serverless, options); + }); + + describe('#constructor()', () => { + let compileTargetGroupsStub; + let compileListenerRulesStub; + let compilePermissionsStub; + + beforeEach(() => { + compileTargetGroupsStub = sinon.stub(awsCompileAlbEvents, 'compileTargetGroups').resolves(); + compileListenerRulesStub = sinon.stub(awsCompileAlbEvents, 'compileListenerRules').resolves(); + compilePermissionsStub = sinon.stub(awsCompileAlbEvents, 'compilePermissions').resolves(); + }); + + afterEach(() => { + awsCompileAlbEvents.compileTargetGroups.restore(); + awsCompileAlbEvents.compileListenerRules.restore(); + awsCompileAlbEvents.compilePermissions.restore(); + }); + + it('should have hooks', () => expect(awsCompileAlbEvents.hooks).to.be.not.empty); + + it('should set the provider variable to be an instanceof AwsProvider', () => + expect(awsCompileAlbEvents.provider).to.be.instanceof(AwsProvider)); + + describe('"package:compileEvents" promise chain', () => { + afterEach(() => { + awsCompileAlbEvents.validate.restore(); + }); + + it('should run the promise chain in order', () => { + const validateStub = sinon.stub(awsCompileAlbEvents, 'validate').returns({ + events: [ + { + functionName: 'first', + listenerArn: + 'arn:aws:elasticloadbalancing:' + + 'us-east-1:123456789012:listener/app/my-load-balancer/' + + '50dc6c495c0c9188/f2f7dc8efc522ab2', + priority: 1, + conditions: { + host: 'example.com', + path: '/hello', + }, + }, + ], + }); + + return awsCompileAlbEvents.hooks['package:compileEvents']().then(() => { + expect(validateStub.calledOnce).to.be.equal(true); + expect(compileTargetGroupsStub.calledAfter(validateStub)).to.be.equal(true); + expect(compileListenerRulesStub.calledAfter(compileTargetGroupsStub)).to.be.equal(true); + expect(compilePermissionsStub.calledAfter(compileListenerRulesStub)).to.be.equal(true); + }); + }); + }); + + it('should resolve if no functions are given', () => { + awsCompileAlbEvents.serverless.service.functions = {}; + + return awsCompileAlbEvents.hooks['package:compileEvents'](); + }); + }); +}); diff --git a/lib/plugins/aws/package/compile/events/alb/lib/listenerRules.js b/lib/plugins/aws/package/compile/events/alb/lib/listenerRules.js new file mode 100644 index 000000000..ab661d780 --- /dev/null +++ b/lib/plugins/aws/package/compile/events/alb/lib/listenerRules.js @@ -0,0 +1,82 @@ +'use strict'; + +module.exports = { + compileListenerRules() { + this.validated.events.forEach(event => { + const listenerRuleLogicalId = this.provider.naming.getAlbListenerRuleLogicalId( + event.functionName, + event.priority + ); + const targetGroupLogicalId = this.provider.naming.getAlbTargetGroupLogicalId( + event.functionName + ); + + const Conditions = [ + { + Field: 'path-pattern', + Values: event.conditions.path, + }, + ]; + if (event.conditions.host) { + Conditions.push({ + Field: 'host-header', + Values: event.conditions.host, + }); + } + if (event.conditions.method) { + Conditions.push({ + Field: 'http-request-method', + HttpRequestMethodConfig: { + Values: event.conditions.method, + }, + }); + } + if (event.conditions.header) { + Conditions.push({ + Field: 'http-header', + HttpHeaderConfig: { + HttpHeaderName: event.conditions.header.name, + Values: event.conditions.header.values, + }, + }); + } + if (event.conditions.query) { + Conditions.push({ + Field: 'query-string', + QueryStringConfig: { + Values: Object.keys(event.conditions.query).map(key => ({ + Key: key, + Value: event.conditions.query[key], + })), + }, + }); + } + if (event.conditions.ip) { + Conditions.push({ + Field: 'source-ip', + SourceIpConfig: { + Values: event.conditions.ip, + }, + }); + } + Object.assign(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, { + [listenerRuleLogicalId]: { + Type: 'AWS::ElasticLoadBalancingV2::ListenerRule', + Properties: { + Actions: [ + { + Type: 'forward', + TargetGroupArn: { + Ref: targetGroupLogicalId, + }, + }, + ], + Conditions, + ListenerArn: event.listenerArn, + Priority: event.priority, + }, + }, + }); + }); + }, +}; diff --git a/lib/plugins/aws/package/compile/events/alb/lib/listenerRules.test.js b/lib/plugins/aws/package/compile/events/alb/lib/listenerRules.test.js new file mode 100644 index 000000000..1e038d885 --- /dev/null +++ b/lib/plugins/aws/package/compile/events/alb/lib/listenerRules.test.js @@ -0,0 +1,107 @@ +'use strict'; + +const expect = require('chai').expect; +const AwsCompileAlbEvents = require('../index'); +const Serverless = require('../../../../../../../Serverless'); +const AwsProvider = require('../../../../../provider/awsProvider'); + +describe('#compileListenerRules()', () => { + let awsCompileAlbEvents; + + beforeEach(() => { + const serverless = new Serverless(); + serverless.setProvider('aws', new AwsProvider(serverless)); + serverless.service.service = 'some-service'; + serverless.service.provider.compiledCloudFormationTemplate = { Resources: {} }; + + awsCompileAlbEvents = new AwsCompileAlbEvents(serverless); + }); + + it('should create ELB listener rule resources', () => { + awsCompileAlbEvents.validated = { + events: [ + { + functionName: 'first', + listenerArn: + 'arn:aws:elasticloadbalancing:' + + 'us-east-1:123456789012:listener/app/my-load-balancer/' + + '50dc6c495c0c9188/f2f7dc8efc522ab2', + priority: 1, + conditions: { + host: ['example.com'], + path: ['/hello'], + }, + }, + { + functionName: 'second', + listenerArn: + 'arn:aws:elasticloadbalancing:' + + 'us-east-1:123456789012:listener/app/my-load-balancer/' + + '50dc6c495c0c9188/f2f7dc8efc522ab2', + priority: 2, + conditions: { + path: ['/world'], + }, + }, + ], + }; + + awsCompileAlbEvents.compileListenerRules(); + + const resources = + awsCompileAlbEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources; + + expect(resources.FirstAlbListenerRule1).to.deep.equal({ + Type: 'AWS::ElasticLoadBalancingV2::ListenerRule', + Properties: { + Actions: [ + { + TargetGroupArn: { + Ref: 'FirstAlbTargetGroup', + }, + Type: 'forward', + }, + ], + Conditions: [ + { + Field: 'path-pattern', + Values: ['/hello'], + }, + { + Field: 'host-header', + Values: ['example.com'], + }, + ], + ListenerArn: + 'arn:aws:elasticloadbalancing:' + + 'us-east-1:123456789012:listener/app/my-load-balancer/' + + '50dc6c495c0c9188/f2f7dc8efc522ab2', + Priority: 1, + }, + }); + expect(resources.SecondAlbListenerRule2).to.deep.equal({ + Type: 'AWS::ElasticLoadBalancingV2::ListenerRule', + Properties: { + Actions: [ + { + TargetGroupArn: { + Ref: 'SecondAlbTargetGroup', + }, + Type: 'forward', + }, + ], + Conditions: [ + { + Field: 'path-pattern', + Values: ['/world'], + }, + ], + ListenerArn: + 'arn:aws:elasticloadbalancing:' + + 'us-east-1:123456789012:listener/app/my-load-balancer/' + + '50dc6c495c0c9188/f2f7dc8efc522ab2', + Priority: 2, + }, + }); + }); +}); diff --git a/lib/plugins/aws/package/compile/events/alb/lib/permissions.js b/lib/plugins/aws/package/compile/events/alb/lib/permissions.js new file mode 100644 index 000000000..80df61ba2 --- /dev/null +++ b/lib/plugins/aws/package/compile/events/alb/lib/permissions.js @@ -0,0 +1,28 @@ +'use strict'; + +module.exports = { + compilePermissions() { + this.validated.events.forEach(event => { + const { functionName } = event; + + const lambdaLogicalId = this.provider.naming.getLambdaLogicalId(functionName); + const albPermissionLogicalId = this.provider.naming.getLambdaAlbPermissionLogicalId( + functionName + ); + + Object.assign(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, { + [albPermissionLogicalId]: { + Type: 'AWS::Lambda::Permission', + Properties: { + FunctionName: { + 'Fn::GetAtt': [lambdaLogicalId, 'Arn'], + }, + Action: 'lambda:InvokeFunction', + Principal: 'elasticloadbalancing.amazonaws.com', + }, + DependsOn: [lambdaLogicalId], + }, + }); + }); + }, +}; diff --git a/lib/plugins/aws/package/compile/events/alb/lib/permissions.test.js b/lib/plugins/aws/package/compile/events/alb/lib/permissions.test.js new file mode 100644 index 000000000..16473e379 --- /dev/null +++ b/lib/plugins/aws/package/compile/events/alb/lib/permissions.test.js @@ -0,0 +1,77 @@ +'use strict'; + +const expect = require('chai').expect; +const AwsCompileAlbEvents = require('../index'); +const Serverless = require('../../../../../../../Serverless'); +const AwsProvider = require('../../../../../provider/awsProvider'); + +describe('#compilePermissions()', () => { + let awsCompileAlbEvents; + + beforeEach(() => { + const serverless = new Serverless(); + serverless.setProvider('aws', new AwsProvider(serverless)); + serverless.service.service = 'some-service'; + serverless.service.provider.compiledCloudFormationTemplate = { Resources: {} }; + + awsCompileAlbEvents = new AwsCompileAlbEvents(serverless); + }); + + it('should create Lambda permission resources', () => { + awsCompileAlbEvents.validated = { + events: [ + { + functionName: 'first', + listenerArn: + 'arn:aws:elasticloadbalancing:' + + 'us-east-1:123456789012:listener/app/my-load-balancer/' + + '50dc6c495c0c9188/f2f7dc8efc522ab2', + priority: 1, + conditions: { + host: 'example.com', + path: '/hello', + }, + }, + { + functionName: 'second', + listenerArn: + 'arn:aws:elasticloadbalancing:' + + 'us-east-1:123456789012:listener/app/my-load-balancer/' + + '50dc6c495c0c9188/f2f7dc8efc522ab2', + priority: 2, + conditions: { + path: '/world', + }, + }, + ], + }; + + awsCompileAlbEvents.compilePermissions(); + + const resources = + awsCompileAlbEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources; + + expect(resources.FirstLambdaPermissionAlb).to.deep.equal({ + Type: 'AWS::Lambda::Permission', + Properties: { + Action: 'lambda:InvokeFunction', + FunctionName: { + 'Fn::GetAtt': ['FirstLambdaFunction', 'Arn'], + }, + Principal: 'elasticloadbalancing.amazonaws.com', + }, + DependsOn: ['FirstLambdaFunction'], + }); + expect(resources.SecondLambdaPermissionAlb).to.deep.equal({ + Type: 'AWS::Lambda::Permission', + Properties: { + Action: 'lambda:InvokeFunction', + FunctionName: { + 'Fn::GetAtt': ['SecondLambdaFunction', 'Arn'], + }, + Principal: 'elasticloadbalancing.amazonaws.com', + }, + DependsOn: ['SecondLambdaFunction'], + }); + }); +}); diff --git a/lib/plugins/aws/package/compile/events/alb/lib/targetGroups.js b/lib/plugins/aws/package/compile/events/alb/lib/targetGroups.js new file mode 100644 index 000000000..9d9e81763 --- /dev/null +++ b/lib/plugins/aws/package/compile/events/alb/lib/targetGroups.js @@ -0,0 +1,39 @@ +'use strict'; + +module.exports = { + compileTargetGroups() { + this.validated.events.forEach(event => { + const { functionName } = event; + + const targetGroupLogicalId = this.provider.naming.getAlbTargetGroupLogicalId(functionName); + const lambdaLogicalId = this.provider.naming.getLambdaLogicalId(functionName); + const lambdaPermissionLogicalId = this.provider.naming.getLambdaAlbPermissionLogicalId( + functionName + ); + + Object.assign(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, { + [targetGroupLogicalId]: { + Type: 'AWS::ElasticLoadBalancingV2::TargetGroup', + Properties: { + TargetType: 'lambda', + Targets: [ + { + Id: { + 'Fn::GetAtt': [lambdaLogicalId, 'Arn'], + }, + }, + ], + Name: this.provider.naming.getAlbTargetGroupName(functionName), + Tags: [ + { + Key: 'Name', + Value: this.provider.naming.getAlbTargetGroupNameTagValue(functionName), + }, + ], + }, + DependsOn: [lambdaPermissionLogicalId], + }, + }); + }); + }, +}; diff --git a/lib/plugins/aws/package/compile/events/alb/lib/targetGroups.test.js b/lib/plugins/aws/package/compile/events/alb/lib/targetGroups.test.js new file mode 100644 index 000000000..0f97c75ce --- /dev/null +++ b/lib/plugins/aws/package/compile/events/alb/lib/targetGroups.test.js @@ -0,0 +1,97 @@ +'use strict'; + +const expect = require('chai').expect; +const AwsCompileAlbEvents = require('../index'); +const Serverless = require('../../../../../../../Serverless'); +const AwsProvider = require('../../../../../provider/awsProvider'); + +describe('#compileTargetGroups()', () => { + let awsCompileAlbEvents; + + beforeEach(() => { + const serverless = new Serverless(); + serverless.setProvider('aws', new AwsProvider(serverless)); + serverless.service.service = 'some-service'; + serverless.service.provider.compiledCloudFormationTemplate = { Resources: {} }; + + awsCompileAlbEvents = new AwsCompileAlbEvents(serverless); + }); + + it('should create ELB target group resources', () => { + awsCompileAlbEvents.validated = { + events: [ + { + functionName: 'first', + listenerArn: + 'arn:aws:elasticloadbalancing:' + + 'us-east-1:123456789012:listener/app/my-load-balancer/' + + '50dc6c495c0c9188/f2f7dc8efc522ab2', + priority: 1, + conditions: { + host: 'example.com', + path: '/hello', + }, + }, + { + functionName: 'second', + listenerArn: + 'arn:aws:elasticloadbalancing:' + + 'us-east-1:123456789012:listener/app/my-load-balancer/' + + '50dc6c495c0c9188/f2f7dc8efc522ab2', + priority: 2, + conditions: { + path: '/world', + }, + }, + ], + }; + + awsCompileAlbEvents.compileTargetGroups(); + + const resources = + awsCompileAlbEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources; + + expect(resources.FirstAlbTargetGroup).to.deep.equal({ + Type: 'AWS::ElasticLoadBalancingV2::TargetGroup', + Properties: { + Name: 'd370cffdc0b94175c8239183d0b344a4', + TargetType: 'lambda', + Targets: [ + { + Id: { + 'Fn::GetAtt': ['FirstLambdaFunction', 'Arn'], + }, + }, + ], + Tags: [ + { + Key: 'Name', + Value: 'some-service-first-dev', + }, + ], + }, + DependsOn: ['FirstLambdaPermissionAlb'], + }); + expect(resources.SecondAlbTargetGroup).to.deep.equal({ + Type: 'AWS::ElasticLoadBalancingV2::TargetGroup', + Properties: { + Name: '33af5fa54e565b8aca63f904de895aab', + TargetType: 'lambda', + Targets: [ + { + Id: { + 'Fn::GetAtt': ['SecondLambdaFunction', 'Arn'], + }, + }, + ], + Tags: [ + { + Key: 'Name', + Value: 'some-service-second-dev', + }, + ], + }, + DependsOn: ['SecondLambdaPermissionAlb'], + }); + }); +}); diff --git a/lib/plugins/aws/package/compile/events/alb/lib/validate.js b/lib/plugins/aws/package/compile/events/alb/lib/validate.js new file mode 100644 index 000000000..e38dc9f70 --- /dev/null +++ b/lib/plugins/aws/package/compile/events/alb/lib/validate.js @@ -0,0 +1,99 @@ +'use strict'; + +const _ = require('lodash'); + +// eslint-disable-next-line max-len +const CIDR_IPV6_PATTERN = /^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))$/; +const CIDR_IPV4_PATTERN = /^([0-9]{1,3}\.){3}[0-9]{1,3}(\/([0-9]|[1-2][0-9]|3[0-2]))$/; + +module.exports = { + validate() { + const events = []; + + _.forEach(this.serverless.service.functions, (functionObject, functionName) => { + _.forEach(functionObject.events, event => { + if (_.has(event, 'alb')) { + if (_.isObject(event.alb)) { + const albObj = { + listenerArn: event.alb.listenerArn, + priority: event.alb.priority, + conditions: { + // concat usage allows the user to provide value as a string or an array + path: _.concat(event.alb.conditions.path), + }, + // the following is data which is not defined on the event-level + functionName, + }; + if (event.alb.conditions.host) { + albObj.conditions.host = _.concat(event.alb.conditions.host); + } + if (event.alb.conditions.method) { + albObj.conditions.method = _.concat(event.alb.conditions.method); + } + if (event.alb.conditions.header) { + albObj.conditions.header = this.validateHeaderCondition(event, functionName); + } + if (event.alb.conditions.query) { + albObj.conditions.query = this.validateQueryCondition(event, functionName); + } + if (event.alb.conditions.ip) { + albObj.conditions.ip = this.validateIpCondition(event, functionName); + } + events.push(albObj); + } + } + }); + }); + + return { + events, + }; + }, + + validateHeaderCondition(event, functionName) { + const messageTitle = `Invalid ALB event "header" condition in function "${functionName}".`; + if ( + !_.isObject(event.alb.conditions.header) || + !event.alb.conditions.header.name || + !event.alb.conditions.header.values + ) { + const errorMessage = [ + messageTitle, + ' You must provide an object with "name" and "values" properties.', + ].join(''); + throw new this.serverless.classes.Error(errorMessage); + } + if (!_.isArray(event.alb.conditions.header.values)) { + const errorMessage = [messageTitle, ' Property "values" must be an array.'].join(''); + throw new this.serverless.classes.Error(errorMessage); + } + return event.alb.conditions.header; + }, + + validateQueryCondition(event, functionName) { + if (!_.isObject(event.alb.conditions.query)) { + const errorMessage = [ + `Invalid ALB event "query" condition in function "${functionName}".`, + ' You must provide an object.', + ].join(''); + throw new this.serverless.classes.Error(errorMessage); + } + return event.alb.conditions.query; + }, + + validateIpCondition(event, functionName) { + const cidrBlocks = _.concat(event.alb.conditions.ip); + const allValuesAreCidr = cidrBlocks.every( + cidr => CIDR_IPV4_PATTERN.test(cidr) || CIDR_IPV6_PATTERN.test(cidr) + ); + + if (!allValuesAreCidr) { + const errorMessage = [ + `Invalid ALB event "ip" condition in function "${functionName}".`, + ' You must provide values in a valid IPv4 or IPv6 CIDR format.', + ].join(''); + throw new this.serverless.classes.Error(errorMessage); + } + return cidrBlocks; + }, +}; diff --git a/lib/plugins/aws/package/compile/events/alb/lib/validate.test.js b/lib/plugins/aws/package/compile/events/alb/lib/validate.test.js new file mode 100644 index 000000000..a5b69e3f5 --- /dev/null +++ b/lib/plugins/aws/package/compile/events/alb/lib/validate.test.js @@ -0,0 +1,220 @@ +'use strict'; + +const expect = require('chai').expect; +const AwsCompileAlbEvents = require('../index'); +const Serverless = require('../../../../../../../Serverless'); +const AwsProvider = require('../../../../../provider/awsProvider'); + +describe('#validate()', () => { + let awsCompileAlbEvents; + + beforeEach(() => { + const serverless = new Serverless(); + serverless.setProvider('aws', new AwsProvider(serverless)); + serverless.service.service = 'some-service'; + serverless.service.provider.compiledCloudFormationTemplate = { Resources: {} }; + + awsCompileAlbEvents = new AwsCompileAlbEvents(serverless); + }); + + it('should detect alb event definitions', () => { + awsCompileAlbEvents.serverless.service.functions = { + first: { + events: [ + { + alb: { + listenerArn: + 'arn:aws:elasticloadbalancing:' + + 'us-east-1:123456789012:listener/app/my-load-balancer/' + + '50dc6c495c0c9188/f2f7dc8efc522ab2', + priority: 1, + conditions: { + host: 'example.com', + path: '/hello', + method: 'GET', + ip: ['192.168.0.1/1', 'fe80:0000:0000:0000:0204:61ff:fe9d:f156/3'], + }, + }, + }, + ], + }, + second: { + events: [ + { + alb: { + listenerArn: + 'arn:aws:elasticloadbalancing:' + + 'us-east-1:123456789012:listener/app/my-load-balancer/' + + '50dc6c495c0c9188/f2f7dc8efc522ab2', + priority: 2, + conditions: { + path: '/world', + method: ['POST', 'GET'], + query: { + foo: 'bar', + }, + }, + }, + }, + ], + }, + }; + + const validated = awsCompileAlbEvents.validate(); + + expect(validated.events).to.deep.equal([ + { + functionName: 'first', + listenerArn: + 'arn:aws:elasticloadbalancing:' + + 'us-east-1:123456789012:listener/app/my-load-balancer/' + + '50dc6c495c0c9188/f2f7dc8efc522ab2', + priority: 1, + conditions: { + host: ['example.com'], + path: ['/hello'], + method: ['GET'], + ip: ['192.168.0.1/1', 'fe80:0000:0000:0000:0204:61ff:fe9d:f156/3'], + }, + }, + { + functionName: 'second', + listenerArn: + 'arn:aws:elasticloadbalancing:' + + 'us-east-1:123456789012:listener/app/my-load-balancer/' + + '50dc6c495c0c9188/f2f7dc8efc522ab2', + priority: 2, + conditions: { + path: ['/world'], + method: ['POST', 'GET'], + query: { + foo: 'bar', + }, + }, + }, + ]); + }); + + it('should throw when given an invalid query condition', () => { + awsCompileAlbEvents.serverless.service.functions = { + first: { + events: [ + { + alb: { + listenerArn: + 'arn:aws:elasticloadbalancing:' + + 'us-east-1:123456789012:listener/app/my-load-balancer/' + + '50dc6c495c0c9188/f2f7dc8efc522ab2', + priority: 1, + conditions: { + path: '/hello', + query: 'ss', + }, + }, + }, + ], + }, + }; + + expect(() => awsCompileAlbEvents.validate()).to.throw(Error); + }); + + it('should throw when given an invalid ip condition', () => { + awsCompileAlbEvents.serverless.service.functions = { + first: { + events: [ + { + alb: { + listenerArn: + 'arn:aws:elasticloadbalancing:' + + 'us-east-1:123456789012:listener/app/my-load-balancer/' + + '50dc6c495c0c9188/f2f7dc8efc522ab2', + priority: 1, + conditions: { + path: '/hello', + ip: '1.1.1.1', + }, + }, + }, + ], + }, + }; + + expect(() => awsCompileAlbEvents.validate()).to.throw(Error); + }); + + it('should throw when given an invalid header condition', () => { + awsCompileAlbEvents.serverless.service.functions = { + first: { + events: [ + { + alb: { + listenerArn: + 'arn:aws:elasticloadbalancing:' + + 'us-east-1:123456789012:listener/app/my-load-balancer/' + + '50dc6c495c0c9188/f2f7dc8efc522ab2', + priority: 1, + conditions: { + path: '/hello', + header: ['foo'], + }, + }, + }, + ], + }, + }; + + expect(() => awsCompileAlbEvents.validate()).to.throw(Error); + }); + + describe('#validateIpCondition()', () => { + it('should throw if ip is not a valid ipv6 or ipv4 cidr block', () => { + const event = { alb: { conditions: { ip: 'fe80:0000:0000:0000:0204:61ff:fe9d:f156/' } } }; + expect(() => awsCompileAlbEvents.validateIpCondition(event, '')).to.throw(Error); + }); + + it('should return the value as array if it is a valid ipv6 cidr block', () => { + const event = { alb: { conditions: { ip: 'fe80:0000:0000:0000:0204:61ff:fe9d:f156/127' } } }; + expect(awsCompileAlbEvents.validateIpCondition(event, '')).to.deep.equal([ + 'fe80:0000:0000:0000:0204:61ff:fe9d:f156/127', + ]); + }); + + it('should return the value as array if it is a valid ipv4 cidr block', () => { + const event = { alb: { conditions: { ip: '192.168.0.1/21' } } }; + expect(awsCompileAlbEvents.validateIpCondition(event, '')).to.deep.equal(['192.168.0.1/21']); + }); + }); + + describe('#validateQueryCondition()', () => { + it('should throw if query is not an object', () => { + const event = { alb: { conditions: { query: 'foo' } } }; + expect(() => awsCompileAlbEvents.validateQueryCondition(event, '')).to.throw(Error); + }); + + it('should return the value if it is an object', () => { + const event = { alb: { conditions: { query: { foo: 'bar' } } } }; + expect(awsCompileAlbEvents.validateQueryCondition(event, '')).to.deep.equal({ foo: 'bar' }); + }); + }); + + describe('#validateHeaderCondition()', () => { + it('should throw if header does not have the required properties', () => { + const event = { alb: { conditions: { header: { name: 'foo', value: 'bar' } } } }; + expect(() => awsCompileAlbEvents.validateHeaderCondition(event, '')).to.throw(Error); + }); + + it('should throw if header.values is not an array', () => { + const event = { alb: { conditions: { header: { name: 'foo', values: 'bar' } } } }; + expect(() => awsCompileAlbEvents.validateHeaderCondition(event, '')).to.throw(Error); + }); + + it('should return the value if it is valid', () => { + const event = { alb: { conditions: { header: { name: 'foo', values: ['bar'] } } } }; + expect(awsCompileAlbEvents.validateHeaderCondition(event, '')).to.deep.equal({ + name: 'foo', + values: ['bar'], + }); + }); + }); +}); diff --git a/lib/plugins/aws/package/compile/events/alexaSkill/index.js b/lib/plugins/aws/package/compile/events/alexaSkill/index.js index 89c863bb4..5b9ca7d91 100644 --- a/lib/plugins/aws/package/compile/events/alexaSkill/index.js +++ b/lib/plugins/aws/package/compile/events/alexaSkill/index.js @@ -13,7 +13,7 @@ class AwsCompileAlexaSkillEvents { } compileAlexaSkillEvents() { - this.serverless.service.getAllFunctions().forEach((functionName) => { + this.serverless.service.getAllFunctions().forEach(functionName => { const functionObj = this.serverless.service.getFunction(functionName); let alexaSkillNumberInFunction = 0; @@ -24,7 +24,7 @@ class AwsCompileAlexaSkillEvents { let appId; if (event === 'alexaSkill') { const warningMessage = [ - 'Warning! You are using an old syntax for alexaSkill which doesn\'t', + "Warning! You are using an old syntax for alexaSkill which doesn't", ' restrict the invocation solely to your skill.', ' Please refer to the documentation for additional information.', ].join(''); @@ -54,17 +54,13 @@ class AwsCompileAlexaSkillEvents { } alexaSkillNumberInFunction++; - const lambdaLogicalId = this.provider.naming - .getLambdaLogicalId(functionName); + const lambdaLogicalId = this.provider.naming.getLambdaLogicalId(functionName); const permissionTemplate = { Type: 'AWS::Lambda::Permission', Properties: { FunctionName: { - 'Fn::GetAtt': [ - lambdaLogicalId, - 'Arn', - ], + 'Fn::GetAtt': [lambdaLogicalId, 'Arn'], }, Action: enabled ? 'lambda:InvokeFunction' : 'lambda:DisableInvokeFunction', Principal: 'alexa-appkit.amazon.com', @@ -75,16 +71,19 @@ class AwsCompileAlexaSkillEvents { permissionTemplate.Properties.EventSourceToken = appId.replace(/\\n|\\r/g, ''); } - const lambdaPermissionLogicalId = this.provider.naming - .getLambdaAlexaSkillPermissionLogicalId(functionName, - alexaSkillNumberInFunction); + const lambdaPermissionLogicalId = this.provider.naming.getLambdaAlexaSkillPermissionLogicalId( + functionName, + alexaSkillNumberInFunction + ); const permissionCloudForamtionResource = { [lambdaPermissionLogicalId]: permissionTemplate, }; - _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, - permissionCloudForamtionResource); + _.merge( + this.serverless.service.provider.compiledCloudFormationTemplate.Resources, + permissionCloudForamtionResource + ); } }); } diff --git a/lib/plugins/aws/package/compile/events/alexaSkill/index.test.js b/lib/plugins/aws/package/compile/events/alexaSkill/index.test.js index cb9090ae6..3c96f9128 100644 --- a/lib/plugins/aws/package/compile/events/alexaSkill/index.test.js +++ b/lib/plugins/aws/package/compile/events/alexaSkill/index.test.js @@ -38,9 +38,7 @@ describe('AwsCompileAlexaSkillEvents', () => { it('should show a warning if alexaSkill appId is not specified', () => { awsCompileAlexaSkillEvents.serverless.service.functions = { first: { - events: [ - 'alexaSkill', - ], + events: ['alexaSkill'], }, }; @@ -48,25 +46,25 @@ describe('AwsCompileAlexaSkillEvents', () => { expect(consolePrinted).to.contain.string('old syntax for alexaSkill'); - expect(awsCompileAlexaSkillEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionAlexaSkill1.Type + expect( + awsCompileAlexaSkillEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLambdaPermissionAlexaSkill1.Type ).to.equal('AWS::Lambda::Permission'); - expect(awsCompileAlexaSkillEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionAlexaSkill1.Properties.FunctionName + expect( + awsCompileAlexaSkillEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLambdaPermissionAlexaSkill1.Properties.FunctionName ).to.deep.equal({ 'Fn::GetAtt': ['FirstLambdaFunction', 'Arn'] }); - expect(awsCompileAlexaSkillEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionAlexaSkill1.Properties.Action + expect( + awsCompileAlexaSkillEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLambdaPermissionAlexaSkill1.Properties.Action ).to.equal('lambda:InvokeFunction'); - expect(awsCompileAlexaSkillEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionAlexaSkill1.Properties.Principal + expect( + awsCompileAlexaSkillEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLambdaPermissionAlexaSkill1.Properties.Principal ).to.equal('alexa-appkit.amazon.com'); - expect(awsCompileAlexaSkillEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionAlexaSkill1.Properties.EventSourceToken + expect( + awsCompileAlexaSkillEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLambdaPermissionAlexaSkill1.Properties.EventSourceToken ).to.be.undefined; }); @@ -120,46 +118,46 @@ describe('AwsCompileAlexaSkillEvents', () => { awsCompileAlexaSkillEvents.compileAlexaSkillEvents(); - expect(awsCompileAlexaSkillEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionAlexaSkill1.Type + expect( + awsCompileAlexaSkillEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLambdaPermissionAlexaSkill1.Type ).to.equal('AWS::Lambda::Permission'); - expect(awsCompileAlexaSkillEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionAlexaSkill1.Properties.FunctionName + expect( + awsCompileAlexaSkillEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLambdaPermissionAlexaSkill1.Properties.FunctionName ).to.deep.equal({ 'Fn::GetAtt': ['FirstLambdaFunction', 'Arn'] }); - expect(awsCompileAlexaSkillEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionAlexaSkill1.Properties.Action + expect( + awsCompileAlexaSkillEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLambdaPermissionAlexaSkill1.Properties.Action ).to.equal('lambda:InvokeFunction'); - expect(awsCompileAlexaSkillEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionAlexaSkill1.Properties.Principal + expect( + awsCompileAlexaSkillEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLambdaPermissionAlexaSkill1.Properties.Principal ).to.equal('alexa-appkit.amazon.com'); - expect(awsCompileAlexaSkillEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionAlexaSkill1.Properties.EventSourceToken + expect( + awsCompileAlexaSkillEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLambdaPermissionAlexaSkill1.Properties.EventSourceToken ).to.equal(skillId1); - expect(awsCompileAlexaSkillEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionAlexaSkill2.Type + expect( + awsCompileAlexaSkillEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLambdaPermissionAlexaSkill2.Type ).to.equal('AWS::Lambda::Permission'); - expect(awsCompileAlexaSkillEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionAlexaSkill2.Properties.FunctionName + expect( + awsCompileAlexaSkillEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLambdaPermissionAlexaSkill2.Properties.FunctionName ).to.deep.equal({ 'Fn::GetAtt': ['FirstLambdaFunction', 'Arn'] }); - expect(awsCompileAlexaSkillEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionAlexaSkill2.Properties.Action + expect( + awsCompileAlexaSkillEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLambdaPermissionAlexaSkill2.Properties.Action ).to.equal('lambda:InvokeFunction'); - expect(awsCompileAlexaSkillEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionAlexaSkill2.Properties.Principal + expect( + awsCompileAlexaSkillEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLambdaPermissionAlexaSkill2.Properties.Principal ).to.equal('alexa-appkit.amazon.com'); - expect(awsCompileAlexaSkillEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionAlexaSkill2.Properties.EventSourceToken + expect( + awsCompileAlexaSkillEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLambdaPermissionAlexaSkill2.Properties.EventSourceToken ).to.equal(skillId2); }); @@ -180,25 +178,25 @@ describe('AwsCompileAlexaSkillEvents', () => { awsCompileAlexaSkillEvents.compileAlexaSkillEvents(); - expect(awsCompileAlexaSkillEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionAlexaSkill1.Type + expect( + awsCompileAlexaSkillEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLambdaPermissionAlexaSkill1.Type ).to.equal('AWS::Lambda::Permission'); - expect(awsCompileAlexaSkillEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionAlexaSkill1.Properties.FunctionName + expect( + awsCompileAlexaSkillEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLambdaPermissionAlexaSkill1.Properties.FunctionName ).to.deep.equal({ 'Fn::GetAtt': ['FirstLambdaFunction', 'Arn'] }); - expect(awsCompileAlexaSkillEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionAlexaSkill1.Properties.Action + expect( + awsCompileAlexaSkillEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLambdaPermissionAlexaSkill1.Properties.Action ).to.equal('lambda:DisableInvokeFunction'); - expect(awsCompileAlexaSkillEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionAlexaSkill1.Properties.Principal + expect( + awsCompileAlexaSkillEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLambdaPermissionAlexaSkill1.Properties.Principal ).to.equal('alexa-appkit.amazon.com'); - expect(awsCompileAlexaSkillEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionAlexaSkill1.Properties.EventSourceToken + expect( + awsCompileAlexaSkillEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLambdaPermissionAlexaSkill1.Properties.EventSourceToken ).to.equal(skillId1); }); @@ -212,8 +210,8 @@ describe('AwsCompileAlexaSkillEvents', () => { awsCompileAlexaSkillEvents.compileAlexaSkillEvents(); expect( - awsCompileAlexaSkillEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources + awsCompileAlexaSkillEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources ).to.deep.equal({}); }); diff --git a/lib/plugins/aws/package/compile/events/alexaSmartHome/index.js b/lib/plugins/aws/package/compile/events/alexaSmartHome/index.js index 97d34abf7..24e0efdcc 100644 --- a/lib/plugins/aws/package/compile/events/alexaSmartHome/index.js +++ b/lib/plugins/aws/package/compile/events/alexaSmartHome/index.js @@ -13,7 +13,7 @@ class AwsCompileAlexaSmartHomeEvents { } compileAlexaSmartHomeEvents() { - this.serverless.service.getAllFunctions().forEach((functionName) => { + this.serverless.service.getAllFunctions().forEach(functionName => { const functionObj = this.serverless.service.getFunction(functionName); let alexaSmartHomeNumberInFunction = 0; @@ -32,12 +32,13 @@ class AwsCompileAlexaSmartHomeEvents { ' OR an object with "appId" property.', ' Please check the docs for more info.', ].join(''); - throw new this.serverless.classes - .Error(errorMessage); + throw new this.serverless.classes.Error(errorMessage); } EventSourceToken = event.alexaSmartHome.appId; - Action = event.alexaSmartHome.enabled !== false ? - 'lambda:InvokeFunction' : 'lambda:DisableInvokeFunction'; + Action = + event.alexaSmartHome.enabled !== false + ? 'lambda:InvokeFunction' + : 'lambda:DisableInvokeFunction'; } else if (typeof event.alexaSmartHome === 'string') { EventSourceToken = event.alexaSmartHome; Action = 'lambda:InvokeFunction'; @@ -49,17 +50,13 @@ class AwsCompileAlexaSmartHomeEvents { ].join(''); throw new this.serverless.classes.Error(errorMessage); } - const lambdaLogicalId = this.provider.naming - .getLambdaLogicalId(functionName); + const lambdaLogicalId = this.provider.naming.getLambdaLogicalId(functionName); const permissionTemplate = { Type: 'AWS::Lambda::Permission', Properties: { FunctionName: { - 'Fn::GetAtt': [ - lambdaLogicalId, - 'Arn', - ], + 'Fn::GetAtt': [lambdaLogicalId, 'Arn'], }, Action: Action.replace(/\\n|\\r/g, ''), Principal: 'alexa-connectedhome.amazon.com', @@ -67,16 +64,19 @@ class AwsCompileAlexaSmartHomeEvents { }, }; - const lambdaPermissionLogicalId = this.provider.naming - .getLambdaAlexaSmartHomePermissionLogicalId(functionName, - alexaSmartHomeNumberInFunction); + const lambdaPermissionLogicalId = this.provider.naming.getLambdaAlexaSmartHomePermissionLogicalId( + functionName, + alexaSmartHomeNumberInFunction + ); const permissionCloudFormationResource = { [lambdaPermissionLogicalId]: permissionTemplate, }; - _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, - permissionCloudFormationResource); + _.merge( + this.serverless.service.provider.compiledCloudFormationTemplate.Resources, + permissionCloudFormationResource + ); } }); } diff --git a/lib/plugins/aws/package/compile/events/alexaSmartHome/index.test.js b/lib/plugins/aws/package/compile/events/alexaSmartHome/index.test.js index 4c0692846..2ec877993 100644 --- a/lib/plugins/aws/package/compile/events/alexaSmartHome/index.test.js +++ b/lib/plugins/aws/package/compile/events/alexaSmartHome/index.test.js @@ -78,65 +78,65 @@ describe('AwsCompileAlexaSmartHomeEvents', () => { awsCompileAlexaSmartHomeEvents.compileAlexaSmartHomeEvents(); - expect(awsCompileAlexaSmartHomeEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionAlexaSmartHome1.Type + expect( + awsCompileAlexaSmartHomeEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLambdaPermissionAlexaSmartHome1.Type ).to.equal('AWS::Lambda::Permission'); - expect(awsCompileAlexaSmartHomeEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionAlexaSmartHome2.Type + expect( + awsCompileAlexaSmartHomeEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLambdaPermissionAlexaSmartHome2.Type ).to.equal('AWS::Lambda::Permission'); - expect(awsCompileAlexaSmartHomeEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionAlexaSmartHome3.Type + expect( + awsCompileAlexaSmartHomeEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLambdaPermissionAlexaSmartHome3.Type ).to.equal('AWS::Lambda::Permission'); - expect(awsCompileAlexaSmartHomeEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionAlexaSmartHome1.Properties.FunctionName + 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 + 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 + 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 + expect( + awsCompileAlexaSmartHomeEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLambdaPermissionAlexaSmartHome1.Properties.Action ).to.equal('lambda:DisableInvokeFunction'); - expect(awsCompileAlexaSmartHomeEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionAlexaSmartHome2.Properties.Action + expect( + awsCompileAlexaSmartHomeEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLambdaPermissionAlexaSmartHome2.Properties.Action ).to.equal('lambda:InvokeFunction'); - expect(awsCompileAlexaSmartHomeEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionAlexaSmartHome3.Properties.Action + expect( + awsCompileAlexaSmartHomeEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLambdaPermissionAlexaSmartHome3.Properties.Action ).to.equal('lambda:InvokeFunction'); - expect(awsCompileAlexaSmartHomeEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionAlexaSmartHome1.Properties.Principal + 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 + 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 + 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 + 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 + 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 + expect( + awsCompileAlexaSmartHomeEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLambdaPermissionAlexaSmartHome3.Properties.EventSourceToken ).to.equal('amzn1.ask.skill.zz-zz-zz-zz'); }); @@ -170,30 +170,28 @@ describe('AwsCompileAlexaSmartHomeEvents', () => { awsCompileAlexaSmartHomeEvents.compileAlexaSmartHomeEvents(); - expect(awsCompileAlexaSmartHomeEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionAlexaSmartHome1.Properties.Action + expect( + awsCompileAlexaSmartHomeEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLambdaPermissionAlexaSmartHome1.Properties.Action ).to.equal('lambda:DisableInvokeFunction'); - expect(awsCompileAlexaSmartHomeEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionAlexaSmartHome2.Properties.Action + expect( + awsCompileAlexaSmartHomeEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLambdaPermissionAlexaSmartHome2.Properties.Action ).to.equal('lambda:InvokeFunction'); - expect(awsCompileAlexaSmartHomeEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionAlexaSmartHome3.Properties.Action + expect( + awsCompileAlexaSmartHomeEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLambdaPermissionAlexaSmartHome3.Properties.Action ).to.equal('lambda:InvokeFunction'); - expect(awsCompileAlexaSmartHomeEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionAlexaSmartHome4.Properties.Action + 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', - ], + events: ['alexaSkill'], }, }; diff --git a/lib/plugins/aws/package/compile/events/apiGateway/index.js b/lib/plugins/aws/package/compile/events/apiGateway/index.js index 42f4f27f1..b89ca8d96 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/index.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/index.js @@ -90,7 +90,8 @@ class AwsCompileApigEvents { // eslint-disable-next-line no-shadow const validate = require('../../../../lib/validate').validate; // eslint-disable-next-line max-len - const disassociateUsagePlan = require('./lib/hack/disassociateUsagePlan').disassociateUsagePlan; + const disassociateUsagePlan = require('./lib/hack/disassociateUsagePlan') + .disassociateUsagePlan; return BbPromise.bind(this) .then(validate) diff --git a/lib/plugins/aws/package/compile/events/apiGateway/index.test.js b/lib/plugins/aws/package/compile/events/apiGateway/index.test.js index cc15ab4b5..ca4042838 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/index.test.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/index.test.js @@ -48,24 +48,17 @@ describe('AwsCompileApigEvents', () => { let disassociateUsagePlanStub; beforeEach(() => { - compileRestApiStub = sinon - .stub(awsCompileApigEvents, 'compileRestApi').resolves(); - compileResourcesStub = sinon - .stub(awsCompileApigEvents, 'compileResources').resolves(); - compileMethodsStub = sinon - .stub(awsCompileApigEvents, 'compileMethods').resolves(); - compileDeploymentStub = sinon - .stub(awsCompileApigEvents, 'compileDeployment').resolves(); - compileUsagePlanStub = sinon - .stub(awsCompileApigEvents, 'compileUsagePlan').resolves(); - compilePermissionsStub = sinon - .stub(awsCompileApigEvents, 'compilePermissions').resolves(); - compileStageStub = sinon - .stub(awsCompileApigEvents, 'compileStage').resolves(); - updateStageStub = sinon - .stub(updateStage, 'updateStage').resolves(); + compileRestApiStub = sinon.stub(awsCompileApigEvents, 'compileRestApi').resolves(); + compileResourcesStub = sinon.stub(awsCompileApigEvents, 'compileResources').resolves(); + compileMethodsStub = sinon.stub(awsCompileApigEvents, 'compileMethods').resolves(); + compileDeploymentStub = sinon.stub(awsCompileApigEvents, 'compileDeployment').resolves(); + compileUsagePlanStub = sinon.stub(awsCompileApigEvents, 'compileUsagePlan').resolves(); + compilePermissionsStub = sinon.stub(awsCompileApigEvents, 'compilePermissions').resolves(); + compileStageStub = sinon.stub(awsCompileApigEvents, 'compileStage').resolves(); + updateStageStub = sinon.stub(updateStage, 'updateStage').resolves(); disassociateUsagePlanStub = sinon - .stub(disassociateUsagePlan, 'disassociateUsagePlan').resolves(); + .stub(disassociateUsagePlan, 'disassociateUsagePlan') + .resolves(); }); afterEach(() => { @@ -89,18 +82,17 @@ describe('AwsCompileApigEvents', () => { expect(awsCompileApigEvents.apiGatewayMethodLogicalIds).to.deep.equal([])); it('should run "package:compileEvents" promise chain in order', () => { - const validateStub = sinon - .stub(awsCompileApigEvents, 'validate').returns({ - events: [ - { - functionName: 'first', - http: { - path: 'users', - method: 'POST', - }, + const validateStub = sinon.stub(awsCompileApigEvents, 'validate').returns({ + events: [ + { + functionName: 'first', + http: { + path: 'users', + method: 'POST', }, - ], - }); + }, + ], + }); return awsCompileApigEvents.hooks['package:compileEvents']().then(() => { expect(validateStub.calledOnce).to.be.equal(true); @@ -120,8 +112,7 @@ describe('AwsCompileApigEvents', () => { let getServiceStateStub; beforeEach(() => { - getServiceStateStub = sinon - .stub(getServiceState, 'getServiceState'); + getServiceStateStub = sinon.stub(getServiceState, 'getServiceState'); }); afterEach(() => { diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/apiKeys.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/apiKeys.js index e176f4c82..ee875c1a9 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/apiKeys.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/apiKeys.js @@ -14,10 +14,12 @@ function createApiKeyResource(that, apiKey) { Name: name, Value: value, Description: description, - StageKeys: [{ - RestApiId: that.provider.getApiGatewayRestApiId(), - StageName: that.provider.getStage(), - }], + StageKeys: [ + { + RestApiId: that.provider.getApiGatewayRestApiId(), + StageName: that.provider.getStage(), + }, + ], }, DependsOn: that.apiGatewayDeploymentLogicalId, }; @@ -41,22 +43,25 @@ module.exports = { } const resources = this.serverless.service.provider.compiledCloudFormationTemplate.Resources; let keyNumber = 0; - _.forEach(this.serverless.service.provider.apiKeys, (apiKeyDefinition) => { + _.forEach(this.serverless.service.provider.apiKeys, apiKeyDefinition => { // if multiple API key types are used const name = _.first(_.keys(apiKeyDefinition)); - if (_.isObject(apiKeyDefinition) && - _.includes(_.flatten(_.map(this.serverless.service.provider.usagePlan, - (item) => _.keys(item))), name)) { + if ( + _.isObject(apiKeyDefinition) && + _.includes( + _.flatten(_.map(this.serverless.service.provider.usagePlan, item => _.keys(item))), + name + ) + ) { keyNumber = 0; - _.forEach(apiKeyDefinition[name], (key) => { + _.forEach(apiKeyDefinition[name], key => { if (!this.validateApiKeyInput(key)) { throw new this.serverless.classes.Error( 'API Key must be a string or an object which contains name and/or value' ); } keyNumber += 1; - const apiKeyLogicalId = this.provider.naming - .getApiKeyLogicalId(keyNumber, name); + const apiKeyLogicalId = this.provider.naming.getApiKeyLogicalId(keyNumber, name); const resourceTemplate = createApiKeyResource(this, key); _.merge(resources, { [apiKeyLogicalId]: resourceTemplate, @@ -69,8 +74,7 @@ module.exports = { 'API Key must be a string or an object which contains name and/or value' ); } - const apiKeyLogicalId = this.provider.naming - .getApiKeyLogicalId(keyNumber); + const apiKeyLogicalId = this.provider.naming.getApiKeyLogicalId(keyNumber); const resourceTemplate = createApiKeyResource(this, apiKeyDefinition); _.merge(resources, { [apiKeyLogicalId]: resourceTemplate, diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/apiKeys.test.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/apiKeys.test.js index e6eaab68c..cba1c2f2e 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/apiKeys.test.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/apiKeys.test.js @@ -97,17 +97,16 @@ describe('#compileApiKeys()', () => { describe('when using usage plan notation', () => { it('should support usage plan notation', () => { - awsCompileApigEvents.serverless.service.provider.usagePlan = [ - { free: [] }, - { paid: [] }, - ]; + awsCompileApigEvents.serverless.service.provider.usagePlan = [{ free: [] }, { paid: [] }]; awsCompileApigEvents.serverless.service.provider.apiKeys = [ - { free: [ - '1234567890', - { name: '2345678901' }, - { value: 'valueForKeyWithoutName', description: 'Api key description' }, - { name: '3456789012', value: 'valueForKey3456789012' }, - ] }, + { + free: [ + '1234567890', + { name: '2345678901' }, + { value: 'valueForKeyWithoutName', description: 'Api key description' }, + { name: '3456789012', value: 'valueForKey3456789012' }, + ], + }, { paid: ['0987654321', 'jihgfedcba'] }, ]; @@ -128,57 +127,57 @@ describe('#compileApiKeys()', () => { { name: 'jihgfedcba', value: undefined, description: undefined }, ], }; - _.forEach(awsCompileApigEvents.serverless.service.provider.apiKeys, (plan) => { + _.forEach(awsCompileApigEvents.serverless.service.provider.apiKeys, plan => { const planName = _.first(_.keys(plan)); // free || paid const apiKeys = expectedApiKeys[planName]; _.forEach(apiKeys, (apiKey, index) => { expect( awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate .Resources[ - awsCompileApigEvents.provider.naming.getApiKeyLogicalId(index + 1, planName) - ].Type + awsCompileApigEvents.provider.naming.getApiKeyLogicalId(index + 1, planName) + ].Type ).to.equal('AWS::ApiGateway::ApiKey'); expect( awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate .Resources[ - awsCompileApigEvents.provider.naming.getApiKeyLogicalId(index + 1, planName) - ].Properties.Enabled + awsCompileApigEvents.provider.naming.getApiKeyLogicalId(index + 1, planName) + ].Properties.Enabled ).to.equal(true); expect( awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate .Resources[ - awsCompileApigEvents.provider.naming.getApiKeyLogicalId(index + 1, planName) - ].Properties.Name + awsCompileApigEvents.provider.naming.getApiKeyLogicalId(index + 1, planName) + ].Properties.Name ).to.equal(apiKey.name); expect( awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate .Resources[ - awsCompileApigEvents.provider.naming.getApiKeyLogicalId(index + 1, planName) - ].Properties.Description + awsCompileApigEvents.provider.naming.getApiKeyLogicalId(index + 1, planName) + ].Properties.Description ).to.equal(apiKey.description); expect( awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate .Resources[ - awsCompileApigEvents.provider.naming.getApiKeyLogicalId(index + 1, planName) - ].Properties.Value + awsCompileApigEvents.provider.naming.getApiKeyLogicalId(index + 1, planName) + ].Properties.Value ).to.equal(apiKey.value); expect( awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate .Resources[ - awsCompileApigEvents.provider.naming.getApiKeyLogicalId(index + 1, planName) - ].Properties.StageKeys[0].RestApiId.Ref + awsCompileApigEvents.provider.naming.getApiKeyLogicalId(index + 1, planName) + ].Properties.StageKeys[0].RestApiId.Ref ).to.equal('ApiGatewayRestApi'); expect( awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate .Resources[ - awsCompileApigEvents.provider.naming.getApiKeyLogicalId(index + 1, planName) - ].Properties.StageKeys[0].StageName + awsCompileApigEvents.provider.naming.getApiKeyLogicalId(index + 1, planName) + ].Properties.StageKeys[0].StageName ).to.equal('dev'); expect( awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate .Resources[ - awsCompileApigEvents.provider.naming.getApiKeyLogicalId(index + 1, planName) - ].DependsOn + awsCompileApigEvents.provider.naming.getApiKeyLogicalId(index + 1, planName) + ].DependsOn ).to.equal('ApiGatewayDeploymentTest'); }); }); @@ -187,9 +186,11 @@ describe('#compileApiKeys()', () => { }); it('throw error if an apiKey is not a valid object', () => { - awsCompileApigEvents.serverless.service.provider.apiKeys = [{ - named: 'invalid', - }]; + awsCompileApigEvents.serverless.service.provider.apiKeys = [ + { + named: 'invalid', + }, + ]; expect(() => awsCompileApigEvents.compileApiKeys()).to.throw(Error); }); }); 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 118b60325..90eb45db4 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/authorizers.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/authorizers.js @@ -6,7 +6,7 @@ const awsArnRegExs = require('../../../../../utils/arnRegularExpressions'); module.exports = { compileAuthorizers() { - this.validated.events.forEach((event) => { + this.validated.events.forEach(event => { if (event.http.authorizer && event.http.authorizer.arn) { const authorizer = event.http.authorizer; const authorizerProperties = { @@ -24,14 +24,16 @@ module.exports = { const authorizerLogicalId = this.provider.naming.getAuthorizerLogicalId(authorizer.name); - if (typeof authorizer.arn === 'string' - && awsArnRegExs.cognitoIdpArnExpr.test(authorizer.arn)) { + if ( + typeof authorizer.arn === 'string' && + awsArnRegExs.cognitoIdpArnExpr.test(authorizer.arn) + ) { authorizerProperties.Type = 'COGNITO_USER_POOLS'; authorizerProperties.ProviderARNs = [authorizer.arn]; } else { - authorizerProperties.AuthorizerUri = - { - 'Fn::Join': ['', + authorizerProperties.AuthorizerUri = { + 'Fn::Join': [ + '', [ 'arn:', { Ref: 'AWS::Partition' }, 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 5ec1dc893..5b2e4515b 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 @@ -5,7 +5,6 @@ const AwsCompileApigEvents = require('../index'); const Serverless = require('../../../../../../../Serverless'); const AwsProvider = require('../../../../../provider/awsProvider'); - describe('#compileAuthorizers()', () => { let awsCompileApigEvents; @@ -20,27 +19,31 @@ describe('#compileAuthorizers()', () => { }); it('should create an authorizer with minimal configuration', () => { - awsCompileApigEvents.validated.events = [{ - http: { - path: 'users/create', - method: 'POST', - authorizer: { - name: 'authorizer', - arn: { 'Fn::GetAtt': ['SomeLambdaFunction', 'Arn'] }, - resultTtlInSeconds: 300, - identitySource: 'method.request.header.Authorization', + awsCompileApigEvents.validated.events = [ + { + http: { + path: 'users/create', + method: 'POST', + authorizer: { + name: 'authorizer', + arn: { 'Fn::GetAtt': ['SomeLambdaFunction', 'Arn'] }, + resultTtlInSeconds: 300, + identitySource: 'method.request.header.Authorization', + }, }, }, - }]; + ]; return awsCompileApigEvents.compileAuthorizers().then(() => { - const resource = awsCompileApigEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources.AuthorizerApiGatewayAuthorizer; + const resource = + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .AuthorizerApiGatewayAuthorizer; expect(resource.Type).to.equal('AWS::ApiGateway::Authorizer'); expect(resource.Properties.AuthorizerResultTtlInSeconds).to.equal(300); expect(resource.Properties.AuthorizerUri).to.deep.equal({ - 'Fn::Join': ['', + 'Fn::Join': [ + '', [ 'arn:', { Ref: 'AWS::Partition' }, @@ -61,27 +64,31 @@ describe('#compileAuthorizers()', () => { }); it('should create an authorizer with provided configuration', () => { - awsCompileApigEvents.validated.events = [{ - http: { - path: 'users/create', - method: 'POST', - authorizer: { - name: 'authorizer', - arn: 'foo', - resultTtlInSeconds: 500, - identitySource: 'method.request.header.Custom', - identityValidationExpression: 'regex', + awsCompileApigEvents.validated.events = [ + { + http: { + path: 'users/create', + method: 'POST', + authorizer: { + name: 'authorizer', + arn: 'foo', + resultTtlInSeconds: 500, + identitySource: 'method.request.header.Custom', + identityValidationExpression: 'regex', + }, }, }, - }]; + ]; return awsCompileApigEvents.compileAuthorizers().then(() => { - const resource = awsCompileApigEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources.AuthorizerApiGatewayAuthorizer; + const resource = + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .AuthorizerApiGatewayAuthorizer; expect(resource.Type).to.equal('AWS::ApiGateway::Authorizer'); expect(resource.Properties.AuthorizerUri).to.deep.equal({ - 'Fn::Join': ['', + 'Fn::Join': [ + '', [ 'arn:', { Ref: 'AWS::Partition' }, @@ -103,23 +110,26 @@ 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', + 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; + const resource = + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .AuthorizerApiGatewayAuthorizer; expect(resource.Type).to.equal('AWS::ApiGateway::Authorizer'); expect(resource.Properties.Type).to.equal('REQUEST'); @@ -127,22 +137,25 @@ describe('#compileAuthorizers()', () => { }); 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', + 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; + const resource = + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .AuthorizerApiGatewayAuthorizer; expect(resource.Type).to.equal('AWS::ApiGateway::Authorizer'); expect(resource.Properties.Type).to.equal('TOKEN'); @@ -150,25 +163,29 @@ describe('#compileAuthorizers()', () => { }); it('should create a valid cognito user pool authorizer', () => { - awsCompileApigEvents.validated.events = [{ - http: { - path: 'users/create', - method: 'POST', - authorizer: { - name: 'authorizer', - arn: 'arn:aws:cognito-idp:us-east-1:xxx:userpool/us-east-1_ZZZ', + awsCompileApigEvents.validated.events = [ + { + http: { + path: 'users/create', + method: 'POST', + authorizer: { + name: 'authorizer', + arn: 'arn:aws:cognito-idp:us-east-1:xxx:userpool/us-east-1_ZZZ', + }, }, }, - }]; + ]; return awsCompileApigEvents.compileAuthorizers().then(() => { - const resource = awsCompileApigEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources.AuthorizerApiGatewayAuthorizer; + const resource = + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .AuthorizerApiGatewayAuthorizer; expect(resource.Properties.Name).to.equal('authorizer'); - expect(resource.Properties.ProviderARNs[0] - ).to.equal('arn:aws:cognito-idp:us-east-1:xxx:userpool/us-east-1_ZZZ'); + expect(resource.Properties.ProviderARNs[0]).to.equal( + 'arn:aws:cognito-idp:us-east-1:xxx:userpool/us-east-1_ZZZ' + ); expect(resource.Properties.Type).to.equal('COGNITO_USER_POOLS'); }); diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/cors.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/cors.js index e6286f8f2..7f90c3134 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/cors.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/cors.js @@ -8,11 +8,10 @@ module.exports = { _.forEach(this.validated.corsPreflight, (config, path) => { const resourceName = this.getResourceName(path); const resourceRef = this.getResourceId(path); - const corsMethodLogicalId = this.provider.naming - .getMethodLogicalId(resourceName, 'options'); + const corsMethodLogicalId = this.provider.naming.getMethodLogicalId(resourceName, 'options'); let origin = config.origin; - let origins = (config.origins && Array.isArray(config.origins)) ? config.origins : undefined; + let origins = config.origins && Array.isArray(config.origins) ? config.origins : undefined; if (origin && origin.indexOf(',') !== -1) { origins = origin.split(',').map(a => a.trim()); @@ -50,9 +49,9 @@ module.exports = { } if (_.includes(config.methods, 'ANY')) { - preflightHeaders['Access-Control-Allow-Methods'] = - preflightHeaders['Access-Control-Allow-Methods'] - .replace('ANY', 'DELETE,GET,HEAD,PATCH,POST,PUT'); + preflightHeaders['Access-Control-Allow-Methods'] = preflightHeaders[ + 'Access-Control-Allow-Methods' + ].replace('ANY', 'DELETE,GET,HEAD,PATCH,POST,PUT'); } this.apiGatewayMethodLogicalIds.push(corsMethodLogicalId); @@ -71,8 +70,10 @@ module.exports = { 'application/json': '{statusCode:200}', }, ContentHandling: 'CONVERT_TO_TEXT', - IntegrationResponses: - this.generateCorsIntegrationResponses(preflightHeaders, origins), + IntegrationResponses: this.generateCorsIntegrationResponses( + preflightHeaders, + origins + ), }, ResourceId: resourceRef, RestApiId: this.provider.getApiGatewayRestApiId(), @@ -101,8 +102,10 @@ module.exports = { }, generateCorsIntegrationResponses(preflightHeaders, origins) { - const responseParameters = _.mapKeys(preflightHeaders, - (value, header) => `method.response.header.${header}`); + const responseParameters = _.mapKeys( + preflightHeaders, + (value, header) => `method.response.header.${header}` + ); return [ { @@ -110,16 +113,16 @@ module.exports = { ResponseParameters: responseParameters, ResponseTemplates: { 'application/json': - Array.isArray(origins) && origins.length ? - this.generateCorsResponseTemplate(origins) : '', + Array.isArray(origins) && origins.length + ? this.generateCorsResponseTemplate(origins) + : '', }, }, ]; }, regexifyWildcards(orig) { - return orig.map((str) => str.replace(/\./g, '[.]') - .replace('*', '.*')); + return orig.map(str => str.replace(/\./g, '[.]').replace('*', '.*')); }, generateCorsResponseTemplate(origins) { diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/cors.test.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/cors.test.js index f7a7401ac..4415a817e 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/cors.test.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/cors.test.js @@ -97,161 +97,143 @@ describe('#compileCors()', () => { return awsCompileApigEvents.compileCors().then(() => { // users/create expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreateOptions - .Properties.Integration.IntegrationResponses[0] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreateOptions.Properties.Integration.IntegrationResponses[0] .ResponseParameters['method.response.header.Access-Control-Allow-Origin'] - ).to.equal('\'http://localhost:3000\''); + ).to.equal("'http://localhost:3000'"); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreateOptions - .Properties.Integration.IntegrationResponses[0] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreateOptions.Properties.Integration.IntegrationResponses[0] .ResponseTemplates['application/json'] - ).to.equal('#set($origin = $input.params("Origin"))\n#if($origin == "") #set($origin = $input.params("origin")) #end\n#if($origin.matches("http://localhost:3000") || $origin.matches("https://.*[.]example[.]com")) #set($context.responseOverride.header.Access-Control-Allow-Origin = $origin) #end'); + ).to.equal( + '#set($origin = $input.params("Origin"))\n#if($origin == "") #set($origin = $input.params("origin")) #end\n#if($origin.matches("http://localhost:3000") || $origin.matches("https://.*[.]example[.]com")) #set($context.responseOverride.header.Access-Control-Allow-Origin = $origin) #end' + ); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreateOptions - .Properties.Integration.IntegrationResponses[0] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreateOptions.Properties.Integration.IntegrationResponses[0] .ResponseParameters['method.response.header.Access-Control-Allow-Headers'] - ).to.equal('\'*\''); + ).to.equal("'*'"); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreateOptions - .Properties.Integration.IntegrationResponses[0] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreateOptions.Properties.Integration.IntegrationResponses[0] .ResponseParameters['method.response.header.Access-Control-Allow-Methods'] - ).to.equal('\'OPTIONS,POST\''); + ).to.equal("'OPTIONS,POST'"); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreateOptions - .Properties.Integration.IntegrationResponses[0] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreateOptions.Properties.Integration.IntegrationResponses[0] .ResponseParameters['method.response.header.Access-Control-Allow-Credentials'] - ).to.equal('\'true\''); + ).to.equal("'true'"); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreateOptions - .Properties.Integration.IntegrationResponses[0] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreateOptions.Properties.Integration.IntegrationResponses[0] .ResponseParameters['method.response.header.Access-Control-Max-Age'] - ).to.equal('\'86400\''); + ).to.equal("'86400'"); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreateOptions - .Properties.Integration.IntegrationResponses[0] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreateOptions.Properties.Integration.IntegrationResponses[0] .ResponseParameters['method.response.header.Cache-Control'] - ).to.equal('\'max-age=600, s-maxage=600\''); + ).to.equal("'max-age=600, s-maxage=600'"); // users/update expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersUpdateOptions - .Properties.Integration.IntegrationResponses[0] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersUpdateOptions.Properties.Integration.IntegrationResponses[0] .ResponseParameters['method.response.header.Access-Control-Allow-Origin'] - ).to.equal('\'http://example.com\''); + ).to.equal("'http://example.com'"); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersUpdateOptions - .Properties.Integration.IntegrationResponses[0] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersUpdateOptions.Properties.Integration.IntegrationResponses[0] .ResponseParameters['method.response.header.Access-Control-Allow-Methods'] - ).to.equal('\'OPTIONS,PUT\''); + ).to.equal("'OPTIONS,PUT'"); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersUpdateOptions - .Properties.Integration.IntegrationResponses[0] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersUpdateOptions.Properties.Integration.IntegrationResponses[0] .ResponseParameters['method.response.header.Access-Control-Allow-Credentials'] - ).to.equal('\'false\''); + ).to.equal("'false'"); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersUpdateOptions - .Properties.Integration.IntegrationResponses[0] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersUpdateOptions.Properties.Integration.IntegrationResponses[0] .ResponseParameters['method.response.header.Access-Control-Max-Age'] - ).to.equal('\'86400\''); + ).to.equal("'86400'"); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersUpdateOptions - .Properties.Integration.IntegrationResponses[0] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersUpdateOptions.Properties.Integration.IntegrationResponses[0] .ResponseParameters['method.response.header.Cache-Control'] - ).to.equal('\'max-age=600, s-maxage=600\''); + ).to.equal("'max-age=600, s-maxage=600'"); // users/delete expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersDeleteOptions - .Properties.Integration.IntegrationResponses[0] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersDeleteOptions.Properties.Integration.IntegrationResponses[0] .ResponseParameters['method.response.header.Access-Control-Allow-Origin'] - ).to.equal('\'*\''); + ).to.equal("'*'"); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersDeleteOptions - .Properties.Integration.IntegrationResponses[0] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersDeleteOptions.Properties.Integration.IntegrationResponses[0] .ResponseParameters['method.response.header.Access-Control-Allow-Headers'] - ).to.equal('\'CustomHeaderA,CustomHeaderB\''); + ).to.equal("'CustomHeaderA,CustomHeaderB'"); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersDeleteOptions - .Properties.Integration.IntegrationResponses[0] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersDeleteOptions.Properties.Integration.IntegrationResponses[0] .ResponseParameters['method.response.header.Access-Control-Allow-Methods'] - ).to.equal('\'OPTIONS,DELETE\''); + ).to.equal("'OPTIONS,DELETE'"); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersDeleteOptions - .Properties.Integration.IntegrationResponses[0] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersDeleteOptions.Properties.Integration.IntegrationResponses[0] .ResponseParameters['method.response.header.Access-Control-Allow-Credentials'] - ).to.equal('\'false\''); + ).to.equal("'false'"); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersDeleteOptions - .Properties.Integration.IntegrationResponses[0] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersDeleteOptions.Properties.Integration.IntegrationResponses[0] .ResponseParameters['method.response.header.Access-Control-Max-Age'] - ).to.equal('\'86400\''); + ).to.equal("'86400'"); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersDeleteOptions - .Properties.Integration.IntegrationResponses[0] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersDeleteOptions.Properties.Integration.IntegrationResponses[0] .ResponseParameters['method.response.header.Cache-Control'] - ).to.equal('\'max-age=600, s-maxage=600\''); + ).to.equal("'max-age=600, s-maxage=600'"); // users/any expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersAnyOptions - .Properties.Integration.IntegrationResponses[0] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersAnyOptions.Properties.Integration.IntegrationResponses[0] .ResponseTemplates['application/json'] - ).to.equal('#set($origin = $input.params("Origin"))\n#if($origin == "") #set($origin = $input.params("origin")) #end\n#if($origin.matches("http://localhost:3000") || $origin.matches("http://example[.]com")) #set($context.responseOverride.header.Access-Control-Allow-Origin = $origin) #end'); + ).to.equal( + '#set($origin = $input.params("Origin"))\n#if($origin == "") #set($origin = $input.params("origin")) #end\n#if($origin.matches("http://localhost:3000") || $origin.matches("http://example[.]com")) #set($context.responseOverride.header.Access-Control-Allow-Origin = $origin) #end' + ); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersAnyOptions - .Properties.Integration.IntegrationResponses[0] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersAnyOptions.Properties.Integration.IntegrationResponses[0] .ResponseParameters['method.response.header.Access-Control-Allow-Headers'] - ).to.equal('\'*\''); + ).to.equal("'*'"); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersAnyOptions - .Properties.Integration.IntegrationResponses[0] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersAnyOptions.Properties.Integration.IntegrationResponses[0] .ResponseParameters['method.response.header.Access-Control-Allow-Methods'] - ).to.equal('\'OPTIONS,DELETE,GET,HEAD,PATCH,POST,PUT\''); + ).to.equal("'OPTIONS,DELETE,GET,HEAD,PATCH,POST,PUT'"); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersAnyOptions - .Properties.Integration.IntegrationResponses[0] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersAnyOptions.Properties.Integration.IntegrationResponses[0] .ResponseParameters['method.response.header.Access-Control-Allow-Credentials'] - ).to.equal('\'false\''); + ).to.equal("'false'"); }); }); @@ -264,8 +246,10 @@ describe('#compileCors()', () => { }, }; - expect(() => awsCompileApigEvents.compileCors()) - .to.throw(Error, 'must specify either origin or origins'); + expect(() => awsCompileApigEvents.compileCors()).to.throw( + Error, + 'must specify either origin or origins' + ); }); it('should throw error if maxAge is not an integer greater than 0', () => { @@ -279,8 +263,10 @@ describe('#compileCors()', () => { }, }; - expect(() => awsCompileApigEvents.compileCors()) - .to.throw(Error, 'maxAge should be an integer over 0'); + expect(() => awsCompileApigEvents.compileCors()).to.throw( + Error, + 'maxAge should be an integer over 0' + ); }); it('should throw error if maxAge is not an integer', () => { @@ -294,8 +280,10 @@ describe('#compileCors()', () => { }, }; - expect(() => awsCompileApigEvents.compileCors()) - .to.throw(Error, 'maxAge should be an integer over 0'); + expect(() => awsCompileApigEvents.compileCors()).to.throw( + Error, + 'maxAge should be an integer over 0' + ); }); it('should add the methods resource logical id to the array of method logical ids', () => { @@ -315,11 +303,10 @@ describe('#compileCors()', () => { }, }; return awsCompileApigEvents.compileCors().then(() => { - expect(awsCompileApigEvents.apiGatewayMethodLogicalIds) - .to.deep.equal([ - 'ApiGatewayMethodUsersCreateOptions', - 'ApiGatewayMethodUsersAnyOptions', - ]); + expect(awsCompileApigEvents.apiGatewayMethodLogicalIds).to.deep.equal([ + 'ApiGatewayMethodUsersCreateOptions', + 'ApiGatewayMethodUsersAnyOptions', + ]); }); }); }); diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/deployment.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/deployment.js index d09c789f3..c136d6972 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/deployment.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/deployment.js @@ -5,8 +5,9 @@ const BbPromise = require('bluebird'); module.exports = { compileDeployment() { - this.apiGatewayDeploymentLogicalId = this.provider.naming - .generateApiGatewayDeploymentLogicalId(this.serverless.instanceId); + this.apiGatewayDeploymentLogicalId = this.provider.naming.generateApiGatewayDeploymentLogicalId( + this.serverless.instanceId + ); _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, { [this.apiGatewayDeploymentLogicalId]: { @@ -25,7 +26,8 @@ module.exports = { ServiceEndpoint: { Description: 'URL of the service endpoint', Value: { - 'Fn::Join': ['', + 'Fn::Join': [ + '', [ 'https://', this.provider.getApiGatewayRestApiId(), diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/deployment.test.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/deployment.test.js index 4231ae3b0..abe8f4ef9 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/deployment.test.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/deployment.test.js @@ -28,15 +28,16 @@ describe('#compileDeployment()', () => { awsCompileApigEvents.provider = provider; }); - it('should create a deployment resource', () => awsCompileApigEvents - .compileDeployment().then(() => { - const apiGatewayDeploymentLogicalId = Object - .keys(awsCompileApigEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources)[0]; + it('should create a deployment resource', () => + awsCompileApigEvents.compileDeployment().then(() => { + const apiGatewayDeploymentLogicalId = Object.keys( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + )[0]; expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources[apiGatewayDeploymentLogicalId] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + apiGatewayDeploymentLogicalId + ] ).to.deep.equal({ Type: 'AWS::ApiGateway::Deployment', DependsOn: ['method-dependency1', 'method-dependency2'], @@ -48,43 +49,41 @@ describe('#compileDeployment()', () => { StageName: 'dev', }, }); - }) - ); + })); it('should create a deployment resource with description', () => { awsCompileApigEvents.serverless.service.provider.apiGateway = { description: 'Some Description', }; - return awsCompileApigEvents - .compileDeployment().then(() => { - const apiGatewayDeploymentLogicalId = Object - .keys(awsCompileApigEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources)[0]; + return awsCompileApigEvents.compileDeployment().then(() => { + const apiGatewayDeploymentLogicalId = Object.keys( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + )[0]; - expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources[apiGatewayDeploymentLogicalId] - ).to.deep.equal({ - Type: 'AWS::ApiGateway::Deployment', - DependsOn: ['method-dependency1', 'method-dependency2'], - Properties: { - RestApiId: { - Ref: awsCompileApigEvents.apiGatewayRestApiLogicalId, - }, - Description: 'Some Description', - StageName: 'dev', - }, - }); + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + apiGatewayDeploymentLogicalId + ] + ).to.deep.equal({ + Type: 'AWS::ApiGateway::Deployment', + DependsOn: ['method-dependency1', 'method-dependency2'], + Properties: { + RestApiId: { + Ref: awsCompileApigEvents.apiGatewayRestApiLogicalId, + }, + Description: 'Some Description', + StageName: 'dev', + }, }); - } - ); + }); + }); it('should add service endpoint output', () => awsCompileApigEvents.compileDeployment().then(() => { expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Outputs.ServiceEndpoint + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Outputs + .ServiceEndpoint ).to.deep.equal({ Description: 'URL of the service endpoint', Value: { @@ -102,6 +101,5 @@ describe('#compileDeployment()', () => { ], }, }); - }) - ); + })); }); diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/hack/disassociateUsagePlan.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/hack/disassociateUsagePlan.js index 3930e1528..a00c52fca 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/hack/disassociateUsagePlan.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/hack/disassociateUsagePlan.js @@ -9,30 +9,42 @@ module.exports = { if (apiKeys && apiKeys.length) { this.serverless.cli.log('Removing usage plan association...'); - const stackName = `${this.serverless.service.service}-${this.provider.getStage()}`; + const stackName = `${this.provider.naming.getStackName()}`; return BbPromise.all([ - this.provider.request('CloudFormation', - 'describeStackResource', - { - StackName: stackName, - LogicalResourceId: this.provider.naming.getRestApiLogicalId(), - }), - this.provider.request('APIGateway', - 'getUsagePlans', {}), - ]).then((data) => data[1].items.filter((item) => - _.includes(item.apiStages - .map(apistage => apistage.apiId), data[0].StackResourceDetail.PhysicalResourceId))) - .then((items) => BbPromise.all(_.flattenDeep(items.map(item => - item.apiStages.map(apiStage => - this.provider.request('APIGateway', - 'updateUsagePlan', { - usagePlanId: item.id, - patchOperations: [{ - op: 'remove', - path: '/apiStages', - value: `${apiStage.apiId}:${apiStage.stage}`, - }], - })))))); + this.provider.request('CloudFormation', 'describeStackResource', { + StackName: stackName, + LogicalResourceId: this.provider.naming.getRestApiLogicalId(), + }), + this.provider.request('APIGateway', 'getUsagePlans', {}), + ]) + .then(data => + data[1].items.filter(item => + _.includes( + item.apiStages.map(apistage => apistage.apiId), + data[0].StackResourceDetail.PhysicalResourceId + ) + ) + ) + .then(items => + BbPromise.all( + _.flattenDeep( + items.map(item => + item.apiStages.map(apiStage => + this.provider.request('APIGateway', 'updateUsagePlan', { + usagePlanId: item.id, + patchOperations: [ + { + op: 'remove', + path: '/apiStages', + value: `${apiStage.apiId}:${apiStage.stage}`, + }, + ], + }) + ) + ) + ) + ) + ); } return BbPromise.resolve(); diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/hack/disassociateUsagePlan.test.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/hack/disassociateUsagePlan.test.js index c0dfbd7fc..9943a929c 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/hack/disassociateUsagePlan.test.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/hack/disassociateUsagePlan.test.js @@ -30,27 +30,30 @@ describe('#disassociateUsagePlan()', () => { disassociateUsagePlan.options = options; disassociateUsagePlan.provider = awsProvider; - providerRequestStub.withArgs('CloudFormation', 'describeStackResource') + providerRequestStub + .withArgs('CloudFormation', 'describeStackResource') .resolves({ StackResourceDetail: { PhysicalResourceId: 'resource-id' } }); providerRequestStub.withArgs('APIGateway', 'getUsagePlans').resolves({ - items: [{ - apiStages: [ - { - apiId: 'resource-id', - stage: 'dev', - }, - ], - id: 'usage-plan-id', - }, - { - apiStages: [ - { - apiId: 'another-resource-id', - stage: 'dev', - }, - ], - id: 'another-usage-plan-id', - }], + items: [ + { + apiStages: [ + { + apiId: 'resource-id', + stage: 'dev', + }, + ], + id: 'usage-plan-id', + }, + { + apiStages: [ + { + apiId: 'another-resource-id', + stage: 'dev', + }, + ], + id: 'another-usage-plan-id', + }, + ], }); providerRequestStub.withArgs('APIGateway', 'updateUsagePlan').resolves(); }); @@ -65,33 +68,29 @@ describe('#disassociateUsagePlan()', () => { return disassociateUsagePlan.disassociateUsagePlan().then(() => { expect(providerRequestStub.callCount).to.be.equal(3); - expect(providerRequestStub.calledWithExactly( - 'CloudFormation', - 'describeStackResource', - { - StackName: `${serverless.service.service}-${awsProvider.getStage()}`, + expect( + providerRequestStub.calledWithExactly('CloudFormation', 'describeStackResource', { + StackName: `${awsProvider.naming.getStackName()}`, LogicalResourceId: 'ApiGatewayRestApi', - } - )).to.be.equal(true); + }) + ).to.be.equal(true); - expect(providerRequestStub.calledWithExactly( - 'APIGateway', - 'getUsagePlans', - {} - )).to.be.equal(true); + expect(providerRequestStub.calledWithExactly('APIGateway', 'getUsagePlans', {})).to.be.equal( + true + ); - expect(providerRequestStub.calledWithExactly( - 'APIGateway', - 'updateUsagePlan', - { + expect( + providerRequestStub.calledWithExactly('APIGateway', 'updateUsagePlan', { usagePlanId: 'usage-plan-id', - patchOperations: [{ - op: 'remove', - path: '/apiStages', - value: 'resource-id:dev', - }], - } - )).to.be.equal(true); + patchOperations: [ + { + op: 'remove', + path: '/apiStages', + value: 'resource-id:dev', + }, + ], + }) + ).to.be.equal(true); }); }); diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/hack/updateStage.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/hack/updateStage.js index 320b9392c..8f5d2834a 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/hack/updateStage.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/hack/updateStage.js @@ -1,11 +1,21 @@ -/* eslint-disable no-use-before-define */ - 'use strict'; const _ = require('lodash'); const BbPromise = require('bluebird'); const isRestApiId = RegExp.prototype.test.bind(/^[a-z0-9]{3,}$/); +const defaultApiGatewayLogFormat = [ + 'requestId: $context.requestId', + 'ip: $context.identity.sourceIp', + 'caller: $context.identity.caller', + 'user: $context.identity.user', + 'requestTime: $context.requestTime', + 'httpMethod: $context.httpMethod', + 'resourcePath: $context.resourcePath', + 'status: $context.status', + 'protocol: $context.protocol', + 'responseLength: $context.responseLength', +].join(', '); // NOTE --> Keep this file in sync with ../stage.js @@ -17,11 +27,14 @@ module.exports = { // this array is used to gather all the patch operations we need to // perform on the stage this.apiGatewayStagePatchOperations = []; + this.apiGatewayTagResourceParams = []; + this.apiGatewayUntagResourceParams = []; this.apiGatewayStageState = {}; this.apiGatewayDeploymentId = null; this.apiGatewayRestApiId = null; - return resolveAccountId.call(this) + return resolveAccountId + .call(this) .then(resolveRestApiId.bind(this)) .then(() => { if (!this.apiGatewayRestApiId) { @@ -38,13 +51,12 @@ module.exports = { } const errorMessage = [ - 'Rest API could not be resolved. ', - 'This might be casued by a custom API Gateway setup. ', - 'With you current setup stage specific configurations such as ', - '`tracing`, `logs` and `tags` are not supported', - '', - 'Please update your configuration or open up an issue if you feel ', - 'that there\'s a way to support your setup.', + 'Rest API id could not be resolved.\n', + 'This might be caused by a custom API Gateway configuration.\n\n', + 'In given setup stage specific options such as ', + '`tracing`, `logs` and `tags` are not supported.\n\n', + 'Please update your configuration (or open up an issue if you feel ', + "that there's a way to support your setup).", ].join(''); throw new Error(errorMessage); @@ -57,6 +69,8 @@ module.exports = { .then(handleLogs.bind(this)) .then(handleTags.bind(this)) .then(applyUpdates.bind(this)) + .then(addTags.bind(this)) + .then(removeTags.bind(this)) .then(removeLogGroup.bind(this)); }); }, @@ -64,7 +78,7 @@ module.exports = { function resolveAccountId() { // eslint-disable-next-line no-return-assign - return this.provider.getAccountId().then((id) => this.accountId = id); + return this.provider.getAccountId().then(id => (this.accountId = id)); } function resolveRestApiId() { @@ -92,34 +106,39 @@ function resolveRestApiId() { function resolveStage() { const restApiId = this.apiGatewayRestApiId; - return this.provider.request('APIGateway', 'getStage', { - restApiId, - stageName: this.options.stage, - }).then((res) => { - this.apiGatewayStageState = res; - }).catch(() => { - // fail silently... - }); + return this.provider + .request('APIGateway', 'getStage', { + restApiId, + stageName: this.options.stage, + }) + .then(res => { + this.apiGatewayStageState = res; + }) + .catch(() => { + // fail silently... + }); } function resolveDeploymentId() { if (_.isEmpty(this.apiGatewayStageState)) { const restApiId = this.apiGatewayRestApiId; - return this.provider.request('APIGateway', 'getDeployments', { - restApiId, - limit: 500, - }).then(res => { - if (res.items.length) { - // there will ever only be 1 deployment associated - const deployment = res.items.shift(); - return deployment.id; - } - return null; - }) - .then((deploymentId) => { - this.apiGatewayDeploymentId = deploymentId; - }); + return this.provider + .request('APIGateway', 'getDeployments', { + restApiId, + limit: 500, + }) + .then(res => { + if (res.items.length) { + // there will ever only be 1 deployment associated + const deployment = res.items.shift(); + return deployment.id; + } + return null; + }) + .then(deploymentId => { + this.apiGatewayDeploymentId = deploymentId; + }); } return BbPromise.resolve(); @@ -167,6 +186,11 @@ function handleLogs() { const region = this.options.region; const logGroupName = `/aws/api-gateway/${service}-${stage}`; + let logFormat = defaultApiGatewayLogFormat; + if (logs.format) { + logFormat = logs.format; + } + const destinationArn = { op: 'replace', path: '/accessLogSettings/destinationArn', @@ -175,18 +199,7 @@ function handleLogs() { const format = { op: 'replace', path: '/accessLogSettings/format', - value: [ - 'requestId: $context.requestId', - 'ip: $context.identity.sourceIp', - 'caller: $context.identity.caller', - 'user: $context.identity.user', - 'requestTime: $context.requestTime', - 'httpMethod: $context.httpMethod', - 'resourcePath: $context.resourcePath', - 'status: $context.status', - 'protocol: $context.protocol', - 'responseLength: $context.responseLength', - ].join(', '), + value: logFormat, }; dataTrace = { op: 'replace', path: '/*/*/logging/dataTrace', value: 'true' }; logLevel = { op: 'replace', path: '/*/*/logging/loglevel', value: 'INFO' }; @@ -198,32 +211,47 @@ function handleLogs() { function handleTags() { const provider = this.state.service.provider; - const tagsMerged = Object.assign({}, provider.stackTags, provider.tags); + const tagsMerged = _.mapValues(Object.assign({}, provider.stackTags, provider.tags), v => + String(v) + ); + const currentTags = this.apiGatewayStageState.tags || {}; + const tagKeysToBeRemoved = Object.keys(currentTags).filter( + currentKey => !_.isString(tagsMerged[currentKey]) + ); - const tagsToCreate = _.entriesIn(tagsMerged).map(pair => ({ - key: String(pair[0]), - value: String(pair[1]), - })); - if (tagsToCreate) { - tagsToCreate.forEach((tag) => { - const operation = { op: 'replace', path: `/variables/${tag.key}`, value: tag.value }; - this.apiGatewayStagePatchOperations.push(operation); + const restApiId = this.apiGatewayRestApiId; + const stageName = this.options.stage; + const region = this.options.region; + const resourceArn = `arn:aws:apigateway:${region}::/restapis/${restApiId}/stages/${stageName}`; + + if (tagKeysToBeRemoved.length > 0) { + this.apiGatewayUntagResourceParams.push({ + resourceArn, + tagKeys: tagKeysToBeRemoved, }); } - - if (this.apiGatewayStageState.variables) { - const stateTagKeys = Object.keys(this.apiGatewayStageState.variables); - const newTagKeys = Object.keys(tagsMerged); - const tagsToRemove = _.difference(stateTagKeys, newTagKeys); - if (tagsToRemove) { - tagsToRemove.forEach((tagKey) => { - const operation = { op: 'remove', path: `/variables/${tagKey}` }; - this.apiGatewayStagePatchOperations.push(operation); - }); - } + if (!_.isEqual(currentTags, tagsMerged) && _.size(tagsMerged) > 0) { + this.apiGatewayTagResourceParams.push({ + resourceArn, + tags: tagsMerged, + }); } } +function addTags() { + const requests = this.apiGatewayTagResourceParams.map(tagResourceParam => + this.provider.request('APIGateway', 'tagResource', tagResourceParam) + ); + return BbPromise.all(requests); +} + +function removeTags() { + const requests = this.apiGatewayUntagResourceParams.map(untagResourceParam => + this.provider.request('APIGateway', 'untagResource', untagResourceParam) + ); + return BbPromise.all(requests); +} + function applyUpdates() { const restApiId = this.apiGatewayRestApiId; const patchOperations = this.apiGatewayStagePatchOperations; @@ -251,11 +279,13 @@ function removeLogGroup() { // ensure that the log group is removed. Otherwise we'll run into duplicate // log group name issues when logs are enabled again if (!logs) { - return this.provider.request('CloudWatchLogs', 'deleteLogGroup', { - logGroupName, - }).catch(() => { - // fail silently... - }); + return this.provider + .request('CloudWatchLogs', 'deleteLogGroup', { + logGroupName, + }) + .catch(() => { + // fail silently... + }); } return BbPromise.resolve(); diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/hack/updateStage.test.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/hack/updateStage.test.js index 3d9256256..1c6e79297 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/hack/updateStage.test.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/hack/updateStage.test.js @@ -3,13 +3,17 @@ /* eslint-disable max-len */ /* eslint-disable no-unused-expressions */ -const expect = require('chai').expect; +const chai = require('chai'); const sinon = require('sinon'); const _ = require('lodash'); const Serverless = require('../../../../../../../../Serverless'); const AwsProvider = require('../../../../../../provider/awsProvider'); const updateStage = require('./updateStage').updateStage; +chai.use(require('sinon-chai')); + +const { expect } = chai; + describe('#updateStage()', () => { let serverless; let options; @@ -24,8 +28,7 @@ describe('#updateStage()', () => { options = { stage: 'dev', region: 'us-east-1' }; awsProvider = new AwsProvider(serverless, options); serverless.setProvider('aws', awsProvider); - providerGetAccountIdStub = sinon.stub(awsProvider, 'getAccountId') - .resolves(123456); + providerGetAccountIdStub = sinon.stub(awsProvider, 'getAccountId').resolves(123456); providerRequestStub = sinon.stub(awsProvider, 'request'); context = { @@ -49,14 +52,16 @@ describe('#updateStage()', () => { stageName: 'dev', }) .resolves({ - variables: { + tags: { old: 'tag', }, }); - providerRequestStub.withArgs('CloudWatchLogs', 'deleteLogGroup', { - logGroupName: '/aws/api-gateway/my-service-dev', - }).resolves(); + providerRequestStub + .withArgs('CloudWatchLogs', 'deleteLogGroup', { + logGroupName: '/aws/api-gateway/my-service-dev', + }) + .resolves(); }); afterEach(() => { @@ -66,11 +71,11 @@ describe('#updateStage()', () => { it('should update the stage based on the serverless file configuration', () => { context.state.service.provider.tags = { - foo: 'bar', - bar: 'baz', + 'Containing Space': 'bar', + 'bar': 'high-priority', }; context.state.service.provider.stackTags = { - baz: 'qux', + bar: 'low-priority', num: 123, }; context.state.service.provider.tracing = { @@ -83,19 +88,23 @@ describe('#updateStage()', () => { return updateStage.call(context).then(() => { const patchOperations = [ { op: 'replace', path: '/tracingEnabled', value: 'true' }, - { op: 'replace', path: '/accessLogSettings/destinationArn', value: 'arn:aws:logs:us-east-1:123456:log-group:/aws/api-gateway/my-service-dev' }, - { op: 'replace', path: '/accessLogSettings/format', value: 'requestId: $context.requestId, ip: $context.identity.sourceIp, caller: $context.identity.caller, user: $context.identity.user, requestTime: $context.requestTime, httpMethod: $context.httpMethod, resourcePath: $context.resourcePath, status: $context.status, protocol: $context.protocol, responseLength: $context.responseLength' }, + { + op: 'replace', + path: '/accessLogSettings/destinationArn', + value: 'arn:aws:logs:us-east-1:123456:log-group:/aws/api-gateway/my-service-dev', + }, + { + op: 'replace', + path: '/accessLogSettings/format', + value: + 'requestId: $context.requestId, ip: $context.identity.sourceIp, caller: $context.identity.caller, user: $context.identity.user, requestTime: $context.requestTime, httpMethod: $context.httpMethod, resourcePath: $context.resourcePath, status: $context.status, protocol: $context.protocol, responseLength: $context.responseLength', + }, { op: 'replace', path: '/*/*/logging/dataTrace', value: 'true' }, { op: 'replace', path: '/*/*/logging/loglevel', value: 'INFO' }, - { op: 'replace', path: '/variables/baz', value: 'qux' }, - { op: 'replace', path: '/variables/num', value: '123' }, - { op: 'replace', path: '/variables/foo', value: 'bar' }, - { op: 'replace', path: '/variables/bar', value: 'baz' }, - { op: 'remove', path: '/variables/old' }, ]; expect(providerGetAccountIdStub).to.be.calledOnce; - expect(providerRequestStub.args).to.have.length(3); + expect(providerRequestStub.args).to.have.length(5); expect(providerRequestStub.args[0][0]).to.equal('APIGateway'); expect(providerRequestStub.args[0][1]).to.equal('getRestApis'); expect(providerRequestStub.args[0][2]).to.deep.equal({ @@ -104,7 +113,10 @@ describe('#updateStage()', () => { }); expect(providerRequestStub.args[1][0]).to.equal('APIGateway'); expect(providerRequestStub.args[1][1]).to.equal('getStage'); - expect(providerRequestStub.args[1][2]).to.deep.equal({ restApiId: 'someRestApiId', stageName: 'dev' }); + expect(providerRequestStub.args[1][2]).to.deep.equal({ + restApiId: 'someRestApiId', + stageName: 'dev', + }); expect(providerRequestStub.args[2][0]).to.equal('APIGateway'); expect(providerRequestStub.args[2][1]).to.equal('updateStage'); expect(providerRequestStub.args[2][2]).to.deep.equal({ @@ -112,16 +124,34 @@ describe('#updateStage()', () => { stageName: 'dev', patchOperations, }); + expect(providerRequestStub.args[3][0]).to.equal('APIGateway'); + expect(providerRequestStub.args[3][1]).to.equal('tagResource'); + expect(providerRequestStub.args[3][2]).to.deep.equal({ + resourceArn: 'arn:aws:apigateway:us-east-1::/restapis/someRestApiId/stages/dev', + tags: { + 'Containing Space': 'bar', + 'bar': 'high-priority', + 'num': '123', + }, + }); + expect(providerRequestStub.args[4][0]).to.equal('APIGateway'); + expect(providerRequestStub.args[4][1]).to.equal('untagResource'); + expect(providerRequestStub.args[4][2]).to.deep.equal({ + resourceArn: 'arn:aws:apigateway:us-east-1::/restapis/someRestApiId/stages/dev', + tagKeys: ['old'], + }); }); }); - it('should perform default actions if settings are not configure', () => - updateStage.call(context).then(() => { + it('should perform default actions if settings are not configure', () => { + context.state.service.provider.tags = { + old: 'tag', + }; + return updateStage.call(context).then(() => { const patchOperations = [ { op: 'replace', path: '/tracingEnabled', value: 'false' }, { op: 'replace', path: '/*/*/logging/dataTrace', value: 'false' }, { op: 'replace', path: '/*/*/logging/loglevel', value: 'OFF' }, - { op: 'remove', path: '/variables/old' }, ]; expect(providerGetAccountIdStub).to.be.calledOnce; @@ -134,7 +164,10 @@ describe('#updateStage()', () => { }); expect(providerRequestStub.args[1][0]).to.equal('APIGateway'); expect(providerRequestStub.args[1][1]).to.equal('getStage'); - expect(providerRequestStub.args[1][2]).to.deep.equal({ restApiId: 'someRestApiId', stageName: 'dev' }); + expect(providerRequestStub.args[1][2]).to.deep.equal({ + restApiId: 'someRestApiId', + stageName: 'dev', + }); expect(providerRequestStub.args[2][0]).to.equal('APIGateway'); expect(providerRequestStub.args[2][1]).to.equal('updateStage'); expect(providerRequestStub.args[2][2]).to.deep.equal({ @@ -147,25 +180,33 @@ describe('#updateStage()', () => { expect(providerRequestStub.args[3][2]).to.deep.equal({ logGroupName: '/aws/api-gateway/my-service-dev', }); - })); + }); + }); it('should create a new stage and proceed as usual if none can be found', () => { - providerRequestStub.withArgs('APIGateway', 'getStage', { - restApiId: 'someRestApiId', stageName: 'dev', - }).rejects(); + providerRequestStub + .withArgs('APIGateway', 'getStage', { + restApiId: 'someRestApiId', + stageName: 'dev', + }) + .rejects(); - providerRequestStub.withArgs('APIGateway', 'getDeployments', { - restApiId: 'someRestApiId', - limit: 500, - }).resolves({ - items: [{ id: 'someDeploymentId' }], - }); + providerRequestStub + .withArgs('APIGateway', 'getDeployments', { + restApiId: 'someRestApiId', + limit: 500, + }) + .resolves({ + items: [{ id: 'someDeploymentId' }], + }); - providerRequestStub.withArgs('APIGateway', 'createStage', { - deploymentId: 'someDeploymentId', - restApiId: 'someRestApiId', - stageName: 'dev', - }).resolves(); + providerRequestStub + .withArgs('APIGateway', 'createStage', { + deploymentId: 'someDeploymentId', + restApiId: 'someRestApiId', + stageName: 'dev', + }) + .resolves(); return updateStage.call(context).then(() => { const patchOperations = [ @@ -184,10 +225,16 @@ describe('#updateStage()', () => { }); expect(providerRequestStub.args[1][0]).to.equal('APIGateway'); expect(providerRequestStub.args[1][1]).to.equal('getStage'); - expect(providerRequestStub.args[1][2]).to.deep.equal({ restApiId: 'someRestApiId', stageName: 'dev' }); + expect(providerRequestStub.args[1][2]).to.deep.equal({ + restApiId: 'someRestApiId', + stageName: 'dev', + }); expect(providerRequestStub.args[2][0]).to.equal('APIGateway'); expect(providerRequestStub.args[2][1]).to.equal('getDeployments'); - expect(providerRequestStub.args[2][2]).to.deep.equal({ restApiId: 'someRestApiId', limit: 500 }); + expect(providerRequestStub.args[2][2]).to.deep.equal({ + restApiId: 'someRestApiId', + limit: 500, + }); expect(providerRequestStub.args[3][0]).to.equal('APIGateway'); expect(providerRequestStub.args[3][1]).to.equal('createStage'); expect(providerRequestStub.args[3][2]).to.deep.equal({ @@ -288,4 +335,58 @@ describe('#updateStage()', () => { }); } ); + + it('should update the stage with a custom APIGW log format if given', () => { + context.state.service.provider.logs = { + restApi: { + format: 'requestId: $context.requestId', + }, + }; + + return updateStage.call(context).then(() => { + const patchOperations = [ + { op: 'replace', path: '/tracingEnabled', value: 'false' }, + { + op: 'replace', + path: '/accessLogSettings/destinationArn', + value: 'arn:aws:logs:us-east-1:123456:log-group:/aws/api-gateway/my-service-dev', + }, + { + op: 'replace', + path: '/accessLogSettings/format', + value: 'requestId: $context.requestId', + }, + { op: 'replace', path: '/*/*/logging/dataTrace', value: 'true' }, + { op: 'replace', path: '/*/*/logging/loglevel', value: 'INFO' }, + ]; + + expect(providerGetAccountIdStub).to.be.calledOnce; + expect(providerRequestStub.args).to.have.length(4); + expect(providerRequestStub.args[0][0]).to.equal('APIGateway'); + expect(providerRequestStub.args[0][1]).to.equal('getRestApis'); + expect(providerRequestStub.args[0][2]).to.deep.equal({ + limit: 500, + position: undefined, + }); + expect(providerRequestStub.args[1][0]).to.equal('APIGateway'); + expect(providerRequestStub.args[1][1]).to.equal('getStage'); + expect(providerRequestStub.args[1][2]).to.deep.equal({ + restApiId: 'someRestApiId', + stageName: 'dev', + }); + expect(providerRequestStub.args[2][0]).to.equal('APIGateway'); + expect(providerRequestStub.args[2][1]).to.equal('updateStage'); + expect(providerRequestStub.args[2][2]).to.deep.equal({ + restApiId: 'someRestApiId', + stageName: 'dev', + patchOperations, + }); + expect(providerRequestStub.args[3][0]).to.equal('APIGateway'); + expect(providerRequestStub.args[3][1]).to.equal('untagResource'); + expect(providerRequestStub.args[3][2]).to.deep.equal({ + resourceArn: 'arn:aws:apigateway:us-east-1::/restapis/someRestApiId/stages/dev', + tagKeys: ['old'], + }); + }); + }); }); diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/method/authorization.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/method/authorization.js index 03957b8e8..ce62a8781 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/method/authorization.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/method/authorization.js @@ -15,22 +15,28 @@ module.exports = { if (http.authorizer) { if (http.authorizer.type && http.authorizer.authorizerId) { - return { + const authReturn = { Properties: { AuthorizationType: http.authorizer.type, AuthorizerId: http.authorizer.authorizerId, }, }; + if ( + http.authorizer.type === 'COGNITO_USER_POOLS' && + http.authorizer.scopes && + http.authorizer.scopes.length + ) { + authReturn.Properties.AuthorizationScopes = http.authorizer.scopes; + } + return authReturn; } - const authorizerLogicalId = this.provider.naming - .getAuthorizerLogicalId(http.authorizer.name); + const authorizerLogicalId = this.provider.naming.getAuthorizerLogicalId(http.authorizer.name); const authorizerArn = http.authorizer.arn; let authorizationType; - if (typeof authorizerArn === 'string' - && awsArnRegExs.cognitoIdpArnExpr.test(authorizerArn)) { + if (typeof authorizerArn === 'string' && awsArnRegExs.cognitoIdpArnExpr.test(authorizerArn)) { authorizationType = 'COGNITO_USER_POOLS'; const cognitoReturn = { Properties: { @@ -39,7 +45,7 @@ module.exports = { }, DependsOn: authorizerLogicalId, }; - if (http.authorizer.scopes) { + if (http.authorizer.scopes && http.authorizer.scopes.length) { cognitoReturn.Properties.AuthorizationScopes = http.authorizer.scopes; } return cognitoReturn; 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 0d12adb4d..e846393a2 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 @@ -7,7 +7,7 @@ module.exports = { compileMethods() { this.permissionMapping = []; - this.validated.events.forEach((event) => { + this.validated.events.forEach(event => { const resourceId = this.getResourceId(event.http.path); const resourceName = this.getResourceName(event.http.path); const requestParameters = (event.http.request && event.http.request.parameters) || {}; @@ -28,17 +28,21 @@ module.exports = { template.Properties.ApiKeyRequired = false; } - const methodLogicalId = this.provider.naming - .getMethodLogicalId(resourceName, event.http.method); - const validatorLogicalId = this.provider.naming - .getValidatorLogicalId(resourceName, event.http.method); - const lambdaLogicalId = this.provider.naming - .getLambdaLogicalId(event.functionName); + const methodLogicalId = this.provider.naming.getMethodLogicalId( + resourceName, + event.http.method + ); + const validatorLogicalId = this.provider.naming.getValidatorLogicalId( + resourceName, + event.http.method + ); + const lambdaLogicalId = this.provider.naming.getLambdaLogicalId(event.functionName); const singlePermissionMapping = { resourceName, lambdaLogicalId, event }; this.permissionMapping.push(singlePermissionMapping); - _.merge(template, + _.merge( + template, this.getMethodAuthorization(event.http), this.getMethodIntegration(event.http, lambdaLogicalId, methodLogicalId), this.getMethodResponses(event.http) @@ -47,7 +51,7 @@ module.exports = { let extraCognitoPoolClaims; if (event.http.authorizer) { const claims = event.http.authorizer.claims || []; - extraCognitoPoolClaims = _.map(claims, (claim) => { + extraCognitoPoolClaims = _.map(claims, claim => { if (typeof claim === 'string') { const colonIndex = claim.indexOf(':'); if (colonIndex !== -1) { @@ -74,8 +78,11 @@ module.exports = { const contentType = requestSchema[0]; const schema = requestSchema[1]; - const modelLogicalId = this.provider.naming - .getModelLogicalId(resourceName, event.http.method, contentType); + const modelLogicalId = this.provider.naming.getModelLogicalId( + resourceName, + event.http.method, + contentType + ); template.Properties.RequestValidatorId = { Ref: validatorLogicalId }; template.Properties.RequestModels = { [contentType]: { Ref: modelLogicalId } }; @@ -84,9 +91,7 @@ module.exports = { [modelLogicalId]: { Type: 'AWS::ApiGateway::Model', Properties: { - RestApiId: { - Ref: this.provider.naming.getRestApiLogicalId(), - }, + RestApiId: this.provider.getApiGatewayRestApiId(), ContentType: contentType, Schema: schema, }, @@ -94,9 +99,7 @@ module.exports = { [validatorLogicalId]: { Type: 'AWS::ApiGateway::RequestValidator', Properties: { - RestApiId: { - Ref: this.provider.naming.getRestApiLogicalId(), - }, + RestApiId: this.provider.getApiGatewayRestApiId(), ValidateRequestBody: true, ValidateRequestParameters: true, }, 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 2ac2b2957..cdfe6ba94 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 @@ -71,8 +71,8 @@ describe('#compileMethods()', () => { ]; return awsCompileApigEvents.compileMethods().then(() => { expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreatePostValidator + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePostValidator ).to.deep.equal({ Type: 'AWS::ApiGateway::RequestValidator', Properties: { @@ -82,8 +82,8 @@ describe('#compileMethods()', () => { }, }); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreatePostApplicationJsonModel + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePostApplicationJsonModel ).to.deep.equal({ Type: 'AWS::ApiGateway::Model', Properties: { @@ -93,14 +93,14 @@ describe('#compileMethods()', () => { }, }); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreatePost.Properties.RequestModels + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.RequestModels ).to.deep.equal({ 'application/json': { Ref: 'ApiGatewayMethodUsersCreatePostApplicationJsonModel' }, }); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreatePost.Properties.RequestValidatorId + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.RequestValidatorId ).to.deep.equal({ Ref: 'ApiGatewayMethodUsersCreatePostValidator' }); }); }); @@ -135,34 +135,32 @@ describe('#compileMethods()', () => { ]; return awsCompileApigEvents.compileMethods().then(() => { expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreatePost.Properties - .RequestParameters['method.request.header.foo'] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.RequestParameters['method.request.header.foo'] ).to.equal(true); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreatePost.Properties - .RequestParameters['method.request.header.bar'] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.RequestParameters['method.request.header.bar'] ).to.equal(false); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreatePost.Properties - .RequestParameters['method.request.querystring.foo'] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.RequestParameters[ + 'method.request.querystring.foo' + ] ).to.equal(true); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreatePost.Properties - .RequestParameters['method.request.querystring.bar'] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.RequestParameters[ + 'method.request.querystring.bar' + ] ).to.equal(false); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreatePost.Properties - .RequestParameters['method.request.path.foo'] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.RequestParameters['method.request.path.foo'] ).to.equal(true); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreatePost.Properties - .RequestParameters['method.request.path.bar'] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.RequestParameters['method.request.path.bar'] ).to.equal(false); }); }); @@ -180,8 +178,8 @@ describe('#compileMethods()', () => { ]; return awsCompileApigEvents.compileMethods().then(() => { expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreatePost.Properties.Integration + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.Integration ).to.not.have.key('RequestParameters'); }); }); @@ -205,12 +203,12 @@ describe('#compileMethods()', () => { ]; return awsCompileApigEvents.compileMethods().then(() => { expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreatePost.Type + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Type ).to.equal('AWS::ApiGateway::Method'); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Type + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersListGet.Type ).to.equal('AWS::ApiGateway::Method'); }); }); @@ -238,12 +236,12 @@ describe('#compileMethods()', () => { ]; return awsCompileApigEvents.compileMethods().then(() => { expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreatePost.Properties.Integration.Type + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.Integration.Type ).to.equal('AWS'); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreatePost.Properties.Integration.RequestParameters + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.Integration.RequestParameters ).to.deep.equal({ 'integration.request.querystring.foo': 'method.request.querystring.foo', 'integration.request.querystring.bar': 'method.request.querystring.bar', @@ -268,8 +266,8 @@ describe('#compileMethods()', () => { ]; return awsCompileApigEvents.compileMethods().then(() => { expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreatePost.Properties.Integration.Type + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.Integration.Type ).to.equal('AWS_PROXY'); }); }); @@ -298,24 +296,24 @@ describe('#compileMethods()', () => { ]; return awsCompileApigEvents.compileMethods().then(() => { expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreatePost.Properties.Integration.Type + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.Integration.Type ).to.equal('HTTP'); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreatePost.Properties.Integration.Uri + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.Integration.Uri ).to.equal('https://example.com'); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreatePost.Properties.Integration.IntegrationHttpMethod + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.Integration.IntegrationHttpMethod ).to.equal('POST'); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreatePost.Properties.Integration.RequestTemplates + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.Integration.RequestTemplates ).to.equal(undefined); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreatePost.Properties.Integration.RequestParameters + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.Integration.RequestParameters ).to.deep.equal({ 'integration.request.querystring.foo': 'method.request.querystring.foo', 'integration.request.querystring.bar': 'method.request.querystring.bar', @@ -344,16 +342,16 @@ describe('#compileMethods()', () => { ]; return awsCompileApigEvents.compileMethods().then(() => { expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreatePost.Properties.Integration.Type + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.Integration.Type ).to.equal('HTTP'); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreatePost.Properties.Integration.Uri + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.Integration.Uri ).to.equal('https://example.com'); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreatePost.Properties.Integration.IntegrationHttpMethod + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.Integration.IntegrationHttpMethod ).to.equal('PUT'); }); }); @@ -383,20 +381,20 @@ describe('#compileMethods()', () => { ]; return awsCompileApigEvents.compileMethods().then(() => { expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreatePost.Properties.Integration.Type + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.Integration.Type ).to.equal('HTTP_PROXY'); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreatePost.Properties.Integration.Uri + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.Integration.Uri ).to.equal('https://example.com'); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreatePost.Properties.Integration.IntegrationHttpMethod + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.Integration.IntegrationHttpMethod ).to.equal('PATCH'); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreatePost.Properties.Integration.RequestParameters + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.Integration.RequestParameters ).to.deep.equal({ 'integration.request.querystring.foo': 'method.request.querystring.foo', 'integration.request.querystring.bar': 'method.request.querystring.bar', @@ -421,8 +419,8 @@ describe('#compileMethods()', () => { ]; return awsCompileApigEvents.compileMethods().then(() => { expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreatePost.Properties.Integration.Type + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.Integration.Type ).to.equal('MOCK'); }); }); @@ -440,9 +438,10 @@ describe('#compileMethods()', () => { ]; return awsCompileApigEvents.compileMethods().then(() => { expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreatePost.Properties.Integration - .RequestParameters['integration.request.header.X-Amz-Invocation-Type'] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.Integration.RequestParameters[ + 'integration.request.header.X-Amz-Invocation-Type' + ] ).to.equal("'Event'"); }); }); @@ -461,9 +460,10 @@ describe('#compileMethods()', () => { ]; return awsCompileApigEvents.compileMethods().then(() => { expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreatePost.Properties.Integration - .RequestParameters['integration.request.header.X-Amz-Invocation-Type'] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.Integration.RequestParameters[ + 'integration.request.header.X-Amz-Invocation-Type' + ] ).to.equal("'Event'"); }); }); @@ -483,13 +483,13 @@ describe('#compileMethods()', () => { ]; return awsCompileApigEvents.compileMethods().then(() => { expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreatePost.Properties.AuthorizationType + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.AuthorizationType ).to.equal('AWS_IAM'); }); }); - it('should set custom authorizer config with authorizeId', () => { + it('should set custom authorizer config with authorizerId', () => { awsCompileApigEvents.validated.events = [ { functionName: 'First', @@ -505,12 +505,12 @@ describe('#compileMethods()', () => { ]; return awsCompileApigEvents.compileMethods().then(() => { expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreatePost.Properties.AuthorizationType + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.AuthorizationType ).to.equal('COGNITO_USER_POOLS'); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreatePost.Properties.AuthorizerId + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.AuthorizerId ).to.equal('gy7lyj'); }); }); @@ -531,18 +531,18 @@ describe('#compileMethods()', () => { ]; return awsCompileApigEvents.compileMethods().then(() => { expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreatePost.Properties.AuthorizationType + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.AuthorizationType ).to.equal('CUSTOM'); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreatePost.Properties.AuthorizerId.Ref + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.AuthorizerId.Ref ).to.equal('AuthorizerApiGatewayAuthorizer'); }); }); - it('should set authorizer config for a cognito user pool', () => { + it('should set authorizer config for a cognito user pool when given authorizer arn', () => { awsCompileApigEvents.validated.events = [ { functionName: 'First', @@ -561,28 +561,98 @@ describe('#compileMethods()', () => { return awsCompileApigEvents.compileMethods().then(() => { expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreatePost.Properties.AuthorizationType + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.AuthorizationType ).to.equal('COGNITO_USER_POOLS'); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreatePost.Properties.AuthorizationScopes + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.AuthorizationScopes ).to.contain('myapp/read'); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreatePost.Properties.AuthorizerId.Ref + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.AuthorizerId.Ref ).to.equal('AuthorizerApiGatewayAuthorizer'); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreatePost.Properties - .Integration.RequestTemplates['application/json'] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.Integration.RequestTemplates[ + 'application/json' + ] ).to.not.match(/undefined/); }); }); + it('should set authorizer config for a cognito user pool when given authorizerId Ref', () => { + awsCompileApigEvents.validated.events = [ + { + functionName: 'First', + http: { + authorizer: { + name: 'authorizer', + type: 'COGNITO_USER_POOLS', + authorizerId: { Ref: 'CognitoAuthorizer' }, + scopes: ['myapp/read', 'myapp/write'], + }, + integration: 'AWS', + path: 'users/create', + method: 'post', + }, + }, + ]; + + return awsCompileApigEvents.compileMethods().then(() => { + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.AuthorizationType + ).to.equal('COGNITO_USER_POOLS'); + + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.AuthorizationScopes + ).to.contain('myapp/read'); + + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.AuthorizerId.Ref + ).to.equal('CognitoAuthorizer'); + + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.Integration.RequestTemplates[ + 'application/json' + ] + ).to.not.match(/undefined/); + }); + }); + + it('should not scopes for a cognito user pool when given empty scopes array', () => { + awsCompileApigEvents.validated.events = [ + { + functionName: 'First', + http: { + authorizer: { + name: 'authorizer', + type: 'COGNITO_USER_POOLS', + authorizerId: { Ref: 'CognitoAuthorizer' }, + scopes: [], + }, + integration: 'AWS', + path: 'users/create', + method: 'post', + }, + }, + ]; + + return awsCompileApigEvents.compileMethods().then(() => { + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties + ).to.not.have.property('AuthorizationScopes'); + }); + }); + it('should set claims for a cognito user pool', () => { awsCompileApigEvents.validated.events = [ { @@ -601,9 +671,11 @@ describe('#compileMethods()', () => { ]; return awsCompileApigEvents.compileMethods().then(() => { - const jsonRequestTemplatesString = awsCompileApigEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources.ApiGatewayMethodUsersCreatePost.Properties - .Integration.RequestTemplates['application/json']; + const jsonRequestTemplatesString = + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.Integration.RequestTemplates[ + 'application/json' + ]; const cognitoPoolClaimsRegex = /"cognitoPoolClaims"\s*:\s*(\{[^}]*\})/; const cognitoPoolClaimsString = jsonRequestTemplatesString.match(cognitoPoolClaimsRegex)[1]; const cognitoPoolClaims = JSON.parse(cognitoPoolClaimsString); @@ -629,9 +701,11 @@ describe('#compileMethods()', () => { ]; return awsCompileApigEvents.compileMethods().then(() => { - const jsonRequestTemplatesString = awsCompileApigEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources.ApiGatewayMethodUsersCreatePost.Properties - .Integration.RequestTemplates['application/json']; + const jsonRequestTemplatesString = + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.Integration.RequestTemplates[ + 'application/json' + ]; const cognitoPoolClaimsRegex = /"cognitoPoolClaims"\s*:\s*(\{[^}]*\})/; const cognitoPoolClaimsString = jsonRequestTemplatesString.match(cognitoPoolClaimsRegex)[1]; const cognitoPoolClaims = JSON.parse(cognitoPoolClaimsString); @@ -658,18 +732,19 @@ describe('#compileMethods()', () => { ]; return awsCompileApigEvents.compileMethods().then(() => { - const jsonRequestTemplatesString = awsCompileApigEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources.ApiGatewayMethodUsersCreatePost.Properties - .Integration.RequestTemplates['application/json']; + const jsonRequestTemplatesString = + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.Integration.RequestTemplates[ + 'application/json' + ]; const cognitoPoolClaimsRegex = /"cognitoPoolClaims"\s*:\s*(\{[^}]*\})/; const cognitoPoolClaimsString = jsonRequestTemplatesString.match(cognitoPoolClaimsRegex)[1]; const cognitoPoolClaims = JSON.parse(cognitoPoolClaimsString); expect(cognitoPoolClaims.email).to.equal('$context.authorizer.claims.email'); - expect(cognitoPoolClaims.score).to.equal('$context.authorizer.claims[\'custom:score\']'); + expect(cognitoPoolClaims.score).to.equal("$context.authorizer.claims['custom:score']"); }); }); - it('should replace the extra claims in the template if there are none', () => { awsCompileApigEvents.validated.events = [ { @@ -688,9 +763,10 @@ describe('#compileMethods()', () => { return awsCompileApigEvents.compileMethods().then(() => { expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreatePost.Properties - .Integration.RequestTemplates['application/json'] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.Integration.RequestTemplates[ + 'application/json' + ] ).to.not.match(/extraCognitoPoolClaims/); }); }); @@ -744,8 +820,8 @@ describe('#compileMethods()', () => { ]; return awsCompileApigEvents.compileMethods().then(() => { expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreatePost.Properties.ApiKeyRequired + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.ApiKeyRequired ).to.equal(true); }); }); @@ -762,8 +838,8 @@ describe('#compileMethods()', () => { ]; return awsCompileApigEvents.compileMethods().then(() => { expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreatePost.Properties.ApiKeyRequired + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.ApiKeyRequired ).to.equal(false); }); }); @@ -786,30 +862,36 @@ describe('#compileMethods()', () => { }, ]; return awsCompileApigEvents.compileMethods().then(() => { - expect(awsCompileApigEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .ApiGatewayMethodUsersCreatePost.Properties.Integration.Uri + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.Integration.Uri ).to.deep.equal({ 'Fn::Join': [ - '', [ + '', + [ 'arn:', { Ref: 'AWS::Partition' }, - ':apigateway:', { Ref: 'AWS::Region' }, - ':lambda:path/2015-03-31/functions/', { 'Fn::GetAtt': ['FirstLambdaFunction', 'Arn'] }, + ':apigateway:', + { Ref: 'AWS::Region' }, + ':lambda:path/2015-03-31/functions/', + { 'Fn::GetAtt': ['FirstLambdaFunction', 'Arn'] }, '/invocations', ], ], }); - expect(awsCompileApigEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .ApiGatewayMethodUsersListGet.Properties.Integration.Uri + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersListGet.Properties.Integration.Uri ).to.deep.equal({ 'Fn::Join': [ - '', [ + '', + [ 'arn:', { Ref: 'AWS::Partition' }, - ':apigateway:', { Ref: 'AWS::Region' }, - ':lambda:path/2015-03-31/functions/', { 'Fn::GetAtt': ['SecondLambdaFunction', 'Arn'] }, + ':apigateway:', + { Ref: 'AWS::Region' }, + ':lambda:path/2015-03-31/functions/', + { 'Fn::GetAtt': ['SecondLambdaFunction', 'Arn'] }, '/invocations', ], ], @@ -873,26 +955,23 @@ describe('#compileMethods()', () => { ]; return awsCompileApigEvents.compileMethods().then(() => { expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreatePost.Properties - .Integration.IntegrationResponses[0] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.Integration.IntegrationResponses[0] .ResponseParameters['method.response.header.Access-Control-Allow-Origin'] - ).to.equal('\'http://example.com\''); + ).to.equal("'http://example.com'"); // CORS not enabled! expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties - .Integration.IntegrationResponses[0] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[0] .ResponseParameters['method.response.header.Access-Control-Allow-Origin'] - ).to.not.equal('\'*\''); + ).to.not.equal("'*'"); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersUpdatePut.Properties - .Integration.IntegrationResponses[0] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersUpdatePut.Properties.Integration.IntegrationResponses[0] .ResponseParameters['method.response.header.Access-Control-Allow-Origin'] - ).to.equal('\'*\''); + ).to.equal("'*'"); }); }); @@ -916,9 +995,11 @@ describe('#compileMethods()', () => { }, ]; return awsCompileApigEvents.compileMethods().then(() => { - expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties - .Integration.RequestTemplates['application/json'] + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersListGet.Properties.Integration.RequestTemplates[ + 'application/json' + ] ).to.have.length.above(0); }); }); @@ -942,9 +1023,11 @@ describe('#compileMethods()', () => { }, ]; return awsCompileApigEvents.compileMethods().then(() => { - expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties - .Integration.RequestTemplates['application/x-www-form-urlencoded'] + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersListGet.Properties.Integration.RequestTemplates[ + 'application/x-www-form-urlencoded' + ] ).to.have.length.above(0); }); }); @@ -971,8 +1054,9 @@ describe('#compileMethods()', () => { }, ]; return awsCompileApigEvents.compileMethods().then(() => { - expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.PassthroughBehavior + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersListGet.Properties.Integration.PassthroughBehavior ).to.equal('WHEN_NO_TEMPLATES'); }); }); @@ -1002,14 +1086,14 @@ describe('#compileMethods()', () => { }, ]; return awsCompileApigEvents.compileMethods().then(() => { - expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties - .Integration.RequestTemplates['template/1'] + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersListGet.Properties.Integration.RequestTemplates['template/1'] ).to.equal('{ "stage" : "$context.stage" }'); - expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties - .Integration.RequestTemplates['template/2'] + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersListGet.Properties.Integration.RequestTemplates['template/2'] ).to.equal('{ "httpMethod" : "$context.httpMethod" }'); }); }); @@ -1038,9 +1122,11 @@ describe('#compileMethods()', () => { }, ]; return awsCompileApigEvents.compileMethods().then(() => { - expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties - .Integration.RequestTemplates['application/json'] + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersListGet.Properties.Integration.RequestTemplates[ + 'application/json' + ] ).to.equal('overwritten-request-template-content'); }); }); @@ -1071,13 +1157,13 @@ describe('#compileMethods()', () => { ]; return awsCompileApigEvents.compileMethods().then(() => { expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[0] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[0] .ResponseParameters['method.response.header.Content-Type'] ).to.equal("'text/plain'"); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[0] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[0] .ResponseParameters['method.response.header.My-Custom-Header'] ).to.equal('my/custom/header'); }); @@ -1104,8 +1190,8 @@ describe('#compileMethods()', () => { ]; return awsCompileApigEvents.compileMethods().then(() => { expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[0] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[0] .ResponseTemplates['application/json'] ).to.equal("$input.path('$.foo')"); }); @@ -1135,12 +1221,12 @@ describe('#compileMethods()', () => { ]; return awsCompileApigEvents.compileMethods().then(() => { expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.MethodResponses[0].StatusCode + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersListGet.Properties.MethodResponses[0].StatusCode ).to.equal(200); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.MethodResponses[1].StatusCode + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersListGet.Properties.MethodResponses[1].StatusCode ).to.equal(202); }); }); @@ -1168,8 +1254,8 @@ describe('#compileMethods()', () => { ]; return awsCompileApigEvents.compileMethods().then(() => { expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[1] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[1] ).to.deep.equal({ StatusCode: 202, SelectionPattern: 'foo', @@ -1177,8 +1263,8 @@ describe('#compileMethods()', () => { ResponseTemplates: {}, }); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[0] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[0] ).to.deep.equal({ StatusCode: 200, SelectionPattern: '', @@ -1212,13 +1298,13 @@ describe('#compileMethods()', () => { ]; return awsCompileApigEvents.compileMethods().then(() => { expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[0] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[0] .ResponseTemplates['application/json'] ).to.equal('foo'); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[0] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[0] .ResponseParameters['method.response.header.Content-Type'] ).to.equal('text/csv'); }); @@ -1236,11 +1322,11 @@ describe('#compileMethods()', () => { statusCodes: { 200: { pattern: '', - template: '$input.path(\'$.foo\')', + template: "$input.path('$.foo')", }, 404: { pattern: '.*"statusCode":404,.*', - template: '$input.path(\'$.errorMessage\')', + template: "$input.path('$.errorMessage')", headers: { 'Content-Type': 'text/html', }, @@ -1252,28 +1338,28 @@ describe('#compileMethods()', () => { ]; return awsCompileApigEvents.compileMethods().then(() => { expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[0] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[0] .ResponseTemplates['application/json'] ).to.equal("$input.path('$.foo')"); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[0] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[0] .SelectionPattern ).to.equal(''); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[1] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[1] .ResponseTemplates['application/json'] ).to.equal("$input.path('$.errorMessage')"); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[1] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[1] .SelectionPattern ).to.equal('.*"statusCode":404,.*'); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[1] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[1] .ResponseParameters['method.response.header.Content-Type'] ).to.equal('text/html'); }); @@ -1290,7 +1376,7 @@ describe('#compileMethods()', () => { response: { statusCodes: { 200: { - template: '$input.path(\'$.foo\')', + template: "$input.path('$.foo')", headers: { 'Content-Type': 'text/csv', }, @@ -1298,8 +1384,8 @@ describe('#compileMethods()', () => { 404: { pattern: '.*"statusCode":404,.*', template: { - 'application/json': '$input.path(\'$.errorMessage\')', - 'application/xml': '$input.path(\'$.xml.errorMessage\')', + 'application/json': "$input.path('$.errorMessage')", + 'application/xml': "$input.path('$.xml.errorMessage')", }, headers: { 'Content-Type': 'text/html', @@ -1312,38 +1398,38 @@ describe('#compileMethods()', () => { ]; return awsCompileApigEvents.compileMethods().then(() => { expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[0] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[0] .ResponseTemplates['application/json'] ).to.equal("$input.path('$.foo')"); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[0] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[0] .SelectionPattern ).to.equal(''); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[0] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[0] .ResponseParameters['method.response.header.Content-Type'] ).to.equal('text/csv'); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[1] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[1] .ResponseTemplates['application/json'] ).to.equal("$input.path('$.errorMessage')"); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[1] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[1] .ResponseTemplates['application/xml'] ).to.equal("$input.path('$.xml.errorMessage')"); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[1] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[1] .SelectionPattern ).to.equal('.*"statusCode":404,.*'); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[1] + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[1] .ResponseParameters['method.response.header.Content-Type'] ).to.equal('text/html'); }); @@ -1360,13 +1446,12 @@ describe('#compileMethods()', () => { }, ]; return awsCompileApigEvents.compileMethods().then(() => { - expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodGet.Properties.ResourceId).to.deep.equal({ - 'Fn::GetAtt': [ - 'ApiGatewayRestApi', - 'RootResourceId', - ], - }); + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodGet.Properties.ResourceId + ).to.deep.equal({ + 'Fn::GetAtt': ['ApiGatewayRestApi', 'RootResourceId'], + }); }); }); }); diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/method/integration.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/method/integration.js index de7da698c..ef7844947 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/method/integration.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/method/integration.js @@ -63,7 +63,8 @@ module.exports = { if (type === 'AWS' || type === 'AWS_PROXY') { _.assign(integration, { Uri: { - 'Fn::Join': ['', + 'Fn::Join': [ + '', [ 'arn:', { Ref: 'AWS::Partition' }, @@ -92,8 +93,11 @@ module.exports = { IntegrationResponses: this.getIntegrationResponses(http), }); } - if (((type === 'AWS' || type === 'HTTP' || type === 'HTTP_PROXY') && - (http.request && !_.isEmpty(http.request.parameters))) || http.async) { + if ( + ((type === 'AWS' || type === 'HTTP' || type === 'HTTP_PROXY') && + (http.request && !_.isEmpty(http.request.parameters))) || + http.async + ) { _.assign(integration, { RequestParameters: this.getIntegrationRequestParameters(http), }); @@ -129,8 +133,10 @@ module.exports = { } _.each(http.response.statusCodes, (config, statusCode) => { - const responseParameters = _.mapKeys(integrationResponseHeaders, - (value, header) => `method.response.header.${header}`); + const responseParameters = _.mapKeys( + integrationResponseHeaders, + (value, header) => `method.response.header.${header}` + ); const integrationResponse = { StatusCode: parseInt(statusCode, 10), @@ -140,8 +146,10 @@ module.exports = { }; if (config.headers) { - _.merge(integrationResponse.ResponseParameters, _.mapKeys(config.headers, - (value, header) => `method.response.header.${header}`)); + _.merge( + integrationResponse.ResponseParameters, + _.mapKeys(config.headers, (value, header) => `method.response.header.${header}`) + ); } if (http.response.template) { @@ -151,9 +159,10 @@ module.exports = { } if (config.template) { - const template = typeof config.template === 'string' ? - { 'application/json': config.template } - : config.template; + const template = + typeof config.template === 'string' + ? { 'application/json': config.template } + : config.template; _.merge(integrationResponse.ResponseTemplates, template); } diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/method/responses.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/method/responses.js index 5d57455e1..f44bc6ce3 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/method/responses.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/method/responses.js @@ -3,7 +3,6 @@ const _ = require('lodash'); module.exports = { - getMethodResponses(http) { const methodResponses = []; @@ -34,12 +33,16 @@ module.exports = { StatusCode: parseInt(statusCode, 10), }; - _.merge(methodResponse.ResponseParameters, - this.getMethodResponseHeaders(methodResponseHeaders)); + _.merge( + methodResponse.ResponseParameters, + this.getMethodResponseHeaders(methodResponseHeaders) + ); if (config.headers) { - _.merge(methodResponse.ResponseParameters, - this.getMethodResponseHeaders(config.headers)); + _.merge( + methodResponse.ResponseParameters, + this.getMethodResponseHeaders(config.headers) + ); } methodResponses.push(methodResponse); @@ -63,5 +66,4 @@ module.exports = { return methodResponseHeaders; }, - }; diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/permissions.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/permissions.js index 61641b535..decf7ff49 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/permissions.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/permissions.js @@ -5,11 +5,11 @@ const BbPromise = require('bluebird'); const awsArnRegExs = require('../../../../../utils/arnRegularExpressions'); module.exports = { - compilePermissions() { - this.permissionMapping.forEach((singlePermissionMapping) => { - const lambdaPermissionLogicalId = this.provider.naming - .getLambdaApiGatewayPermissionLogicalId(singlePermissionMapping.event.functionName); + this.permissionMapping.forEach(singlePermissionMapping => { + const lambdaPermissionLogicalId = this.provider.naming.getLambdaApiGatewayPermissionLogicalId( + singlePermissionMapping.event.functionName + ); _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, { [lambdaPermissionLogicalId]: { @@ -19,9 +19,10 @@ module.exports = { 'Fn::GetAtt': [singlePermissionMapping.lambdaLogicalId, 'Arn'], }, Action: 'lambda:InvokeFunction', - Principal: { 'Fn::Join': ['', ['apigateway.', { Ref: 'AWS::URLSuffix' }]] }, + Principal: 'apigateway.amazonaws.com', SourceArn: { - 'Fn::Join': ['', + 'Fn::Join': [ + '', [ 'arn:', { Ref: 'AWS::Partition' }, @@ -39,14 +40,19 @@ module.exports = { }, }); - if (singlePermissionMapping.event.http.authorizer && - singlePermissionMapping.event.http.authorizer.arn) { + if ( + singlePermissionMapping.event.http.authorizer && + singlePermissionMapping.event.http.authorizer.arn + ) { const authorizer = singlePermissionMapping.event.http.authorizer; - const authorizerPermissionLogicalId = this.provider.naming - .getLambdaApiGatewayPermissionLogicalId(authorizer.name); + const authorizerPermissionLogicalId = this.provider.naming.getLambdaApiGatewayPermissionLogicalId( + authorizer.name + ); - if (typeof authorizer.arn === 'string' - && awsArnRegExs.cognitoIdpArnExpr.test(authorizer.arn)) { + if ( + typeof authorizer.arn === 'string' && + awsArnRegExs.cognitoIdpArnExpr.test(authorizer.arn) + ) { return; } @@ -56,7 +62,7 @@ module.exports = { Properties: { FunctionName: authorizer.arn, Action: 'lambda:InvokeFunction', - Principal: { 'Fn::Join': ['', ['apigateway.', { Ref: 'AWS::URLSuffix' }]] }, + Principal: 'apigateway.amazonaws.com', }, }, }); diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/permissions.test.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/permissions.test.js index a0ff32a67..853d7fd23 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/permissions.test.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/permissions.test.js @@ -44,12 +44,14 @@ describe('#awsCompilePermissions()', () => { ]; return awsCompileApigEvents.compilePermissions().then(() => { - expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FirstLambdaPermissionApiGateway - .Properties.FunctionName['Fn::GetAtt'][0]).to.equal('FirstLambdaFunction'); + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstLambdaPermissionApiGateway.Properties.FunctionName['Fn::GetAtt'][0] + ).to.equal('FirstLambdaFunction'); const deepObj = { - 'Fn::Join': ['', + 'Fn::Join': [ + '', [ 'arn:', { Ref: 'AWS::Partition' }, @@ -64,9 +66,10 @@ describe('#awsCompilePermissions()', () => { ], }; - expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FirstLambdaPermissionApiGateway - .Properties.SourceArn).to.deep.equal(deepObj); + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstLambdaPermissionApiGateway.Properties.SourceArn + ).to.deep.equal(deepObj); }); }); @@ -99,12 +102,14 @@ describe('#awsCompilePermissions()', () => { ]; return awsCompileApigEvents.compilePermissions().then(() => { - expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FirstLambdaPermissionApiGateway - .Properties.FunctionName['Fn::GetAtt'][0]).to.equal('FirstLambdaFunction'); + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstLambdaPermissionApiGateway.Properties.FunctionName['Fn::GetAtt'][0] + ).to.equal('FirstLambdaFunction'); const deepObj = { - 'Fn::Join': ['', + 'Fn::Join': [ + '', [ 'arn:', { Ref: 'AWS::Partition' }, @@ -119,9 +124,10 @@ describe('#awsCompilePermissions()', () => { ], }; - expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FirstLambdaPermissionApiGateway - .Properties.SourceArn).to.deep.equal(deepObj); + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstLambdaPermissionApiGateway.Properties.SourceArn + ).to.deep.equal(deepObj); }); }); @@ -158,9 +164,10 @@ describe('#awsCompilePermissions()', () => { }, ]; return awsCompileApigEvents.compilePermissions().then(() => { - expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.AuthorizerLambdaPermissionApiGateway - .Properties.FunctionName['Fn::GetAtt'][0]).to.equal('AuthorizerLambdaFunction'); + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .AuthorizerLambdaPermissionApiGateway.Properties.FunctionName['Fn::GetAtt'][0] + ).to.equal('AuthorizerLambdaFunction'); }); }); diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/resources.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/resources.js index fac0b36ca..5fb01f1a5 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/resources.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/resources.js @@ -4,12 +4,11 @@ const BbPromise = require('bluebird'); const _ = require('lodash'); module.exports = { - compileResources() { this.apiGatewayResources = this.getResourcePaths(); // ['users', 'users/create', 'users/create/something'] - _.keys(this.apiGatewayResources).forEach((path) => { + _.keys(this.apiGatewayResources).forEach(path => { const resource = this.apiGatewayResources[path]; if (resource.resourceId) { return; @@ -18,8 +17,7 @@ module.exports = { resource.resourceLogicalId = this.provider.naming.getResourceLogicalId(path); resource.resourceId = { Ref: resource.resourceLogicalId }; - const parentRef = resource.parent - ? resource.parent.resourceId : this.getResourceId(); + const parentRef = resource.parent ? resource.parent.resourceId : this.getResourceId(); _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, { [resource.resourceLogicalId]: { @@ -46,13 +44,17 @@ module.exports = { r[node.path].name = self.provider.naming.normalizePath(node.path); } - node.children.forEach((child) => getNodePaths(result, child)); + node.children.forEach(child => getNodePaths(result, child)); } - return _.reduce(trees, (result, tree) => { - getNodePaths(result, tree); - return result; - }, {}); + return _.reduce( + trees, + (result, tree) => { + getNodePaths(result, tree); + return result; + }, + {} + ); }, getResourcePaths() { @@ -61,7 +63,6 @@ module.exports = { const methodNodes = []; const predefinedResources = this.provider.getApiGatewayPredefinedResources(); - function cutBranch(node) { if (!node.parent) { return; @@ -71,7 +72,7 @@ module.exports = { if (node.parent.children.length <= 1) { n.parent.children = []; } else { - n.parent.children = node.parent.children.filter((c) => c.path !== n.path); + n.parent.children = node.parent.children.filter(c => c.path !== n.path); n.parent.isCut = true; } n.parent = null; @@ -91,13 +92,13 @@ module.exports = { n.name = resource.name; if (resource.resourceId) { n.resourceId = resource.resourceId; - if (_.every(predefinedResourceNodes, (iter) => iter.path !== n.path)) { + if (_.every(predefinedResourceNodes, iter => iter.path !== n.path)) { predefinedResourceNodes.push(node); } } if (isMethod && !node.hasMethod) { n.hasMethod = true; - if (_.every(methodNodes, (iter) => iter.path !== n.path)) { + if (_.every(methodNodes, iter => iter.path !== n.path)) { methodNodes.push(node); } } @@ -108,7 +109,7 @@ module.exports = { pathParts.forEach((pathPart, index) => { currentPath = currentPath ? `${currentPath}/${pathPart}` : pathPart; - root = root || _.find(trees, (node) => node.path === currentPath); + root = root || _.find(trees, node => node.path === currentPath); parent = parent || root; let node; @@ -117,7 +118,7 @@ module.exports = { applyNodeResource(parent, pathParts, index); return; } else if (parent.children.length > 0) { - node = _.find(parent.children, (n) => n.path === currentPath); + node = _.find(parent.children, n => n.path === currentPath); if (node) { applyNodeResource(node, pathParts, index); return; @@ -148,7 +149,7 @@ module.exports = { } predefinedResources.forEach(applyResource); - this.validated.events.forEach((event) => { + this.validated.events.forEach(event => { if (event.http.path) { applyResource(event.http, true); } @@ -160,23 +161,28 @@ module.exports = { } // if all methods have resource ID already, no need to validate resource trees - if (_.every(this.validated.events, (event) => - _.some(predefinedResourceNodes, (node) => - node.path === event.http.path))) { - return _.reduce(predefinedResources, (resourceMap, resource) => { - const r = resourceMap; - r[resource.path] = resource; + if ( + _.every(this.validated.events, event => + _.some(predefinedResourceNodes, node => node.path === event.http.path) + ) + ) { + return _.reduce( + predefinedResources, + (resourceMap, resource) => { + const r = resourceMap; + r[resource.path] = resource; - if (!resource.name) { - r[resource.path].name = this.provider.naming.normalizePath(resource.path); - } - return r; - }, {}); + if (!resource.name) { + r[resource.path].name = this.provider.naming.normalizePath(resource.path); + } + return r; + }, + {} + ); } // cut resource branches from trees - const sortedResourceNodes = _.sortBy(predefinedResourceNodes, - node => node.level); + const sortedResourceNodes = _.sortBy(predefinedResourceNodes, node => node.level); const validatedTrees = []; for (let i = sortedResourceNodes.length - 1; i >= 0; i--) { @@ -198,12 +204,12 @@ module.exports = { } // get branches that begin from root resource - methodNodes.forEach((node) => { + methodNodes.forEach(node => { let iter = node; while (iter) { if (iter.resourceId) { cutBranch(iter); - if (_.every(validatedTrees, (t) => t.path !== node.path)) { + if (_.every(validatedTrees, t => t.path !== node.path)) { validatedTrees.push(iter); } @@ -235,10 +241,13 @@ module.exports = { throw new Error(`Can not find API Gateway resource from path ${path}`); } - if (!this.apiGatewayResources[path].resourceId - && this.apiGatewayResources[path].resourceLogicalId) { - this.apiGatewayResources[path].resourceId = - { Ref: this.apiGatewayResources[path].resourceLogicalId }; + if ( + !this.apiGatewayResources[path].resourceId && + this.apiGatewayResources[path].resourceLogicalId + ) { + this.apiGatewayResources[path].resourceId = { + Ref: this.apiGatewayResources[path].resourceLogicalId, + }; } return this.apiGatewayResources[path].resourceId; }, diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/resources.test.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/resources.test.js index 18daad469..1c649d385 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/resources.test.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/resources.test.js @@ -117,18 +117,22 @@ describe('#compileResources()', () => { }, ]; return awsCompileApigEvents.compileResources().then(() => { - expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayResourceFoo.Properties.ParentId['Fn::GetAtt'][0]) - .to.equal('ApiGatewayRestApi'); - expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayResourceFoo.Properties.ParentId['Fn::GetAtt'][1]) - .to.equal('RootResourceId'); - expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayResourceFooBar.Properties.ParentId.Ref) - .to.equal('ApiGatewayResourceFoo'); - expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayResourceBarIdVar.Properties.ParentId.Ref) - .to.equal('ApiGatewayResourceBar'); + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayResourceFoo.Properties.ParentId['Fn::GetAtt'][0] + ).to.equal('ApiGatewayRestApi'); + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayResourceFoo.Properties.ParentId['Fn::GetAtt'][1] + ).to.equal('RootResourceId'); + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayResourceFooBar.Properties.ParentId.Ref + ).to.equal('ApiGatewayResourceFoo'); + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayResourceBarIdVar.Properties.ParentId.Ref + ).to.equal('ApiGatewayResourceBar'); }); }); @@ -161,15 +165,16 @@ describe('#compileResources()', () => { ]; return awsCompileApigEvents.compileResources().then(() => { const expectedResourceLogicalIds = { - baz: 'ApiGatewayResourceBaz', + 'baz': 'ApiGatewayResourceBaz', 'baz/foo': 'ApiGatewayResourceBazFoo', - foo: 'ApiGatewayResourceFoo', + 'foo': 'ApiGatewayResourceFoo', 'foo/{foo_id}': 'ApiGatewayResourceFooFooidVar', 'foo/{foo_id}/bar': 'ApiGatewayResourceFooFooidVarBar', }; - Object.keys(expectedResourceLogicalIds).forEach((path) => { - expect(awsCompileApigEvents.apiGatewayResources[path].resourceLogicalId) - .equal(expectedResourceLogicalIds[path]); + Object.keys(expectedResourceLogicalIds).forEach(path => { + expect(awsCompileApigEvents.apiGatewayResources[path].resourceLogicalId).equal( + expectedResourceLogicalIds[path] + ); }); }); }); @@ -191,13 +196,14 @@ describe('#compileResources()', () => { ]; return awsCompileApigEvents.compileResources().then(() => { const expectedResourceLogicalIds = { - foo: 'ApiGatewayResourceFoo', + 'foo': 'ApiGatewayResourceFoo', 'foo/bar': 'ApiGatewayResourceFooBar', 'foo/{bar}': 'ApiGatewayResourceFooBarVar', }; - Object.keys(expectedResourceLogicalIds).forEach((path) => { - expect(awsCompileApigEvents.apiGatewayResources[path].resourceLogicalId) - .equal(expectedResourceLogicalIds[path]); + Object.keys(expectedResourceLogicalIds).forEach(path => { + expect(awsCompileApigEvents.apiGatewayResources[path].resourceLogicalId).equal( + expectedResourceLogicalIds[path] + ); }); }); }); @@ -224,15 +230,18 @@ describe('#compileResources()', () => { }, ]; return awsCompileApigEvents.compileResources().then(() => { - expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayResourceFooBar.Properties.PathPart) - .to.equal('bar'); - expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayResourceFooBarVar.Properties.PathPart) - .to.equal('{bar}'); - expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayResourceFooBarVarBaz.Properties.PathPart) - .to.equal('baz'); + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayResourceFooBar.Properties.PathPart + ).to.equal('bar'); + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayResourceFooBarVar.Properties.PathPart + ).to.equal('{bar}'); + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayResourceFooBarVarBaz.Properties.PathPart + ).to.equal('baz'); }); }); @@ -246,8 +255,9 @@ describe('#compileResources()', () => { }, ]; return awsCompileApigEvents.compileResources().then(() => { - expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources).to.deep.equal({}); + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + ).to.deep.equal({}); }); }); @@ -333,25 +343,34 @@ describe('#compileResources()', () => { expect(e.message).to.equal('Can not find API Gateway resource from path users/{userId}'); } - expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayResourceFoo).to.equal(undefined); - expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayResourceBar.Properties.RestApiId) - .to.equal('6fyzt1pfpk'); - expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayResourceBar.Properties.ParentId) - .to.equal('z5d4qh4oqi'); - expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayResourceFooBar.Properties.ParentId) - .to.equal('axcybf2i39'); - expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayResourceBarIdVar.Properties.ParentId.Ref) - .to.equal('ApiGatewayResourceBar'); - expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayResourceUsersMePosts).not.equal(undefined); - expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayResourceUsersFriendsComments.Properties.ParentId) - .to.equal('fcasdoojp1'); + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayResourceFoo + ).to.equal(undefined); + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayResourceBar.Properties.RestApiId + ).to.equal('6fyzt1pfpk'); + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayResourceBar.Properties.ParentId + ).to.equal('z5d4qh4oqi'); + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayResourceFooBar.Properties.ParentId + ).to.equal('axcybf2i39'); + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayResourceBarIdVar.Properties.ParentId.Ref + ).to.equal('ApiGatewayResourceBar'); + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayResourceUsersMePosts + ).not.equal(undefined); + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayResourceUsersFriendsComments.Properties.ParentId + ).to.equal('fcasdoojp1'); }); }); @@ -360,8 +379,8 @@ describe('#compileResources()', () => { restApiId: '6fyzt1pfpk', restApiRootResourceId: 'z5d4qh4oqi', restApiResources: { - foo: 'axcybf2i39', - users: 'zxcvbnmasd', + 'foo': 'axcybf2i39', + 'users': 'zxcvbnmasd', 'users/friends': 'fcasdoojp1', 'users/is/this/a/long/path': 'sadvgpoujk', }, @@ -395,14 +414,22 @@ describe('#compileResources()', () => { ]; return awsCompileApigEvents.compileResources().then(() => { - expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayResourceFoo).to.equal(undefined); - expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayResourceUsers).to.equal(undefined); - expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayResourceUsersFriends).to.equal(undefined); - expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayResourceUsersIsThis).to.equal(undefined); + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayResourceFoo + ).to.equal(undefined); + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayResourceUsers + ).to.equal(undefined); + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayResourceUsersFriends + ).to.equal(undefined); + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayResourceUsersIsThis + ).to.equal(undefined); }); }); @@ -430,8 +457,10 @@ describe('#compileResources()', () => { }, ]; - expect(() => awsCompileApigEvents.compileResources()) - .to.throw(Error, 'Resource ID for path users is required'); + expect(() => awsCompileApigEvents.compileResources()).to.throw( + Error, + 'Resource ID for path users is required' + ); }); it('should named all method paths if all resources are predefined', () => { @@ -478,11 +507,13 @@ describe('#compileResources()', () => { ]; return awsCompileApigEvents.compileResources().then(() => { - expect(Object.keys(awsCompileApigEvents.serverless - .service.provider.compiledCloudFormationTemplate - .Resources).every((k) => ['ApiGatewayMethodundefinedGet', - 'ApiGatewayMethodundefinedPost'].indexOf(k) === -1)) - .to.equal(true); + expect( + Object.keys( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + ).every( + k => ['ApiGatewayMethodundefinedGet', 'ApiGatewayMethodundefinedPost'].indexOf(k) === -1 + ) + ).to.equal(true); }); }); }); diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/restApi.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/restApi.js index baf6f7b9e..b420b2881 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/restApi.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/restApi.js @@ -28,10 +28,10 @@ module.exports = { throw new this.serverless.classes.Error('endpointType must be a string'); } - if (!_.includes(validEndpointTypes, endpointType.toUpperCase())) { - const message = 'endpointType must be one of "REGIONAL" or "EDGE" or "PRIVATE". ' + - `You provided ${endpointType}.`; + const message = + 'endpointType must be one of "REGIONAL" or "EDGE" or "PRIVATE". ' + + `You provided ${endpointType}.`; throw new this.serverless.classes.Error(message); } endpointType = endpointType.toUpperCase(); @@ -55,10 +55,14 @@ module.exports = { Version: '2012-10-17', Statement: this.serverless.service.provider.resourcePolicy, }; - _.merge(this.serverless.service.provider.compiledCloudFormationTemplate - .Resources[this.apiGatewayRestApiLogicalId].Properties, { - Policy: policy, - }); + _.merge( + this.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + this.apiGatewayRestApiLogicalId + ].Properties, + { + Policy: policy, + } + ); } if (!_.isEmpty(apiGateway.apiKeySourceType)) { @@ -73,8 +77,9 @@ module.exports = { } _.merge( - this.serverless.service.provider.compiledCloudFormationTemplate - .Resources[this.apiGatewayRestApiLogicalId].Properties, + this.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + this.apiGatewayRestApiLogicalId + ].Properties, { ApiKeySourceType: apiKeySourceType } ); } @@ -97,8 +102,9 @@ module.exports = { } _.merge( - this.serverless.service.provider.compiledCloudFormationTemplate - .Resources[this.apiGatewayRestApiLogicalId].Properties, + this.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + this.apiGatewayRestApiLogicalId + ].Properties, { MinimumCompressionSize: minimumCompressionSize } ); } diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/restApi.test.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/restApi.test.js index 559d825d5..4c7ffa6df 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/restApi.test.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/restApi.test.js @@ -1,10 +1,13 @@ 'use strict'; -const expect = require('chai').expect; +const chai = require('chai'); const AwsCompileApigEvents = require('../index'); const Serverless = require('../../../../../../../Serverless'); const AwsProvider = require('../../../../../provider/awsProvider'); +const expect = chai.expect; +chai.use(require('chai-as-promised')); + describe('#compileRestApi()', () => { let serverless; let awsCompileApigEvents; @@ -35,8 +38,8 @@ describe('#compileRestApi()', () => { it('should create a REST API resource', () => awsCompileApigEvents.compileRestApi().then(() => { - const resources = awsCompileApigEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources; + const resources = + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources; expect(resources.ApiGatewayRestApi).to.deep.equal({ Type: 'AWS::ApiGateway::RestApi', @@ -65,8 +68,8 @@ describe('#compileRestApi()', () => { }, ]; return awsCompileApigEvents.compileRestApi().then(() => { - const resources = awsCompileApigEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources; + const resources = + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources; expect(resources.ApiGatewayRestApi).to.deep.equal({ Type: 'AWS::ApiGateway::RestApi', @@ -103,33 +106,26 @@ describe('#compileRestApi()', () => { restApiRootResourceId: 'z5d4qh4oqi', }; return awsCompileApigEvents.compileRestApi().then(() => { - expect(awsCompileApigEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources).to.deep.equal( - {} - ); + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + ).to.deep.equal({}); }); }); it('should set binary media types if defined at the apiGateway provider config level', () => { awsCompileApigEvents.serverless.service.provider.apiGateway = { - binaryMediaTypes: [ - '*/*', - ], + binaryMediaTypes: ['*/*'], }; return awsCompileApigEvents.compileRestApi().then(() => { - const resources = awsCompileApigEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources; + const resources = + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources; expect(resources.ApiGatewayRestApi).to.deep.equal({ Type: 'AWS::ApiGateway::RestApi', Properties: { - BinaryMediaTypes: [ - '*/*', - ], + BinaryMediaTypes: ['*/*'], EndpointConfiguration: { - Types: [ - 'EDGE', - ], + Types: ['EDGE'], }, Name: 'dev-new-service', }, @@ -163,14 +159,15 @@ describe('#compileRestApi()', () => { }); it('should compile correctly if apiKeySourceType property is AUTHORIZER', () => { - awsCompileApigEvents.serverless.service.provider.apiGateway = - { apiKeySourceType: 'AUTHORIZER' }; + awsCompileApigEvents.serverless.service.provider.apiGateway = { + apiKeySourceType: 'AUTHORIZER', + }; expect(() => awsCompileApigEvents.compileRestApi()).to.not.throw(Error); }); it('throw error if apiKeySourceType is not HEADER or AUTHORIZER', () => { awsCompileApigEvents.serverless.service.provider.apiGateway = { apiKeySourceType: 'Testing' }; - expect(awsCompileApigEvents.compileRestApi()).to.be.rejectedWith(Error); + return expect(awsCompileApigEvents.compileRestApi()).to.be.rejectedWith(Error); }); it('should compile correctly if minimumCompressionSize is an integer', () => { @@ -184,20 +181,20 @@ describe('#compileRestApi()', () => { awsCompileApigEvents.serverless.service.provider.apiGateway = { minimumCompressionSize: 'Testing', }; - expect(awsCompileApigEvents.compileRestApi()).to.be.rejectedWith(Error); + return expect(awsCompileApigEvents.compileRestApi()).to.be.rejectedWith(Error); }); it('should throw error if minimumCompressionSize is less than 0', () => { awsCompileApigEvents.serverless.service.provider.apiGateway = { minimumCompressionSize: -1, }; - expect(awsCompileApigEvents.compileRestApi()).to.be.rejectedWith(Error); + return expect(awsCompileApigEvents.compileRestApi()).to.be.rejectedWith(Error); }); it('should throw error if minimumCompressionSize is greater than 10485760', () => { awsCompileApigEvents.serverless.service.provider.apiGateway = { minimumCompressionSize: 10485761, }; - expect(awsCompileApigEvents.compileRestApi()).to.be.rejectedWith(Error); + return expect(awsCompileApigEvents.compileRestApi()).to.be.rejectedWith(Error); }); }); diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/stage.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/stage.js index 3fb978548..4ac905e68 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/stage.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/stage.js @@ -1,8 +1,7 @@ -/* eslint-disable no-use-before-define */ -/* eslint-disable max-len */ - 'use strict'; +/* eslint-disable max-len */ + // NOTE: --> Keep this file in sync with ./hack/updateStage.js const _ = require('lodash'); @@ -33,15 +32,11 @@ module.exports = { // --- currently commented out since this is done via the SDK in updateStage.js --- // const deploymentId = this.apiGatewayDeploymentLogicalId; // -------------------------------------------------------------------------------- - const logGroupLogicalId = this.provider.naming - .getApiGatewayLogGroupLogicalId(); - const logsRoleLogicalId = this.provider.naming - .getApiGatewayLogsRoleLogicalId(); - const accountLogicalid = this.provider.naming - .getApiGatewayAccountLogicalId(); + const logGroupLogicalId = this.provider.naming.getApiGatewayLogGroupLogicalId(); + const logsRoleLogicalId = this.provider.naming.getApiGatewayLogsRoleLogicalId(); + const accountLogicalid = this.provider.naming.getApiGatewayAccountLogicalId(); - this.apiGatewayStageLogicalId = this.provider.naming - .getStageLogicalId(); + this.apiGatewayStageLogicalId = this.provider.naming.getStageLogicalId(); // NOTE: right now we're only using a dedicated Stage resource // - if AWS X-Ray tracing is enabled @@ -110,16 +105,16 @@ module.exports = { }; function getLogGroupResource(service, stage) { - return ({ + return { Type: 'AWS::Logs::LogGroup', Properties: { LogGroupName: `/aws/api-gateway/${service}-${stage}`, }, - }); + }; } function getIamRoleResource(service, stage) { - return ({ + return { Type: 'AWS::IAM::Role', Properties: { AssumeRolePolicyDocument: { @@ -128,13 +123,9 @@ function getIamRoleResource(service, stage) { { Effect: 'Allow', Principal: { - Service: [ - 'apigateway.amazonaws.com', - ], + Service: ['apigateway.amazonaws.com'], }, - Action: [ - 'sts:AssumeRole', - ], + Action: ['sts:AssumeRole'], }, ], }, @@ -156,19 +147,16 @@ function getIamRoleResource(service, stage) { ], }, }, - }); + }; } function getAccountResource(logsRoleLogicalId) { - return ({ + return { Type: 'AWS::ApiGateway::Account', Properties: { CloudWatchRoleArn: { - 'Fn::GetAtt': [ - logsRoleLogicalId, - 'Arn', - ], + 'Fn::GetAtt': [logsRoleLogicalId, 'Arn'], }, }, - }); + }; } diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/stage.test.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/stage.test.js index 229ddefa7..deaac6988 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/stage.test.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/stage.test.js @@ -33,22 +33,19 @@ describe('#compileStage()', () => { awsCompileApigEvents.apiGatewayDeploymentLogicalId = 'ApiGatewayDeploymentTest'; awsCompileApigEvents.provider = provider; stage = awsCompileApigEvents.provider.getStage(); - stageLogicalId = awsCompileApigEvents.provider.naming - .getStageLogicalId(); - accountLogicalid = awsCompileApigEvents.provider.naming - .getApiGatewayAccountLogicalId(); - logsRoleLogicalId = awsCompileApigEvents.provider.naming - .getApiGatewayLogsRoleLogicalId(); - logGroupLogicalId = awsCompileApigEvents.provider.naming - .getApiGatewayLogGroupLogicalId(); + stageLogicalId = awsCompileApigEvents.provider.naming.getStageLogicalId(); + accountLogicalid = awsCompileApigEvents.provider.naming.getApiGatewayAccountLogicalId(); + logsRoleLogicalId = awsCompileApigEvents.provider.naming.getApiGatewayLogsRoleLogicalId(); + logGroupLogicalId = awsCompileApigEvents.provider.naming.getApiGatewayLogGroupLogicalId(); // mocking the result of a Deployment resource since we remove the stage name // when using the Stage resource - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources[awsCompileApigEvents.apiGatewayDeploymentLogicalId] = { - Properties: { - StageName: stage, - }, - }; + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + awsCompileApigEvents.apiGatewayDeploymentLogicalId + ] = { + Properties: { + StageName: stage, + }, + }; }); describe('tracing', () => { @@ -61,8 +58,8 @@ describe('#compileStage()', () => { it.skip('should create a dedicated stage resource if tracing is configured', () => awsCompileApigEvents.compileStage().then(() => { - const resources = awsCompileApigEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources; + const resources = + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources; expect(resources[stageLogicalId]).to.deep.equal({ Type: 'AWS::ApiGateway::Stage', @@ -82,15 +79,14 @@ describe('#compileStage()', () => { expect(resources[awsCompileApigEvents.apiGatewayDeploymentLogicalId]).to.deep.equal({ Properties: {}, }); - }) - ); + })); it('should NOT create a dedicated stage resource if tracing is not enabled', () => { awsCompileApigEvents.serverless.service.provider.tracing = {}; return awsCompileApigEvents.compileStage().then(() => { - const resources = awsCompileApigEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources; + const resources = + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources; // eslint-disable-next-line expect(resources[stageLogicalId]).not.to.exist; @@ -111,8 +107,8 @@ describe('#compileStage()', () => { }; awsCompileApigEvents.compileStage().then(() => { - const resources = awsCompileApigEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources; + const resources = + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources; expect(resources[awsCompileApigEvents.apiGatewayDeploymentLogicalId]).to.deep.equal({ Properties: {}, }); @@ -128,9 +124,7 @@ describe('#compileStage()', () => { }, StageName: stage, TracingEnabled: false, - Tags: [ - { Key: 'foo', Value: '1' }, - ], + Tags: [{ Key: 'foo', Value: '1' }], }, }); }); @@ -142,8 +136,8 @@ describe('#compileStage()', () => { }; awsCompileApigEvents.compileStage().then(() => { - const resources = awsCompileApigEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources; + const resources = + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources; expect(resources[awsCompileApigEvents.apiGatewayDeploymentLogicalId]).to.deep.equal({ Properties: {}, }); @@ -159,9 +153,7 @@ describe('#compileStage()', () => { }, StageName: stage, TracingEnabled: false, - Tags: [ - { Key: 'foo', Value: '1' }, - ], + Tags: [{ Key: 'foo', Value: '1' }], }, }); }); @@ -178,8 +170,8 @@ describe('#compileStage()', () => { }; awsCompileApigEvents.compileStage().then(() => { - const resources = awsCompileApigEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources; + const resources = + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources; expect(resources[stageLogicalId]).to.deep.equal({ Type: 'AWS::ApiGateway::Stage', @@ -213,8 +205,8 @@ describe('#compileStage()', () => { it.skip('should create a dedicated stage resource if logs are configured', () => awsCompileApigEvents.compileStage().then(() => { - const resources = awsCompileApigEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources; + const resources = + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources; expect(resources[stageLogicalId]).to.deep.equal({ Type: 'AWS::ApiGateway::Stage', @@ -238,13 +230,11 @@ describe('#compileStage()', () => { ], AccessLogSetting: { DestinationArn: { - 'Fn::GetAtt': [ - logGroupLogicalId, - 'Arn', - ], + 'Fn::GetAtt': [logGroupLogicalId, 'Arn'], }, // eslint-disable-next-line - Format: 'requestId: $context.requestId, ip: $context.identity.sourceIp, caller: $context.identity.caller, user: $context.identity.user, requestTime: $context.requestTime, httpMethod: $context.httpMethod, resourcePath: $context.resourcePath, status: $context.status, protocol: $context.protocol, responseLength: $context.responseLength', + Format: + 'requestId: $context.requestId, ip: $context.identity.sourceIp, caller: $context.identity.caller, user: $context.identity.user, requestTime: $context.requestTime, httpMethod: $context.httpMethod, resourcePath: $context.resourcePath, status: $context.status, protocol: $context.protocol, responseLength: $context.responseLength', }, }, }); @@ -256,8 +246,8 @@ describe('#compileStage()', () => { it('should create a Log Group resource', () => awsCompileApigEvents.compileStage().then(() => { - const resources = awsCompileApigEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources; + const resources = + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources; expect(resources[logGroupLogicalId]).to.deep.equal({ Type: 'AWS::Logs::LogGroup', @@ -269,8 +259,8 @@ describe('#compileStage()', () => { it('should create a IAM Role resource', () => awsCompileApigEvents.compileStage().then(() => { - const resources = awsCompileApigEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources; + const resources = + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources; expect(resources[logsRoleLogicalId]).to.deep.equal({ Type: 'AWS::IAM::Role', @@ -278,14 +268,10 @@ describe('#compileStage()', () => { AssumeRolePolicyDocument: { Statement: [ { - Action: [ - 'sts:AssumeRole', - ], + Action: ['sts:AssumeRole'], Effect: 'Allow', Principal: { - Service: [ - 'apigateway.amazonaws.com', - ], + Service: ['apigateway.amazonaws.com'], }, }, ], @@ -314,17 +300,14 @@ describe('#compileStage()', () => { it('should create an Account resource', () => awsCompileApigEvents.compileStage().then(() => { - const resources = awsCompileApigEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources; + const resources = + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources; expect(resources[accountLogicalid]).to.deep.equal({ Type: 'AWS::ApiGateway::Account', Properties: { CloudWatchRoleArn: { - 'Fn::GetAtt': [ - logsRoleLogicalId, - 'Arn', - ], + 'Fn::GetAtt': [logsRoleLogicalId, 'Arn'], }, }, }); diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/usagePlan.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/usagePlan.js index 861e8b78c..3a8990e3e 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/usagePlan.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/usagePlan.js @@ -14,76 +14,90 @@ function createUsagePlanResource(that, name) { Stage: that.provider.getStage(), }, ], - Description: `Usage plan "${name}" for ${that.serverless.service.service} ${ - that.provider.getStage()} stage`, - UsagePlanName: `${that.serverless.service.service}-${name}-${ - that.provider.getStage()}`, + Description: `Usage plan "${name}" for ${ + that.serverless.service.service + } ${that.provider.getStage()} stage`, + UsagePlanName: `${that.serverless.service.service}-${name}-${that.provider.getStage()}`, }, }; const template = _.cloneDeep(resourceTemplate); // this is done for backward compatibility if (name === 'default') { // create old legacy resources - template.Properties.UsagePlanName = - `${that.serverless.service.service}-${that.provider.getStage()}`; - template.Properties.Description = - `Usage plan for ${that.serverless.service.service} ${that.provider.getStage()} stage`; + template.Properties.UsagePlanName = `${ + that.serverless.service.service + }-${that.provider.getStage()}`; + template.Properties.Description = `Usage plan for ${ + that.serverless.service.service + } ${that.provider.getStage()} stage`; // assign quota - if (_.has(that.serverless.service.provider, 'usagePlan.quota') - && that.serverless.service.provider.usagePlan.quota !== null) { + if ( + _.has(that.serverless.service.provider, 'usagePlan.quota') && + that.serverless.service.provider.usagePlan.quota !== null + ) { _.merge(template, { Properties: { Quota: _.merge( { Limit: that.serverless.service.provider.usagePlan.quota.limit }, { Offset: that.serverless.service.provider.usagePlan.quota.offset }, - { Period: that.serverless.service.provider.usagePlan.quota.period }), + { Period: that.serverless.service.provider.usagePlan.quota.period } + ), }, }); } // assign throttle - if (_.has(that.serverless.service.provider, 'usagePlan.throttle') - && that.serverless.service.provider.usagePlan.throttle !== null) { + if ( + _.has(that.serverless.service.provider, 'usagePlan.throttle') && + that.serverless.service.provider.usagePlan.throttle !== null + ) { _.merge(template, { Properties: { Throttle: _.merge( { BurstLimit: that.serverless.service.provider.usagePlan.throttle.burstLimit }, - { RateLimit: that.serverless.service.provider.usagePlan.throttle.rateLimit }), + { RateLimit: that.serverless.service.provider.usagePlan.throttle.rateLimit } + ), }, }); } } else { // assign quota - const quotaProperties = that.serverless.service.provider.usagePlan - .reduce((accum, planObject) => { + const quotaProperties = that.serverless.service.provider.usagePlan.reduce( + (accum, planObject) => { if (planObject[name] && planObject[name].quota) { return planObject[name].quota; } return accum; - }, {}); + }, + {} + ); if (!_.isEmpty(quotaProperties)) { _.merge(template, { Properties: { Quota: _.merge( { Limit: quotaProperties.limit }, { Offset: quotaProperties.offset }, - { Period: quotaProperties.period }), + { Period: quotaProperties.period } + ), }, }); } // assign throttle - const throttleProperties = that.serverless.service.provider.usagePlan - .reduce((accum, planObject) => { + const throttleProperties = that.serverless.service.provider.usagePlan.reduce( + (accum, planObject) => { if (planObject[name] && planObject[name].throttle) { return planObject[name].throttle; } return accum; - }, {}); + }, + {} + ); if (!_.isEmpty(throttleProperties)) { _.merge(template, { Properties: { Throttle: _.merge( { BurstLimit: throttleProperties.burstLimit }, - { RateLimit: throttleProperties.rateLimit }), + { RateLimit: throttleProperties.rateLimit } + ), }, }); } @@ -98,7 +112,7 @@ module.exports = { this.apiGatewayUsagePlanNames = []; if (_.isArray(this.serverless.service.provider.usagePlan)) { - _.forEach(this.serverless.service.provider.usagePlan, (planObject) => { + _.forEach(this.serverless.service.provider.usagePlan, planObject => { const usagePlanName = Object.keys(planObject)[0]; const logicalId = this.provider.naming.getUsagePlanLogicalId(usagePlanName); const resourceTemplate = createUsagePlanResource(this, usagePlanName); diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/usagePlan.test.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/usagePlan.test.js index b46263c13..53caa3cec 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/usagePlan.test.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/usagePlan.test.js @@ -30,40 +30,34 @@ describe('#compileUsagePlan()', () => { serverless.service.provider.apiKeys = ['1234567890']; return awsCompileApigEvents.compileUsagePlan().then(() => { expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources[ + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources[ awsCompileApigEvents.provider.naming.getUsagePlanLogicalId() - ].Type + ].Type ).to.equal('AWS::ApiGateway::UsagePlan'); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources[ + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources[ awsCompileApigEvents.provider.naming.getUsagePlanLogicalId() - ].DependsOn + ].DependsOn ).to.equal('ApiGatewayDeploymentTest'); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources[ + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources[ awsCompileApigEvents.provider.naming.getUsagePlanLogicalId() - ].Properties.ApiStages[0].ApiId.Ref + ].Properties.ApiStages[0].ApiId.Ref ).to.equal('ApiGatewayRestApi'); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources[ + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources[ awsCompileApigEvents.provider.naming.getUsagePlanLogicalId() - ].Properties.ApiStages[0].Stage + ].Properties.ApiStages[0].Stage ).to.equal('dev'); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources[ + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources[ awsCompileApigEvents.provider.naming.getUsagePlanLogicalId() - ].Properties.Description + ].Properties.Description ).to.equal('Usage plan for first-service dev stage'); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources[ + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources[ awsCompileApigEvents.provider.naming.getUsagePlanLogicalId() - ].Properties.UsagePlanName + ].Properties.UsagePlanName ).to.equal('first-service-dev'); expect(awsCompileApigEvents.apiGatewayUsagePlanNames).to.deep.equal(['default']); @@ -87,43 +81,51 @@ describe('#compileUsagePlan()', () => { const logicalId = awsCompileApigEvents.provider.naming.getUsagePlanLogicalId(); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources[logicalId].Type + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + logicalId + ].Type ).to.equal('AWS::ApiGateway::UsagePlan'); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources[logicalId].DependsOn + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + logicalId + ].DependsOn ).to.equal('ApiGatewayDeploymentTest'); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources[logicalId].Properties.ApiStages[0].ApiId.Ref + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + logicalId + ].Properties.ApiStages[0].ApiId.Ref ).to.equal('ApiGatewayRestApi'); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources[logicalId].Properties.ApiStages[0].Stage + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + logicalId + ].Properties.ApiStages[0].Stage ).to.equal('dev'); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources[logicalId].Properties.Description + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + logicalId + ].Properties.Description ).to.equal('Usage plan for first-service dev stage'); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources[logicalId].Properties.Quota + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + logicalId + ].Properties.Quota ).to.deep.equal({ Limit: 500, Offset: 10, Period: 'MONTH', }); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources[logicalId].Properties.Throttle + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + logicalId + ].Properties.Throttle ).to.deep.equal({ BurstLimit: 200, RateLimit: 100, }); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources[logicalId].Properties.UsagePlanName + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + logicalId + ].Properties.UsagePlanName ).to.equal('first-service-dev'); expect(awsCompileApigEvents.apiGatewayUsagePlanNames).to.deep.equal(['default']); @@ -168,88 +170,105 @@ describe('#compileUsagePlan()', () => { return awsCompileApigEvents.compileUsagePlan().then(() => { // resources for the "free" plan expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources[logicalIdFree].Type + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + logicalIdFree + ].Type ).to.equal('AWS::ApiGateway::UsagePlan'); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources[logicalIdFree].DependsOn + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + logicalIdFree + ].DependsOn ).to.equal('ApiGatewayDeploymentTest'); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources[logicalIdFree].Properties.ApiStages[0].ApiId.Ref + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + logicalIdFree + ].Properties.ApiStages[0].ApiId.Ref ).to.equal('ApiGatewayRestApi'); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources[logicalIdFree].Properties.ApiStages[0].Stage + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + logicalIdFree + ].Properties.ApiStages[0].Stage ).to.equal('dev'); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources[logicalIdFree].Properties.Description + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + logicalIdFree + ].Properties.Description ).to.equal(`Usage plan "${freePlanName}" for first-service dev stage`); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources[logicalIdFree].Properties.Quota + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + logicalIdFree + ].Properties.Quota ).to.deep.equal({ Limit: 1000, Offset: 100, Period: 'MONTH', }); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources[logicalIdFree].Properties.Throttle + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + logicalIdFree + ].Properties.Throttle ).to.deep.equal({ BurstLimit: 1, RateLimit: 1, }); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources[logicalIdFree].Properties.UsagePlanName + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + logicalIdFree + ].Properties.UsagePlanName ).to.equal(`first-service-${freePlanName}-dev`); // resources for the "paid" plan expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources[logicalIdPaid].Type + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + logicalIdPaid + ].Type ).to.equal('AWS::ApiGateway::UsagePlan'); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources[logicalIdPaid].DependsOn + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + logicalIdPaid + ].DependsOn ).to.equal('ApiGatewayDeploymentTest'); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources[logicalIdPaid].Properties.ApiStages[0].ApiId.Ref + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + logicalIdPaid + ].Properties.ApiStages[0].ApiId.Ref ).to.equal('ApiGatewayRestApi'); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources[logicalIdPaid].Properties.ApiStages[0].Stage + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + logicalIdPaid + ].Properties.ApiStages[0].Stage ).to.equal('dev'); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources[logicalIdPaid].Properties.Description + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + logicalIdPaid + ].Properties.Description ).to.equal(`Usage plan "${paidPlanName}" for first-service dev stage`); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources[logicalIdPaid].Properties.Quota + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + logicalIdPaid + ].Properties.Quota ).to.deep.equal({ Limit: 1000000, Offset: 200, Period: 'MONTH', }); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources[logicalIdPaid].Properties.Throttle + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + logicalIdPaid + ].Properties.Throttle ).to.deep.equal({ BurstLimit: 1000, RateLimit: 1000, }); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources[logicalIdPaid].Properties.UsagePlanName + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + logicalIdPaid + ].Properties.UsagePlanName ).to.equal(`first-service-${paidPlanName}-dev`); expect(awsCompileApigEvents.apiGatewayUsagePlanNames).to.deep.equal([ - freePlanName, paidPlanName, + freePlanName, + paidPlanName, ]); }); }); @@ -262,10 +281,9 @@ describe('#compileUsagePlan()', () => { return awsCompileApigEvents.compileUsagePlan().then(() => { expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources[ + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources[ awsCompileApigEvents.provider.naming.getUsagePlanLogicalId() - ].Properties.ApiStages[0].ApiId + ].Properties.ApiStages[0].ApiId ).to.equal('xxxxx'); }); }); diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/usagePlanKeys.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/usagePlanKeys.js index 9d395da70..3ed7a8953 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/usagePlanKeys.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/usagePlanKeys.js @@ -33,29 +33,38 @@ module.exports = { const resources = this.serverless.service.provider.compiledCloudFormationTemplate.Resources; let keyNumber = 0; - _.forEach(this.serverless.service.provider.apiKeys, (apiKeyDefinition) => { + _.forEach(this.serverless.service.provider.apiKeys, apiKeyDefinition => { // if multiple API key types are used const apiKey = _.first(_.entries(apiKeyDefinition)); const name = _.first(apiKey); const value = _.last(apiKey); - if (this.apiGatewayUsagePlanNames.length > 0 && - !_.includes(this.apiGatewayUsagePlanNames, name) && _.isObject(value)) { + if ( + this.apiGatewayUsagePlanNames.length > 0 && + !_.includes(this.apiGatewayUsagePlanNames, name) && + _.isObject(value) + ) { throw new this.serverless.classes.Error(`API key "${name}" has no usage plan defined`); } if (_.isObject(apiKeyDefinition) && _.includes(this.apiGatewayUsagePlanNames, name)) { keyNumber = 0; - _.forEach(apiKeyDefinition[name], (key) => { + _.forEach(apiKeyDefinition[name], key => { if (!apiKeys.validateApiKeyInput(key)) { throw new this.serverless.classes.Error( 'API Key must be a string or an object which contains name and/or value' ); } keyNumber += 1; - const usagePlanKeyLogicalId = this.provider.naming - .getUsagePlanKeyLogicalId(keyNumber, name); + const usagePlanKeyLogicalId = this.provider.naming.getUsagePlanKeyLogicalId( + keyNumber, + name + ); const usagePlanLogicalId = this.provider.naming.getUsagePlanLogicalId(name); - const resourceTemplate = - createUsagePlanKeyResource(this, usagePlanLogicalId, keyNumber, name); + const resourceTemplate = createUsagePlanKeyResource( + this, + usagePlanLogicalId, + keyNumber, + name + ); _.merge(resources, { [usagePlanKeyLogicalId]: resourceTemplate, }); diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/usagePlanKeys.test.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/usagePlanKeys.test.js index 0949467c0..80ada8ea5 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/usagePlanKeys.test.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/usagePlanKeys.test.js @@ -28,8 +28,7 @@ describe('#compileUsagePlanKeys()', () => { }); it('should support api key notation', () => { - const defaultUsagePlanLogicalId = awsCompileApigEvents - .provider.naming.getUsagePlanLogicalId(); + const defaultUsagePlanLogicalId = awsCompileApigEvents.provider.naming.getUsagePlanLogicalId(); awsCompileApigEvents.apiGatewayUsagePlanNames = ['default']; awsCompileApigEvents.serverless.service.provider.apiKeys = [ '1234567890', @@ -39,54 +38,46 @@ describe('#compileUsagePlanKeys()', () => { return awsCompileApigEvents.compileUsagePlanKeys().then(() => { // key 1 expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources[ + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources[ awsCompileApigEvents.provider.naming.getUsagePlanKeyLogicalId(1) - ].Type + ].Type ).to.equal('AWS::ApiGateway::UsagePlanKey'); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources[ + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources[ awsCompileApigEvents.provider.naming.getUsagePlanKeyLogicalId(1) - ].Properties.KeyId.Ref + ].Properties.KeyId.Ref ).to.equal('ApiGatewayApiKey1'); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources[ + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources[ awsCompileApigEvents.provider.naming.getUsagePlanKeyLogicalId(1) - ].Properties.KeyType + ].Properties.KeyType ).to.equal('API_KEY'); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources[ + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources[ awsCompileApigEvents.provider.naming.getUsagePlanKeyLogicalId(1) - ].Properties.UsagePlanId.Ref + ].Properties.UsagePlanId.Ref ).to.equal(defaultUsagePlanLogicalId); // key 2 expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources[ + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources[ awsCompileApigEvents.provider.naming.getUsagePlanKeyLogicalId(2) - ].Type + ].Type ).to.equal('AWS::ApiGateway::UsagePlanKey'); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources[ + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources[ awsCompileApigEvents.provider.naming.getUsagePlanKeyLogicalId(2) - ].Properties.KeyId.Ref + ].Properties.KeyId.Ref ).to.equal('ApiGatewayApiKey2'); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources[ + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources[ awsCompileApigEvents.provider.naming.getUsagePlanKeyLogicalId(2) - ].Properties.KeyType + ].Properties.KeyType ).to.equal('API_KEY'); expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources[ + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources[ awsCompileApigEvents.provider.naming.getUsagePlanKeyLogicalId(2) - ].Properties.UsagePlanId.Ref + ].Properties.UsagePlanId.Ref ).to.equal(defaultUsagePlanLogicalId); }); }); @@ -96,10 +87,8 @@ describe('#compileUsagePlanKeys()', () => { const freeUsagePlanName = 'free'; const paidUsagePlanName = 'paid'; const logicalIds = { - free: awsCompileApigEvents - .provider.naming.getUsagePlanLogicalId(freeUsagePlanName), - paid: awsCompileApigEvents - .provider.naming.getUsagePlanLogicalId(paidUsagePlanName), + free: awsCompileApigEvents.provider.naming.getUsagePlanLogicalId(freeUsagePlanName), + paid: awsCompileApigEvents.provider.naming.getUsagePlanLogicalId(paidUsagePlanName), }; awsCompileApigEvents.apiGatewayUsagePlanNames = [freeUsagePlanName, paidUsagePlanName]; awsCompileApigEvents.serverless.service.provider.apiKeys = [ @@ -108,37 +97,33 @@ describe('#compileUsagePlanKeys()', () => { ]; return awsCompileApigEvents.compileUsagePlanKeys().then(() => { - _.forEach(awsCompileApigEvents.serverless.service.provider.apiKeys, (plan) => { + _.forEach(awsCompileApigEvents.serverless.service.provider.apiKeys, plan => { const planName = _.first(_.keys(plan)); // free || paid const apiKeys = plan[planName]; _.forEach(apiKeys, (apiKey, index) => { expect( awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate .Resources[ - awsCompileApigEvents.provider - .naming.getUsagePlanKeyLogicalId(index + 1, planName) - ].Type + awsCompileApigEvents.provider.naming.getUsagePlanKeyLogicalId(index + 1, planName) + ].Type ).to.equal('AWS::ApiGateway::UsagePlanKey'); expect( awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate .Resources[ - awsCompileApigEvents.provider - .naming.getUsagePlanKeyLogicalId(index + 1, planName) - ].Properties.KeyId.Ref + awsCompileApigEvents.provider.naming.getUsagePlanKeyLogicalId(index + 1, planName) + ].Properties.KeyId.Ref ).to.equal(`ApiGatewayApiKey${_.capitalize(planName)}${index + 1}`); expect( awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate .Resources[ - awsCompileApigEvents.provider - .naming.getUsagePlanKeyLogicalId(index + 1, planName) - ].Properties.KeyType + awsCompileApigEvents.provider.naming.getUsagePlanKeyLogicalId(index + 1, planName) + ].Properties.KeyType ).to.equal('API_KEY'); expect( awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate .Resources[ - awsCompileApigEvents.provider - .naming.getUsagePlanKeyLogicalId(index + 1, planName) - ].Properties.UsagePlanId.Ref + awsCompileApigEvents.provider.naming.getUsagePlanKeyLogicalId(index + 1, planName) + ].Properties.UsagePlanId.Ref ).to.equal(logicalIds[planName]); }); }); @@ -147,20 +132,18 @@ describe('#compileUsagePlanKeys()', () => { it('should throw if api key name does not match a usage plan', () => { awsCompileApigEvents.apiGatewayUsagePlanNames = ['default']; - awsCompileApigEvents.serverless.service.provider.apiKeys = [ - { free: ['1234567890'] }, - ]; - expect(() => awsCompileApigEvents.compileUsagePlanKeys()) - .to.throw(/has no usage plan defined/); + awsCompileApigEvents.serverless.service.provider.apiKeys = [{ free: ['1234567890'] }]; + expect(() => awsCompileApigEvents.compileUsagePlanKeys()).to.throw( + /has no usage plan defined/ + ); }); it('should throw if api key definitions are not strings or objects', () => { awsCompileApigEvents.apiGatewayUsagePlanNames = ['free']; - awsCompileApigEvents.serverless.service.provider.apiKeys = [ - { free: [{ foo: 'bar' }] }, - ]; - expect(() => awsCompileApigEvents.compileUsagePlanKeys()) - .to.throw(/must be a string or an object/); + awsCompileApigEvents.serverless.service.provider.apiKeys = [{ free: [{ foo: 'bar' }] }]; + expect(() => awsCompileApigEvents.compileUsagePlanKeys()).to.throw( + /must be a string or an object/ + ); }); }); }); 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 9fde97c46..d242924c2 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/validate.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/validate.js @@ -30,7 +30,7 @@ const DEFAULT_STATUS_CODES = { pattern: '[\\s\\S]*\\[502\\][\\s\\S]*', }, 504: { - pattern: '([\\s\\S]*\\[504\\][\\s\\S]*)|(^[Task timed out].*)', + pattern: '([\\s\\S]*\\[504\\][\\s\\S]*)|(.*Task timed out after \\d+\\.\\d+ seconds$)', }, }; @@ -40,7 +40,7 @@ module.exports = { const corsPreflight = {}; _.forEach(this.serverless.service.functions, (functionObject, functionName) => { - _.forEach(functionObject.events, (event) => { + _.forEach(functionObject.events, event => { if (_.has(event, 'http')) { const http = this.getHttp(event, functionName); @@ -76,8 +76,10 @@ module.exports = { http.integration = this.getIntegration(http, functionName); - if ((http.integration === 'HTTP' || http.integration === 'HTTP_PROXY') && - (!http.request || !http.request.uri)) { + if ( + (http.integration === 'HTTP' || http.integration === 'HTTP_PROXY') && + (!http.request || !http.request.uri) + ) { const errorMessage = [ `You need to set the request uri when using the ${http.integration} integration.`, ]; @@ -189,9 +191,7 @@ module.exports = { if (typeof http.method === 'string') { const method = http.method.toLowerCase(); - const allowedMethods = [ - 'get', 'post', 'put', 'patch', 'options', 'head', 'delete', 'any', - ]; + const allowedMethods = ['get', 'post', 'put', 'patch', 'options', 'head', 'delete', 'any']; if (allowedMethods.indexOf(method) === -1) { const errorMessage = [ `Invalid APIG method "${http.method}" in function "${functionName}".`, @@ -280,10 +280,13 @@ module.exports = { } const integration = this.getIntegration(http); - if (integration === 'AWS_PROXY' - && typeof arn === 'string' - && awsArnRegExs.cognitoIdpArnExpr.test(arn) - && authorizer.claims) { + if ( + integration === 'AWS_PROXY' && + typeof arn === 'string' && + awsArnRegExs.cognitoIdpArnExpr.test(arn) && + claims && + claims.length > 0 + ) { const errorMessage = [ 'Cognito claims can only be filtered when using the lambda integration type', ]; @@ -371,7 +374,13 @@ module.exports = { // normalize the integration for further processing const normalizedIntegration = http.integration.toUpperCase().replace('-', '_'); const allowedIntegrations = [ - 'LAMBDA_PROXY', 'LAMBDA', 'AWS', 'AWS_PROXY', 'HTTP', 'HTTP_PROXY', 'MOCK', + 'LAMBDA_PROXY', + 'LAMBDA', + 'AWS', + 'AWS_PROXY', + 'HTTP', + 'HTTP_PROXY', + 'MOCK', ]; // check if the user has entered a non-valid integration if (allowedIntegrations.indexOf(normalizedIntegration) === NOT_FOUND) { @@ -431,7 +440,7 @@ module.exports = { const parameters = {}; // only these locations are currently supported const locations = ['querystrings', 'paths', 'headers']; - _.each(locations, (location) => { + _.each(locations, location => { // strip the plural s const singular = location.substring(0, location.length - 1); _.each(httpRequest.parameters[location], (value, key) => { @@ -442,9 +451,7 @@ module.exports = { }, getRequestPassThrough(http) { - const requestPassThroughBehaviors = [ - 'NEVER', 'WHEN_NO_MATCH', 'WHEN_NO_TEMPLATES', - ]; + const requestPassThroughBehaviors = ['NEVER', 'WHEN_NO_MATCH', 'WHEN_NO_TEMPLATES']; if (http.request.passThrough) { if (requestPassThroughBehaviors.indexOf(http.request.passThrough) === -1) { 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 e8fb97929..a01f5b033 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 @@ -31,7 +31,9 @@ describe('#validate()', () => { }, }; const validated = awsCompileApigEvents.validate(); - expect(validated.events).to.be.an('Array').with.length(0); + expect(validated.events) + .to.be.an('Array') + .with.length(0); }); it('should reject an invalid http event', () => { @@ -61,7 +63,7 @@ describe('#validate()', () => { expect(() => awsCompileApigEvents.validate()).to.throw(Error); }); - it('should throw a helpful error if http event type object doesn\'t have a path property', () => { + it("should throw a helpful error if http event type object doesn't have a path property", () => { /** * This can happen with surprising subtle syntax error such as when path is not * indented under http in yml. @@ -76,8 +78,9 @@ describe('#validate()', () => { }, }; - expect(() => awsCompileApigEvents.validate()).to - .throw(/invalid "path" property in function "first"/); + expect(() => awsCompileApigEvents.validate()).to.throw( + /invalid "path" property in function "first"/ + ); }); it('should validate the http events "path" property', () => { @@ -133,7 +136,9 @@ describe('#validate()', () => { }; const validated = awsCompileApigEvents.validate(); - expect(validated.events).to.be.an('Array').with.length(2); + expect(validated.events) + .to.be.an('Array') + .with.length(2); }); it('should validate the http events string syntax method is case insensitive', () => { @@ -151,7 +156,9 @@ describe('#validate()', () => { }; const validated = awsCompileApigEvents.validate(); - expect(validated.events).to.be.an('Array').with.length(2); + expect(validated.events) + .to.be.an('Array') + .with.length(2); }); it('should throw an error if the method is invalid', () => { @@ -194,7 +201,9 @@ describe('#validate()', () => { }; const validated = awsCompileApigEvents.validate(); - expect(validated.events).to.be.an('Array').with.length(1); + expect(validated.events) + .to.be.an('Array') + .with.length(1); }); it('should discard a starting slash from paths', () => { @@ -214,7 +223,9 @@ describe('#validate()', () => { }, }; const validated = awsCompileApigEvents.validate(); - expect(validated.events).to.be.an('Array').with.length(2); + expect(validated.events) + .to.be.an('Array') + .with.length(2); expect(validated.events[0].http).to.have.property('path', 'foo/bar'); expect(validated.events[1].http).to.have.property('path', 'foo/bar'); }); @@ -266,10 +277,7 @@ describe('#validate()', () => { integration: 'lambda-proxy', authorizer: { arn: 'arn:aws:cognito-idp:us-east-1:xxx:userpool/us-east-1_ZZZ', - claims: [ - 'email', - 'nickname', - ], + claims: ['email', 'nickname'], }, }, }, @@ -280,6 +288,70 @@ describe('#validate()', () => { expect(() => awsCompileApigEvents.validate()).to.throw(Error); }); + it('should not throw if an cognito claims are undefined with a lambda proxy', () => { + awsCompileApigEvents.serverless.service.functions = { + first: { + events: [ + { + http: { + path: '/{proxy+}', + method: 'ANY', + integration: 'lambda-proxy', + authorizer: { + arn: 'arn:aws:cognito-idp:us-east-1:xxx:userpool/us-east-1_ZZZ', + name: 'CognitoAuthorier', + }, + }, + }, + ], + }, + }; + + expect(() => awsCompileApigEvents.validate()).not.to.throw(Error); + }); + + it('should not throw if an cognito claims are empty arrays with a lambda proxy', () => { + awsCompileApigEvents.serverless.service.functions = { + first: { + events: [ + { + http: { + path: '/{proxy+}', + method: 'ANY', + integration: 'lambda-proxy', + authorizer: { + arn: 'arn:aws:cognito-idp:us-east-1:xxx:userpool/us-east-1_ZZZ', + name: 'CognitoAuthorier', + claims: [], + }, + }, + }, + ], + }, + }; + + expect(() => awsCompileApigEvents.validate()).not.to.throw(Error); + }); + + it('should not throw when using a cognito string authorizer', () => { + awsCompileApigEvents.serverless.service.functions = { + first: { + events: [ + { + http: { + path: '/{proxy+}', + method: 'ANY', + integration: 'lambda-proxy', + authorizer: 'arn:aws:cognito-idp:us-east-1:$XXXXX:userpool/some-user-pool', + }, + }, + ], + }, + }; + + expect(() => awsCompileApigEvents.validate()).not.to.throw(Error); + }); + it('should accept AWS_IAM as authorizer', () => { awsCompileApigEvents.serverless.service.functions = { foo: {}, @@ -310,7 +382,9 @@ describe('#validate()', () => { }; const validated = awsCompileApigEvents.validate(); - expect(validated.events).to.be.an('Array').with.length(2); + expect(validated.events) + .to.be.an('Array') + .with.length(2); expect(validated.events[0].http.authorizer.type).to.equal('AWS_IAM'); expect(validated.events[1].http.authorizer.type).to.equal('AWS_IAM'); }); @@ -343,13 +417,12 @@ describe('#validate()', () => { }; const validated = awsCompileApigEvents.validate(); - expect(validated.events).to.be.an('Array').with.length(2); + expect(validated.events) + .to.be.an('Array') + .with.length(2); expect(validated.events[0].http.authorizer.name).to.equal('foo'); expect(validated.events[0].http.authorizer.arn).to.deep.equal({ - 'Fn::GetAtt': [ - 'FooLambdaFunction', - 'Arn', - ], + 'Fn::GetAtt': ['FooLambdaFunction', 'Arn'], }); expect(validated.events[1].http.authorizer.name).to.equal('authorizer'); expect(validated.events[1].http.authorizer.arn).to.equal('sss:dev-authorizer'); @@ -479,8 +552,7 @@ describe('#validate()', () => { }, }; - expect(() => awsCompileApigEvents.validate()) - .to.throw(Error, 'can only use'); + expect(() => awsCompileApigEvents.validate()).to.throw(Error, 'can only use'); }); it('should process cors defaults', () => { @@ -499,10 +571,18 @@ describe('#validate()', () => { }; const validated = awsCompileApigEvents.validate(); - expect(validated.events).to.be.an('Array').with.length(1); + expect(validated.events) + .to.be.an('Array') + .with.length(1); expect(validated.events[0].http.cors).to.deep.equal({ - headers: ['Content-Type', 'X-Amz-Date', 'Authorization', 'X-Api-Key', - 'X-Amz-Security-Token', 'X-Amz-User-Agent'], + headers: [ + 'Content-Type', + 'X-Amz-Date', + 'Authorization', + 'X-Api-Key', + 'X-Amz-Security-Token', + 'X-Amz-User-Agent', + ], methods: ['OPTIONS', 'POST'], origin: '*', origins: ['*'], @@ -653,7 +733,9 @@ describe('#validate()', () => { }; const validated = awsCompileApigEvents.validate(); - expect(validated.events).to.be.an('Array').with.length(1); + expect(validated.events) + .to.be.an('Array') + .with.length(1); expect(validated.events[0].http.cors).to.deep.equal({ headers: ['X-Foo-Bar'], methods: ['POST', 'OPTIONS'], @@ -673,43 +755,38 @@ describe('#validate()', () => { method: 'GET', path: 'users', cors: { - origins: [ - 'http://example.com', - ], + origins: ['http://example.com'], allowCredentials: true, maxAge: 10000, cacheControl: 'max-age=600, s-maxage=600, proxy-revalidate', }, }, - }, { + }, + { http: { method: 'POST', path: 'users', cors: { - origins: [ - 'http://example2.com', - ], + origins: ['http://example2.com'], maxAge: 86400, }, }, - }, { + }, + { http: { method: 'PUT', path: 'users/{id}', cors: { - headers: [ - 'TestHeader', - ], + headers: ['TestHeader'], }, }, - }, { + }, + { http: { method: 'DELETE', path: 'users/{id}', cors: { - headers: [ - 'TestHeader2', - ], + headers: ['TestHeader2'], }, }, }, @@ -718,20 +795,25 @@ describe('#validate()', () => { }; const validated = awsCompileApigEvents.validate(); - expect(validated.corsPreflight['users/{id}'].methods) - .to.deep.equal(['OPTIONS', 'DELETE', 'PUT']); - expect(validated.corsPreflight.users.origins) - .to.deep.equal(['http://example2.com', 'http://example.com']); - expect(validated.corsPreflight['users/{id}'].headers) - .to.deep.equal(['TestHeader2', 'TestHeader']); - expect(validated.corsPreflight.users.maxAge) - .to.equal(86400); - expect(validated.corsPreflight.users.cacheControl) - .to.equal('max-age=600, s-maxage=600, proxy-revalidate'); - expect(validated.corsPreflight.users.allowCredentials) - .to.equal(true); - expect(validated.corsPreflight['users/{id}'].allowCredentials) - .to.equal(false); + expect(validated.corsPreflight['users/{id}'].methods).to.deep.equal([ + 'OPTIONS', + 'DELETE', + 'PUT', + ]); + expect(validated.corsPreflight.users.origins).to.deep.equal([ + 'http://example2.com', + 'http://example.com', + ]); + expect(validated.corsPreflight['users/{id}'].headers).to.deep.equal([ + 'TestHeader2', + 'TestHeader', + ]); + expect(validated.corsPreflight.users.maxAge).to.equal(86400); + expect(validated.corsPreflight.users.cacheControl).to.equal( + 'max-age=600, s-maxage=600, proxy-revalidate' + ); + expect(validated.corsPreflight.users.allowCredentials).to.equal(true); + expect(validated.corsPreflight['users/{id}'].allowCredentials).to.equal(false); }); it('should throw an error if the maxAge is not a positive integer', () => { @@ -768,7 +850,7 @@ describe('#validate()', () => { statusCodes: { 404: { pattern: '.*"statusCode":404,.*', - template: '$input.path(\'$.errorMessage\')', + template: "$input.path('$.errorMessage')", headers: { 'Content-Type': 'text/html', }, @@ -782,14 +864,16 @@ describe('#validate()', () => { }; const validated = awsCompileApigEvents.validate(); - expect(validated.events).to.be.an('Array').with.length(1); + expect(validated.events) + .to.be.an('Array') + .with.length(1); expect(validated.events[0].http.response.statusCodes).to.deep.equal({ 200: { pattern: '', }, 404: { pattern: '.*"statusCode":404,.*', - template: '$input.path(\'$.errorMessage\')', + template: "$input.path('$.errorMessage')", headers: { 'Content-Type': 'text/html', }, @@ -810,7 +894,7 @@ describe('#validate()', () => { statusCodes: { 418: { pattern: '', - template: '$input.path(\'$.foo\')', + template: "$input.path('$.foo')", }, }, }, @@ -821,11 +905,13 @@ describe('#validate()', () => { }; const validated = awsCompileApigEvents.validate(); - expect(validated.events).to.be.an('Array').with.length(1); + expect(validated.events) + .to.be.an('Array') + .with.length(1); expect(validated.events[0].http.response.statusCodes).to.deep.equal({ 418: { pattern: '', - template: '$input.path(\'$.foo\')', + template: "$input.path('$.foo')", }, }); }); @@ -848,7 +934,9 @@ describe('#validate()', () => { }; const validated = awsCompileApigEvents.validate(); - expect(validated.events).to.be.an('Array').with.length(1); + expect(validated.events) + .to.be.an('Array') + .with.length(1); expect(validated.events[0].http.cors.methods).to.deep.equal(['POST', 'OPTIONS']); }); @@ -886,7 +974,9 @@ describe('#validate()', () => { }; const validated = awsCompileApigEvents.validate(); - expect(validated.events).to.be.an('Array').with.length(1); + expect(validated.events) + .to.be.an('Array') + .with.length(1); expect(validated.events[0].http.authorizer.name).to.equal('authorizer'); expect(validated.events[0].http.authorizer.arn).to.deep.equal({ 'Fn::GetAtt': ['AuthorizerLambdaFunction', 'Arn'], @@ -909,7 +999,9 @@ describe('#validate()', () => { }; const validated = awsCompileApigEvents.validate(); - expect(validated.events).to.be.an('Array').with.length(1); + expect(validated.events) + .to.be.an('Array') + .with.length(1); expect(validated.events[0].http.authorizer.name).to.equal('authorizer'); expect(validated.events[0].http.authorizer.arn).to.equal('xxx:dev-authorizer'); }); @@ -933,13 +1025,12 @@ describe('#validate()', () => { }; const validated = awsCompileApigEvents.validate(); - expect(validated.events).to.be.an('Array').with.length(1); + expect(validated.events) + .to.be.an('Array') + .with.length(1); expect(validated.events[0].http.authorizer.name).to.equal('authorizer'); expect(validated.events[0].http.authorizer.arn).to.deep.equal({ - 'Fn::GetAtt': [ - 'AuthorizerLambdaFunction', - 'Arn', - ], + 'Fn::GetAtt': ['AuthorizerLambdaFunction', 'Arn'], }); }); @@ -961,7 +1052,9 @@ describe('#validate()', () => { }; const validated = awsCompileApigEvents.validate(); - expect(validated.events).to.be.an('Array').with.length(1); + expect(validated.events) + .to.be.an('Array') + .with.length(1); expect(validated.events[0].http.authorizer.name).to.equal('authorizer'); expect(validated.events[0].http.authorizer.arn).to.equal('xxx:dev-authorizer'); }); @@ -985,7 +1078,9 @@ describe('#validate()', () => { }; const validated = awsCompileApigEvents.validate(); - expect(validated.events).to.be.an('Array').with.length(1); + expect(validated.events) + .to.be.an('Array') + .with.length(1); expect(validated.events[0].http.authorizer.name).to.equal('custom-name'); expect(validated.events[0].http.authorizer.arn).to.equal('xxx:dev-authorizer'); }); @@ -1060,7 +1155,9 @@ describe('#validate()', () => { }; const validated = awsCompileApigEvents.validate(); - expect(validated.events).to.be.an('Array').with.length(1); + expect(validated.events) + .to.be.an('Array') + .with.length(1); expect(validated.events[0].http.request.parameters).to.deep.equal({ 'method.request.querystring.foo': true, 'method.request.querystring.bar': false, @@ -1103,7 +1200,9 @@ describe('#validate()', () => { }; const validated = awsCompileApigEvents.validate(); - expect(validated.events).to.be.an('Array').with.length(1); + expect(validated.events) + .to.be.an('Array') + .with.length(1); expect(validated.events[0].http.request.parameters).to.deep.equal({ 'method.request.querystring.foo': true, 'method.request.querystring.bar': false, @@ -1200,7 +1299,9 @@ describe('#validate()', () => { }, }; const validated = awsCompileApigEvents.validate(); - expect(validated.events).to.be.an('Array').with.length(1); + expect(validated.events) + .to.be.an('Array') + .with.length(1); expect(validated.events[0].http.integration).to.equal('AWS_PROXY'); }); @@ -1248,7 +1349,9 @@ describe('#validate()', () => { }; const validated = awsCompileApigEvents.validate(); - expect(validated.events).to.be.an('Array').with.length(5); + expect(validated.events) + .to.be.an('Array') + .with.length(5); expect(validated.events[0].http.integration).to.equal('AWS'); expect(validated.events[1].http.integration).to.equal('AWS'); expect(validated.events[2].http.integration).to.equal('AWS_PROXY'); @@ -1275,7 +1378,9 @@ describe('#validate()', () => { }; const validated = awsCompileApigEvents.validate(); - expect(validated.events).to.be.an('Array').with.length(1); + expect(validated.events) + .to.be.an('Array') + .with.length(1); expect(validated.events[0].http.integration).to.equal('HTTP'); }); @@ -1312,7 +1417,9 @@ describe('#validate()', () => { }; const validated = awsCompileApigEvents.validate(); - expect(validated.events).to.be.an('Array').with.length(1); + expect(validated.events) + .to.be.an('Array') + .with.length(1); expect(validated.events[0].http.request.parameters).to.deep.equal({ 'method.request.querystring.foo': true, 'method.request.querystring.bar': false, @@ -1360,7 +1467,9 @@ describe('#validate()', () => { }; const validated = awsCompileApigEvents.validate(); - expect(validated.events).to.be.an('Array').with.length(1); + expect(validated.events) + .to.be.an('Array') + .with.length(1); expect(validated.events[0].http.integration).to.equal('HTTP_PROXY'); }); @@ -1397,7 +1506,9 @@ describe('#validate()', () => { }; const validated = awsCompileApigEvents.validate(); - expect(validated.events).to.be.an('Array').with.length(1); + expect(validated.events) + .to.be.an('Array') + .with.length(1); expect(validated.events[0].http.request.parameters).to.deep.equal({ 'method.request.querystring.foo': true, 'method.request.querystring.bar': false, @@ -1451,14 +1562,14 @@ describe('#validate()', () => { }, }; // initialize so we get the log method from the CLI in place - serverless.init(); + return serverless.init().then(() => { + const logStub = sinon.stub(serverless.cli, 'log'); - const logStub = sinon.stub(serverless.cli, 'log'); + awsCompileApigEvents.validate(); - awsCompileApigEvents.validate(); - - expect(logStub.calledTwice).to.be.equal(true); - expect(logStub.args[0][0].length).to.be.at.least(1); + expect(logStub.calledTwice).to.be.equal(true); + expect(logStub.args[0][0].length).to.be.at.least(1); + }); }); it('should not show a warning message when using request.parameter with HTTP-PROXY', () => { @@ -1493,13 +1604,13 @@ describe('#validate()', () => { }, }; // initialize so we get the log method from the CLI in place - serverless.init(); + return serverless.init().then(() => { + const logStub = sinon.stub(serverless.cli, 'log'); - const logStub = sinon.stub(serverless.cli, 'log'); + awsCompileApigEvents.validate(); - awsCompileApigEvents.validate(); - - expect(logStub.called).to.be.equal(false); + expect(logStub.called).to.be.equal(false); + }); }); it('should remove non-parameter or uri request/response config with HTTP-PROXY', () => { @@ -1529,17 +1640,19 @@ describe('#validate()', () => { }, }; // initialize so we get the log method from the CLI in place - serverless.init(); + return serverless.init().then(() => { + // don't want to print the logs in this test + sinon.stub(serverless.cli, 'log'); - // don't want to print the logs in this test - sinon.stub(serverless.cli, 'log'); - - const validated = awsCompileApigEvents.validate(); - expect(validated.events).to.be.an('Array').with.length(1); - expect(validated.events[0].http.response).to.equal(undefined); - expect(validated.events[0].http.request.uri).to.equal('http://www.example.com'); - expect(validated.events[0].http.request.parameters).to.deep.equal({ - 'method.request.path.foo': true, + const validated = awsCompileApigEvents.validate(); + expect(validated.events) + .to.be.an('Array') + .with.length(1); + expect(validated.events[0].http.response).to.equal(undefined); + expect(validated.events[0].http.request.uri).to.equal('http://www.example.com'); + expect(validated.events[0].http.request.parameters).to.deep.equal({ + 'method.request.path.foo': true, + }); }); }); @@ -1559,7 +1672,9 @@ describe('#validate()', () => { }; const validated = awsCompileApigEvents.validate(); - expect(validated.events).to.be.an('Array').with.length(1); + expect(validated.events) + .to.be.an('Array') + .with.length(1); expect(validated.events[0].http.integration).to.equal('MOCK'); }); @@ -1579,7 +1694,9 @@ describe('#validate()', () => { }; const validated = awsCompileApigEvents.validate(); - expect(validated.events).to.be.an('Array').with.length(1); + expect(validated.events) + .to.be.an('Array') + .with.length(1); expect(validated.events[0].http.integration).to.equal('AWS'); expect(validated.events[0].http.async); }); @@ -1608,14 +1725,14 @@ describe('#validate()', () => { }, }; // initialize so we get the log method from the CLI in place - serverless.init(); + return serverless.init().then(() => { + const logStub = sinon.stub(serverless.cli, 'log'); - const logStub = sinon.stub(serverless.cli, 'log'); + awsCompileApigEvents.validate(); - awsCompileApigEvents.validate(); - - expect(logStub.calledTwice).to.be.equal(true); - expect(logStub.args[0][0].length).to.be.at.least(1); + expect(logStub.calledTwice).to.be.equal(true); + expect(logStub.args[0][0].length).to.be.at.least(1); + }); }); it('should not show a warning message when using request.parameter with LAMBDA-PROXY', () => { @@ -1649,13 +1766,13 @@ describe('#validate()', () => { }, }; // initialize so we get the log method from the CLI in place - serverless.init(); + return serverless.init().then(() => { + const logStub = sinon.stub(serverless.cli, 'log'); - const logStub = sinon.stub(serverless.cli, 'log'); + awsCompileApigEvents.validate(); - awsCompileApigEvents.validate(); - - expect(logStub.called).to.be.equal(false); + expect(logStub.called).to.be.equal(false); + }); }); it('should remove non-parameter request/response config with LAMBDA-PROXY', () => { @@ -1684,16 +1801,18 @@ describe('#validate()', () => { }, }; // initialize so we get the log method from the CLI in place - serverless.init(); + return serverless.init().then(() => { + // don't want to print the logs in this test + sinon.stub(serverless.cli, 'log'); - // don't want to print the logs in this test - sinon.stub(serverless.cli, 'log'); - - const validated = awsCompileApigEvents.validate(); - expect(validated.events).to.be.an('Array').with.length(1); - expect(validated.events[0].http.response).to.equal(undefined); - expect(validated.events[0].http.request.parameters).to.deep.equal({ - 'method.request.path.foo': true, + const validated = awsCompileApigEvents.validate(); + expect(validated.events) + .to.be.an('Array') + .with.length(1); + expect(validated.events[0].http.response).to.equal(undefined); + expect(validated.events[0].http.request.parameters).to.deep.equal({ + 'method.request.path.foo': true, + }); }); }); @@ -1734,7 +1853,9 @@ describe('#validate()', () => { }; const validated = awsCompileApigEvents.validate(); - expect(validated.events).to.be.an('Array').with.length(1); + expect(validated.events) + .to.be.an('Array') + .with.length(1); expect(validated.events[0].http.request.passThrough).to.equal('WHEN_NO_MATCH'); }); @@ -1754,7 +1875,9 @@ describe('#validate()', () => { }; const validated = awsCompileApigEvents.validate(); - expect(validated.events).to.be.an('Array').with.length(1); + expect(validated.events) + .to.be.an('Array') + .with.length(1); expect(validated.events[0].http.request.passThrough).to.equal('NEVER'); }); @@ -1778,7 +1901,9 @@ describe('#validate()', () => { }; const validated = awsCompileApigEvents.validate(); - expect(validated.events).to.be.an('Array').with.length(1); + expect(validated.events) + .to.be.an('Array') + .with.length(1); expect(validated.events[0].http.request.passThrough).to.equal(undefined); }); @@ -1799,7 +1924,9 @@ describe('#validate()', () => { }; const validated = awsCompileApigEvents.validate(); - expect(validated.events).to.be.an('Array').with.length(1); + expect(validated.events) + .to.be.an('Array') + .with.length(1); expect(validated.events[0].http.response.statusCodes).to.deep.equal({ 200: { pattern: '', @@ -1821,13 +1948,13 @@ describe('#validate()', () => { }, 500: { pattern: - '[\\s\\S]*(Process\\s?exited\\s?before\\s?completing\\s?request|\\[500\\])[\\s\\S]*', + '[\\s\\S]*(Process\\s?exited\\s?before\\s?completing\\s?request|\\[500\\])[\\s\\S]*', }, 502: { pattern: '[\\s\\S]*\\[502\\][\\s\\S]*', }, 504: { - pattern: '([\\s\\S]*\\[504\\][\\s\\S]*)|(^[Task timed out].*)', + pattern: '([\\s\\S]*\\[504\\][\\s\\S]*)|(.*Task timed out after \\d+\\.\\d+ seconds$)', }, }); }); diff --git a/lib/plugins/aws/package/compile/events/cloudWatchEvent/index.js b/lib/plugins/aws/package/compile/events/cloudWatchEvent/index.js index 9f8fff08d..d89c830b5 100644 --- a/lib/plugins/aws/package/compile/events/cloudWatchEvent/index.js +++ b/lib/plugins/aws/package/compile/events/cloudWatchEvent/index.js @@ -13,7 +13,7 @@ class AwsCompileCloudWatchEventEvents { } compileCloudWatchEventEvents() { - this.serverless.service.getAllFunctions().forEach((functionName) => { + this.serverless.service.getAllFunctions().forEach(functionName => { const functionObj = this.serverless.service.getFunction(functionName); let cloudWatchEventNumberInFunction = 0; @@ -35,8 +35,7 @@ class AwsCompileCloudWatchEventEvents { `Missing "event" property for cloudwatch event in function ${functionName}`, ' Please check the docs for more info.', ].join(''); - throw new this.serverless.classes - .Error(errorMessage); + throw new this.serverless.classes.Error(errorMessage); } EventPattern = JSON.stringify(event.cloudwatchEvent.event); @@ -75,17 +74,18 @@ class AwsCompileCloudWatchEventEvents { `CloudWatch event of function "${functionName}" is not an object`, ' Please check the docs for more info.', ].join(''); - throw new this.serverless.classes - .Error(errorMessage); + throw new this.serverless.classes.Error(errorMessage); } - const lambdaLogicalId = this.provider.naming - .getLambdaLogicalId(functionName); - const cloudWatchLogicalId = this.provider.naming - .getCloudWatchEventLogicalId(functionName, cloudWatchEventNumberInFunction); - const lambdaPermissionLogicalId = this.provider.naming - .getLambdaCloudWatchEventPermissionLogicalId(functionName, - cloudWatchEventNumberInFunction); + const lambdaLogicalId = this.provider.naming.getLambdaLogicalId(functionName); + const cloudWatchLogicalId = this.provider.naming.getCloudWatchEventLogicalId( + functionName, + cloudWatchEventNumberInFunction + ); + const lambdaPermissionLogicalId = this.provider.naming.getLambdaCloudWatchEventPermissionLogicalId( + functionName, + cloudWatchEventNumberInFunction + ); const cloudWatchId = this.provider.naming.getCloudWatchEventId(functionName); const cloudWatchEventRuleTemplate = ` @@ -111,10 +111,9 @@ class AwsCompileCloudWatchEventEvents { { "Type": "AWS::Lambda::Permission", "Properties": { - "FunctionName": { "Fn::GetAtt": ["${ - lambdaLogicalId}", "Arn"] }, + "FunctionName": { "Fn::GetAtt": ["${lambdaLogicalId}", "Arn"] }, "Action": "lambda:InvokeFunction", - "Principal": { "Fn::Join": ["", ["events.", { "Ref": "AWS::URLSuffix" }]] }, + "Principal": "events.amazonaws.com", "SourceArn": { "Fn::GetAtt": ["${cloudWatchLogicalId}", "Arn"] } } } @@ -128,8 +127,11 @@ class AwsCompileCloudWatchEventEvents { [lambdaPermissionLogicalId]: JSON.parse(permissionTemplate), }; - _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, - newCloudWatchEventRuleObject, newPermissionObject); + _.merge( + this.serverless.service.provider.compiledCloudFormationTemplate.Resources, + newCloudWatchEventRuleObject, + newPermissionObject + ); } }); } @@ -140,7 +142,7 @@ class AwsCompileCloudWatchEventEvents { if (!inputTransformer.inputTemplate) { throw new this.serverless.classes.Error( 'The inputTemplate key is required when specifying an ' + - 'inputTransformer for a cloudwatchEvent event' + 'inputTransformer for a cloudwatchEvent event' ); } const cfmOutput = { diff --git a/lib/plugins/aws/package/compile/events/cloudWatchEvent/index.test.js b/lib/plugins/aws/package/compile/events/cloudWatchEvent/index.test.js index e7decdb2e..685da8540 100644 --- a/lib/plugins/aws/package/compile/events/cloudWatchEvent/index.test.js +++ b/lib/plugins/aws/package/compile/events/cloudWatchEvent/index.test.js @@ -72,9 +72,9 @@ describe('awsCompileCloudWatchEventEvents', () => { { cloudwatchEvent: { event: { - source: ['aws.ec2'], + 'source': ['aws.ec2'], 'detail-type': ['EC2 Instance State-change Notification'], - detail: { state: ['pending'] }, + 'detail': { state: ['pending'] }, }, enabled: false, }, @@ -82,9 +82,9 @@ describe('awsCompileCloudWatchEventEvents', () => { { cloudwatchEvent: { event: { - source: ['aws.ec2'], + 'source': ['aws.ec2'], 'detail-type': ['EC2 Instance State-change Notification'], - detail: { state: ['pending'] }, + 'detail': { state: ['pending'] }, }, enabled: true, }, @@ -95,19 +95,21 @@ describe('awsCompileCloudWatchEventEvents', () => { awsCompileCloudWatchEventEvents.compileCloudWatchEventEvents(); - expect(awsCompileCloudWatchEventEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventsRuleCloudWatchEvent1.Type + expect( + awsCompileCloudWatchEventEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventsRuleCloudWatchEvent1.Type ).to.equal('AWS::Events::Rule'); - expect(awsCompileCloudWatchEventEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventsRuleCloudWatchEvent2.Type + expect( + awsCompileCloudWatchEventEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventsRuleCloudWatchEvent2.Type ).to.equal('AWS::Events::Rule'); - expect(awsCompileCloudWatchEventEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionEventsRuleCloudWatchEvent1.Type + expect( + awsCompileCloudWatchEventEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLambdaPermissionEventsRuleCloudWatchEvent1.Type ).to.equal('AWS::Lambda::Permission'); - expect(awsCompileCloudWatchEventEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionEventsRuleCloudWatchEvent2.Type + expect( + awsCompileCloudWatchEventEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLambdaPermissionEventsRuleCloudWatchEvent2.Type ).to.equal('AWS::Lambda::Permission'); }); @@ -118,9 +120,9 @@ describe('awsCompileCloudWatchEventEvents', () => { { cloudwatchEvent: { event: { - source: ['aws.ec2'], + 'source': ['aws.ec2'], 'detail-type': ['EC2 Instance State-change Notification'], - detail: { state: ['pending'] }, + 'detail': { state: ['pending'] }, }, enabled: false, }, @@ -128,9 +130,9 @@ describe('awsCompileCloudWatchEventEvents', () => { { cloudwatchEvent: { event: { - source: ['aws.ec2'], + 'source': ['aws.ec2'], 'detail-type': ['EC2 Instance State-change Notification'], - detail: { state: ['pending'] }, + 'detail': { state: ['pending'] }, }, enabled: true, }, @@ -138,9 +140,9 @@ describe('awsCompileCloudWatchEventEvents', () => { { cloudwatchEvent: { event: { - source: ['aws.ec2'], + 'source': ['aws.ec2'], 'detail-type': ['EC2 Instance State-change Notification'], - detail: { state: ['pending'] }, + 'detail': { state: ['pending'] }, }, }, }, @@ -150,17 +152,17 @@ describe('awsCompileCloudWatchEventEvents', () => { awsCompileCloudWatchEventEvents.compileCloudWatchEventEvents(); - expect(awsCompileCloudWatchEventEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventsRuleCloudWatchEvent1 - .Properties.State + expect( + awsCompileCloudWatchEventEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventsRuleCloudWatchEvent1.Properties.State ).to.equal('DISABLED'); - expect(awsCompileCloudWatchEventEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventsRuleCloudWatchEvent2 - .Properties.State + expect( + awsCompileCloudWatchEventEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventsRuleCloudWatchEvent2.Properties.State ).to.equal('ENABLED'); - expect(awsCompileCloudWatchEventEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventsRuleCloudWatchEvent3 - .Properties.State + expect( + awsCompileCloudWatchEventEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventsRuleCloudWatchEvent3.Properties.State ).to.equal('ENABLED'); }); @@ -171,9 +173,9 @@ describe('awsCompileCloudWatchEventEvents', () => { { cloudwatchEvent: { event: { - source: ['aws.ec2'], + 'source': ['aws.ec2'], 'detail-type': ['EC2 Instance State-change Notification'], - detail: { state: ['pending'] }, + 'detail': { state: ['pending'] }, }, enabled: false, inputPath: '$.stageVariables', @@ -185,9 +187,9 @@ describe('awsCompileCloudWatchEventEvents', () => { awsCompileCloudWatchEventEvents.compileCloudWatchEventEvents(); - expect(awsCompileCloudWatchEventEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventsRuleCloudWatchEvent1 - .Properties.Targets[0].InputPath + expect( + awsCompileCloudWatchEventEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventsRuleCloudWatchEvent1.Properties.Targets[0].InputPath ).to.equal('$.stageVariables'); }); @@ -198,9 +200,9 @@ describe('awsCompileCloudWatchEventEvents', () => { { cloudwatchEvent: { event: { - source: ['aws.ec2'], + 'source': ['aws.ec2'], 'detail-type': ['EC2 Instance State-change Notification'], - detail: { state: ['pending'] }, + 'detail': { state: ['pending'] }, }, enabled: false, input: '{"key":"value"}', @@ -212,9 +214,9 @@ describe('awsCompileCloudWatchEventEvents', () => { awsCompileCloudWatchEventEvents.compileCloudWatchEventEvents(); - expect(awsCompileCloudWatchEventEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventsRuleCloudWatchEvent1 - .Properties.Targets[0].Input + expect( + awsCompileCloudWatchEventEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventsRuleCloudWatchEvent1.Properties.Targets[0].Input ).to.equal('{"key":"value"}'); }); @@ -225,9 +227,9 @@ describe('awsCompileCloudWatchEventEvents', () => { { cloudwatchEvent: { event: { - source: ['aws.ec2'], + 'source': ['aws.ec2'], 'detail-type': ['EC2 Instance State-change Notification'], - detail: { state: ['pending'] }, + 'detail': { state: ['pending'] }, }, enabled: false, inputTransformer: { @@ -244,16 +246,15 @@ describe('awsCompileCloudWatchEventEvents', () => { awsCompileCloudWatchEventEvents.compileCloudWatchEventEvents(); - expect(awsCompileCloudWatchEventEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventsRuleCloudWatchEvent1 - .Properties.Targets[0].InputTransformer + expect( + awsCompileCloudWatchEventEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventsRuleCloudWatchEvent1.Properties.Targets[0].InputTransformer ).to.eql({ InputTemplate: '{"time": , "key1": "value1"}', InputPathsMap: { eventTime: '$.time' }, }); }); - it('should respect description variable', () => { awsCompileCloudWatchEventEvents.serverless.service.functions = { first: { @@ -261,9 +262,9 @@ describe('awsCompileCloudWatchEventEvents', () => { { cloudwatchEvent: { event: { - source: ['aws.ec2'], + 'source': ['aws.ec2'], 'detail-type': ['EC2 Instance State-change Notification'], - detail: { state: ['pending'] }, + 'detail': { state: ['pending'] }, }, enabled: false, input: '{"key":"value"}', @@ -276,9 +277,9 @@ describe('awsCompileCloudWatchEventEvents', () => { awsCompileCloudWatchEventEvents.compileCloudWatchEventEvents(); - expect(awsCompileCloudWatchEventEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventsRuleCloudWatchEvent1 - .Properties.Description + expect( + awsCompileCloudWatchEventEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventsRuleCloudWatchEvent1.Properties.Description ).to.equal('test description'); }); @@ -289,9 +290,9 @@ describe('awsCompileCloudWatchEventEvents', () => { { cloudwatchEvent: { event: { - source: ['aws.ec2'], + 'source': ['aws.ec2'], 'detail-type': ['EC2 Instance State-change Notification'], - detail: { state: ['pending'] }, + 'detail': { state: ['pending'] }, }, enabled: false, input: '{"key":"value"}', @@ -304,9 +305,9 @@ describe('awsCompileCloudWatchEventEvents', () => { awsCompileCloudWatchEventEvents.compileCloudWatchEventEvents(); - expect(awsCompileCloudWatchEventEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventsRuleCloudWatchEvent1 - .Properties.Name + expect( + awsCompileCloudWatchEventEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventsRuleCloudWatchEvent1.Properties.Name ).to.equal('test-event-name'); }); @@ -317,9 +318,9 @@ describe('awsCompileCloudWatchEventEvents', () => { { cloudwatchEvent: { event: { - source: ['aws.ec2'], + 'source': ['aws.ec2'], 'detail-type': ['EC2 Instance State-change Notification'], - detail: { state: ['pending'] }, + 'detail': { state: ['pending'] }, }, enabled: false, input: { @@ -333,9 +334,9 @@ describe('awsCompileCloudWatchEventEvents', () => { awsCompileCloudWatchEventEvents.compileCloudWatchEventEvents(); - expect(awsCompileCloudWatchEventEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventsRuleCloudWatchEvent1 - .Properties.Targets[0].Input + expect( + awsCompileCloudWatchEventEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventsRuleCloudWatchEvent1.Properties.Targets[0].Input ).to.equal('{"key":"value"}'); }); @@ -346,9 +347,9 @@ describe('awsCompileCloudWatchEventEvents', () => { { cloudwatchEvent: { event: { - source: ['aws.ec2'], + 'source': ['aws.ec2'], 'detail-type': ['EC2 Instance State-change Notification'], - detail: { state: ['pending'] }, + 'detail': { state: ['pending'] }, }, enabled: false, input: { @@ -371,9 +372,9 @@ describe('awsCompileCloudWatchEventEvents', () => { { cloudwatchEvent: { event: { - source: ['aws.ec2'], + 'source': ['aws.ec2'], 'detail-type': ['EC2 Instance State-change Notification'], - detail: { state: ['pending'] }, + 'detail': { state: ['pending'] }, }, enabled: false, input: { @@ -401,9 +402,9 @@ describe('awsCompileCloudWatchEventEvents', () => { { cloudwatchEvent: { event: { - source: ['aws.ec2'], + 'source': ['aws.ec2'], 'detail-type': ['EC2 Instance State-change Notification'], - detail: { state: ['pending'] }, + 'detail': { state: ['pending'] }, }, enabled: false, inputTransformer: { @@ -427,9 +428,9 @@ describe('awsCompileCloudWatchEventEvents', () => { { cloudwatchEvent: { event: { - source: ['aws.ec2'], + 'source': ['aws.ec2'], 'detail-type': ['EC2 Instance State-change Notification \n with newline'], - detail: { state: ['pending'] }, + 'detail': { state: ['pending'] }, }, enabled: false, input: { @@ -442,13 +443,13 @@ describe('awsCompileCloudWatchEventEvents', () => { }; awsCompileCloudWatchEventEvents.compileCloudWatchEventEvents(); - expect(awsCompileCloudWatchEventEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstEventsRuleCloudWatchEvent1.Properties.EventPattern['detail-type'][0] + expect( + awsCompileCloudWatchEventEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventsRuleCloudWatchEvent1.Properties.EventPattern['detail-type'][0] ).to.equal('EC2 Instance State-change Notification with newline'); - expect(awsCompileCloudWatchEventEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstEventsRuleCloudWatchEvent1.Properties.Targets[0].Input + expect( + awsCompileCloudWatchEventEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventsRuleCloudWatchEvent1.Properties.Targets[0].Input ).to.equal('{"key":"value"}'); }); diff --git a/lib/plugins/aws/package/compile/events/cloudWatchLog/index.js b/lib/plugins/aws/package/compile/events/cloudWatchLog/index.js index b5808ef47..58024492c 100644 --- a/lib/plugins/aws/package/compile/events/cloudWatchLog/index.js +++ b/lib/plugins/aws/package/compile/events/cloudWatchLog/index.js @@ -15,7 +15,7 @@ class AwsCompileCloudWatchLogEvents { compileCloudWatchLogEvents() { const logGroupNames = []; - this.serverless.service.getAllFunctions().forEach((functionName) => { + this.serverless.service.getAllFunctions().forEach(functionName => { const functionObj = this.serverless.service.getFunction(functionName); let cloudWatchLogNumberInFunction = 0; @@ -34,8 +34,7 @@ class AwsCompileCloudWatchLogEvents { 'Missing "logGroup" property for cloudwatchLog event ', `in function ${functionName} Please check the docs for more info.`, ].join(''); - throw new this.serverless.classes - .Error(errorMessage); + throw new this.serverless.classes.Error(errorMessage); } if (event.cloudwatchLog.filter && typeof event.cloudwatchLog.filter !== 'string') { @@ -43,13 +42,13 @@ class AwsCompileCloudWatchLogEvents { `"filter" property for cloudwatchLog event in function ${functionName} `, 'should be string. Please check the docs for more info.', ].join(''); - throw new this.serverless.classes - .Error(errorMessage); + throw new this.serverless.classes.Error(errorMessage); } LogGroupName = event.cloudwatchLog.logGroup.replace(/\r?\n/g, ''); - FilterPattern = event.cloudwatchLog.filter ? - event.cloudwatchLog.filter.replace(/\r?\n/g, '') : ''; + FilterPattern = event.cloudwatchLog.filter + ? event.cloudwatchLog.filter.replace(/\r?\n/g, '') + : ''; } else if (typeof event.cloudwatchLog === 'string') { LogGroupName = event.cloudwatchLog.replace(/\r?\n/g, ''); FilterPattern = ''; @@ -58,8 +57,7 @@ class AwsCompileCloudWatchLogEvents { `cloudwatchLog event of function "${functionName}" is not an object or a string`, ' Please check the docs for more info.', ].join(''); - throw new this.serverless.classes - .Error(errorMessage); + throw new this.serverless.classes.Error(errorMessage); } if (_.indexOf(logGroupNames, LogGroupName) !== -1) { @@ -67,18 +65,19 @@ class AwsCompileCloudWatchLogEvents { `"${LogGroupName}" logGroup for cloudwatchLog event is duplicated.`, ' This property can only be set once per CloudFormation stack.', ].join(''); - throw new this.serverless.classes - .Error(errorMessage); + throw new this.serverless.classes.Error(errorMessage); } logGroupNames.push(LogGroupName); logGroupNamesThisFunction.push(LogGroupName); - const lambdaLogicalId = this.provider.naming - .getLambdaLogicalId(functionName); - const cloudWatchLogLogicalId = this.provider.naming - .getCloudWatchLogLogicalId(functionName, cloudWatchLogNumberInFunction); - const lambdaPermissionLogicalId = this.provider.naming - .getLambdaCloudWatchLogPermissionLogicalId(functionName); + const lambdaLogicalId = this.provider.naming.getLambdaLogicalId(functionName); + const cloudWatchLogLogicalId = this.provider.naming.getCloudWatchLogLogicalId( + functionName, + cloudWatchLogNumberInFunction + ); + const lambdaPermissionLogicalId = this.provider.naming.getLambdaCloudWatchLogPermissionLogicalId( + functionName + ); // unescape quotes once when the first quote is detected escaped const idxFirstSlash = FilterPattern.indexOf('\\'); @@ -105,15 +104,13 @@ class AwsCompileCloudWatchLogEvents { { "Type": "AWS::Lambda::Permission", "Properties": { - "FunctionName": { "Fn::GetAtt": ["${ - lambdaLogicalId}", "Arn"] }, + "FunctionName": { "Fn::GetAtt": ["${lambdaLogicalId}", "Arn"] }, "Action": "lambda:InvokeFunction", "Principal": { "Fn::Join": [ "", [ "logs.", { "Ref": "AWS::Region" }, - ".", - { "Ref": "AWS::URLSuffix" } + ".amazonaws.com" ] ] }, "SourceArn": { @@ -141,8 +138,11 @@ class AwsCompileCloudWatchLogEvents { [lambdaPermissionLogicalId]: JSON.parse(permissionTemplate), }; - _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, - newCloudWatchLogRuleObject, newPermissionObject); + _.merge( + this.serverless.service.provider.compiledCloudFormationTemplate.Resources, + newCloudWatchLogRuleObject, + newPermissionObject + ); } }); } @@ -159,7 +159,7 @@ class AwsCompileCloudWatchLogEvents { } return last; }, first); - return longestCommon + ((longestCommon === first) ? '' : '*'); + return longestCommon + (longestCommon === first ? '' : '*'); } } diff --git a/lib/plugins/aws/package/compile/events/cloudWatchLog/index.test.js b/lib/plugins/aws/package/compile/events/cloudWatchLog/index.test.js index ddab8a51e..9161f09dc 100644 --- a/lib/plugins/aws/package/compile/events/cloudWatchLog/index.test.js +++ b/lib/plugins/aws/package/compile/events/cloudWatchLog/index.test.js @@ -82,33 +82,33 @@ describe('AwsCompileCloudWatchLogEvents', () => { }; awsCompileCloudWatchLogEvents.compileCloudWatchLogEvents(); - expect(awsCompileCloudWatchLogEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstLogsSubscriptionFilterCloudWatchLog1.Type + expect( + awsCompileCloudWatchLogEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLogsSubscriptionFilterCloudWatchLog1.Type ).to.equal('AWS::Logs::SubscriptionFilter'); - expect(awsCompileCloudWatchLogEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstLogsSubscriptionFilterCloudWatchLog2.Type + expect( + awsCompileCloudWatchLogEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLogsSubscriptionFilterCloudWatchLog2.Type ).to.equal('AWS::Logs::SubscriptionFilter'); - expect(awsCompileCloudWatchLogEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstLogsSubscriptionFilterCloudWatchLog1 - .Properties.LogGroupName + expect( + awsCompileCloudWatchLogEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLogsSubscriptionFilterCloudWatchLog1.Properties.LogGroupName ).to.equal('/aws/lambda/hello1'); - expect(awsCompileCloudWatchLogEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstLogsSubscriptionFilterCloudWatchLog2 - .Properties.LogGroupName + expect( + awsCompileCloudWatchLogEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLogsSubscriptionFilterCloudWatchLog2.Properties.LogGroupName ).to.equal('/aws/lambda/hello2'); - expect(awsCompileCloudWatchLogEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstLogsSubscriptionFilterCloudWatchLog1 - .Properties.FilterPattern + expect( + awsCompileCloudWatchLogEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLogsSubscriptionFilterCloudWatchLog1.Properties.FilterPattern ).to.equal(''); - expect(awsCompileCloudWatchLogEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstLogsSubscriptionFilterCloudWatchLog2 - .Properties.FilterPattern + expect( + awsCompileCloudWatchLogEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLogsSubscriptionFilterCloudWatchLog2.Properties.FilterPattern ).to.equal(''); - expect(awsCompileCloudWatchLogEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionLogsSubscriptionFilterCloudWatchLog.Type + expect( + awsCompileCloudWatchLogEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLambdaPermissionLogsSubscriptionFilterCloudWatchLog.Type ).to.equal('AWS::Lambda::Permission'); }); @@ -128,9 +128,9 @@ describe('AwsCompileCloudWatchLogEvents', () => { awsCompileCloudWatchLogEvents.compileCloudWatchLogEvents(); - expect(awsCompileCloudWatchLogEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstLogsSubscriptionFilterCloudWatchLog1 - .Properties.FilterPattern + expect( + awsCompileCloudWatchLogEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLogsSubscriptionFilterCloudWatchLog1.Properties.FilterPattern ).to.equal('{$.userIdentity.type = Root}'); }); @@ -150,9 +150,9 @@ describe('AwsCompileCloudWatchLogEvents', () => { awsCompileCloudWatchLogEvents.compileCloudWatchLogEvents(); - expect(awsCompileCloudWatchLogEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstLogsSubscriptionFilterCloudWatchLog1 - .Properties.FilterPattern + expect( + awsCompileCloudWatchLogEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLogsSubscriptionFilterCloudWatchLog1.Properties.FilterPattern ).to.equal('"Total amount" -"level=Debug"'); }); @@ -163,7 +163,7 @@ describe('AwsCompileCloudWatchLogEvents', () => { { cloudwatchLog: { logGroup: '/aws/lambda/hello1', - filter: "\\\"Total amount\\\" -\\\"level=Debug\\\"", // eslint-disable-line quotes + filter: '\\"Total amount\\" -\\"level=Debug\\"', // eslint-disable-line quotes }, }, ], @@ -172,14 +172,13 @@ describe('AwsCompileCloudWatchLogEvents', () => { awsCompileCloudWatchLogEvents.compileCloudWatchLogEvents(); - expect(awsCompileCloudWatchLogEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstLogsSubscriptionFilterCloudWatchLog1 - .Properties.FilterPattern + expect( + awsCompileCloudWatchLogEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLogsSubscriptionFilterCloudWatchLog1.Properties.FilterPattern ).to.equal('"Total amount" -"level=Debug"'); }); - it('should set an empty string for FilterPattern statement when "filter" variable is not given' - , () => { + it('should set an empty string for FilterPattern statement when "filter" variable is not given', () => { awsCompileCloudWatchLogEvents.serverless.service.functions = { first: { events: [ @@ -194,9 +193,9 @@ describe('AwsCompileCloudWatchLogEvents', () => { awsCompileCloudWatchLogEvents.compileCloudWatchLogEvents(); - expect(awsCompileCloudWatchLogEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstLogsSubscriptionFilterCloudWatchLog1 - .Properties.FilterPattern + expect( + awsCompileCloudWatchLogEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLogsSubscriptionFilterCloudWatchLog1.Properties.FilterPattern ).to.equal(''); }); @@ -232,8 +231,7 @@ describe('AwsCompileCloudWatchLogEvents', () => { expect(() => awsCompileCloudWatchLogEvents.compileCloudWatchLogEvents()).to.throw(Error); }); - it('should create corresponding resources when cloudwatchLog events are given as a string', - () => { + it('should create corresponding resources when cloudwatchLog events are given as a string', () => { awsCompileCloudWatchLogEvents.serverless.service.functions = { first: { events: [ @@ -248,61 +246,71 @@ describe('AwsCompileCloudWatchLogEvents', () => { }; awsCompileCloudWatchLogEvents.compileCloudWatchLogEvents(); - expect(awsCompileCloudWatchLogEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstLogsSubscriptionFilterCloudWatchLog1.Type + expect( + awsCompileCloudWatchLogEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLogsSubscriptionFilterCloudWatchLog1.Type ).to.equal('AWS::Logs::SubscriptionFilter'); - expect(awsCompileCloudWatchLogEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstLogsSubscriptionFilterCloudWatchLog2.Type + expect( + awsCompileCloudWatchLogEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLogsSubscriptionFilterCloudWatchLog2.Type ).to.equal('AWS::Logs::SubscriptionFilter'); - expect(awsCompileCloudWatchLogEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstLogsSubscriptionFilterCloudWatchLog1 - .Properties.LogGroupName + expect( + awsCompileCloudWatchLogEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLogsSubscriptionFilterCloudWatchLog1.Properties.LogGroupName ).to.equal('/aws/lambda/hello1'); - expect(awsCompileCloudWatchLogEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstLogsSubscriptionFilterCloudWatchLog2 - .Properties.LogGroupName + expect( + awsCompileCloudWatchLogEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLogsSubscriptionFilterCloudWatchLog2.Properties.LogGroupName ).to.equal('/aws/lambda/hello2'); - expect(awsCompileCloudWatchLogEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstLogsSubscriptionFilterCloudWatchLog1 - .Properties.FilterPattern + expect( + awsCompileCloudWatchLogEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLogsSubscriptionFilterCloudWatchLog1.Properties.FilterPattern ).to.equal(''); - expect(awsCompileCloudWatchLogEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstLogsSubscriptionFilterCloudWatchLog2 - .Properties.FilterPattern + expect( + awsCompileCloudWatchLogEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLogsSubscriptionFilterCloudWatchLog2.Properties.FilterPattern ).to.equal(''); - expect(awsCompileCloudWatchLogEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionLogsSubscriptionFilterCloudWatchLog.Type + expect( + awsCompileCloudWatchLogEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLambdaPermissionLogsSubscriptionFilterCloudWatchLog.Type ).to.equal('AWS::Lambda::Permission'); }); it('should create a longest-common suffix of logGroup to minimize scope', () => { - expect(awsCompileCloudWatchLogEvents - .longestCommonSuffix(['/aws/lambda/hello1'])) - .to.equal('/aws/lambda/hello1'); - expect(awsCompileCloudWatchLogEvents - .longestCommonSuffix(['/aws/lambda/hello1', '/aws/lambda/hello2'])) - .to.equal('/aws/lambda/hello*'); - expect(awsCompileCloudWatchLogEvents - .longestCommonSuffix(['/aws/lambda/hello1', '/aws/lambda/hot'])) - .to.equal('/aws/lambda/h*'); - expect(awsCompileCloudWatchLogEvents - .longestCommonSuffix(['/aws/lambda/hello1', '/aws/lambda/tweet'])) - .to.equal('/aws/lambda/*'); - expect(awsCompileCloudWatchLogEvents - .longestCommonSuffix(['/aws/lambda/hello1', '/aws/lex/log1', '/aws/lightsail/log1'])) - .to.equal('/aws/l*'); - expect(awsCompileCloudWatchLogEvents - .longestCommonSuffix(['/aws/lambda/hello1', '/aws/batch/log1'])) - .to.equal('/aws/*'); - expect(awsCompileCloudWatchLogEvents - .longestCommonSuffix(['/aws/*', '/aws/lambda/hello'])) - .to.equal('/aws/*'); - expect(awsCompileCloudWatchLogEvents - .longestCommonSuffix(['/aws/lambda/*', '/aws/lambda/hello'])) - .to.equal('/aws/lambda/*'); + expect(awsCompileCloudWatchLogEvents.longestCommonSuffix(['/aws/lambda/hello1'])).to.equal( + '/aws/lambda/hello1' + ); + expect( + awsCompileCloudWatchLogEvents.longestCommonSuffix([ + '/aws/lambda/hello1', + '/aws/lambda/hello2', + ]) + ).to.equal('/aws/lambda/hello*'); + expect( + awsCompileCloudWatchLogEvents.longestCommonSuffix(['/aws/lambda/hello1', '/aws/lambda/hot']) + ).to.equal('/aws/lambda/h*'); + expect( + awsCompileCloudWatchLogEvents.longestCommonSuffix([ + '/aws/lambda/hello1', + '/aws/lambda/tweet', + ]) + ).to.equal('/aws/lambda/*'); + expect( + awsCompileCloudWatchLogEvents.longestCommonSuffix([ + '/aws/lambda/hello1', + '/aws/lex/log1', + '/aws/lightsail/log1', + ]) + ).to.equal('/aws/l*'); + expect( + awsCompileCloudWatchLogEvents.longestCommonSuffix(['/aws/lambda/hello1', '/aws/batch/log1']) + ).to.equal('/aws/*'); + expect( + awsCompileCloudWatchLogEvents.longestCommonSuffix(['/aws/*', '/aws/lambda/hello']) + ).to.equal('/aws/*'); + expect( + awsCompileCloudWatchLogEvents.longestCommonSuffix(['/aws/lambda/*', '/aws/lambda/hello']) + ).to.equal('/aws/lambda/*'); }); it('should throw an error if "logGroup" is duplicated in one CloudFormation stack', () => { @@ -365,13 +373,13 @@ describe('AwsCompileCloudWatchLogEvents', () => { awsCompileCloudWatchLogEvents.compileCloudWatchLogEvents(); - expect(awsCompileCloudWatchLogEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstLogsSubscriptionFilterCloudWatchLog1 - .Properties.LogGroupName + expect( + awsCompileCloudWatchLogEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLogsSubscriptionFilterCloudWatchLog1.Properties.LogGroupName ).to.equal('/aws/lambda/hello1'); - expect(awsCompileCloudWatchLogEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstLogsSubscriptionFilterCloudWatchLog1 - .Properties.FilterPattern + expect( + awsCompileCloudWatchLogEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLogsSubscriptionFilterCloudWatchLog1.Properties.FilterPattern ).to.equal('{$.userIdentity.type = Root}'); awsCompileCloudWatchLogEvents.serverless.service.functions = { @@ -386,19 +394,16 @@ describe('AwsCompileCloudWatchLogEvents', () => { awsCompileCloudWatchLogEvents.compileCloudWatchLogEvents(); - expect(awsCompileCloudWatchLogEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstLogsSubscriptionFilterCloudWatchLog1 - .Properties.LogGroupName + expect( + awsCompileCloudWatchLogEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLogsSubscriptionFilterCloudWatchLog1.Properties.LogGroupName ).to.equal('/aws/lambda/hello3'); }); - it('should not create corresponding resources when cloudwatchLog event is not given', - () => { + it('should not create corresponding resources when cloudwatchLog event is not given', () => { awsCompileCloudWatchLogEvents.serverless.service.functions = { first: { - events: [ - {}, - ], + events: [{}], }, }; diff --git a/lib/plugins/aws/package/compile/events/cognitoUserPool/index.js b/lib/plugins/aws/package/compile/events/cognitoUserPool/index.js index a85c67a4c..345f68e9d 100644 --- a/lib/plugins/aws/package/compile/events/cognitoUserPool/index.js +++ b/lib/plugins/aws/package/compile/events/cognitoUserPool/index.js @@ -29,32 +29,34 @@ class AwsCompileCognitoUserPoolEvents { const cognitoUserPoolTriggerFunctions = []; // Iterate through all functions declared in `serverless.yml` - _.forEach(this.serverless.service.getAllFunctions(), (functionName) => { + _.forEach(this.serverless.service.getAllFunctions(), functionName => { const functionObj = this.serverless.service.getFunction(functionName); if (functionObj.events) { - _.forEach(functionObj.events, (event) => { + _.forEach(functionObj.events, event => { if (event.cognitoUserPool) { // Check event definition for `cognitoUserPool` object if (typeof event.cognitoUserPool === 'object') { // Check `cognitoUserPool` object has required properties if (!event.cognitoUserPool.pool || !event.cognitoUserPool.trigger) { - throw new this.serverless.classes - .Error([ + throw new this.serverless.classes.Error( + [ `Cognito User Pool event of function "${functionName}" is not an object.`, 'The correct syntax is an object with the "pool" and "trigger" properties.', 'Please check the docs for more info.', - ].join(' ')); + ].join(' ') + ); } // Check `cognitoUserPool` trigger is valid if (!_.includes(validTriggerSources, event.cognitoUserPool.trigger)) { - throw new this.serverless.classes - .Error([ + throw new this.serverless.classes.Error( + [ 'Cognito User Pool trigger source is invalid, must be one of:', `${validTriggerSources.join(', ')}.`, 'Please check the docs for more info.', - ].join(' ')); + ].join(' ') + ); } // Save trigger functions so we can use them to generate @@ -69,12 +71,13 @@ class AwsCompileCognitoUserPoolEvents { // CloudFormation resources later userPools.push(event.cognitoUserPool.pool); } else { - throw new this.serverless.classes - .Error([ + throw new this.serverless.classes.Error( + [ `Cognito User Pool event of function "${functionName}" is not an object.`, 'The correct syntax is an object with the "pool" and "trigger" properties.', 'Please check the docs for more info.', - ].join(' ')); + ].join(' ') + ); } } }); @@ -85,25 +88,27 @@ class AwsCompileCognitoUserPoolEvents { } generateTemplateForPool(poolName, currentPoolTriggerFunctions) { - const lambdaConfig = _.reduce(currentPoolTriggerFunctions, (result, value) => { - const lambdaLogicalId = this.provider.naming.getLambdaLogicalId(value.functionName); + const lambdaConfig = _.reduce( + currentPoolTriggerFunctions, + (result, value) => { + const lambdaLogicalId = this.provider.naming.getLambdaLogicalId(value.functionName); - // Return a new object to avoid lint errors - return Object.assign({}, result, { - [value.triggerSource]: { - 'Fn::GetAtt': [ - lambdaLogicalId, - 'Arn', - ], - }, - }); - }, {}); + // Return a new object to avoid lint errors + return Object.assign({}, result, { + [value.triggerSource]: { + 'Fn::GetAtt': [lambdaLogicalId, 'Arn'], + }, + }); + }, + {} + ); const userPoolLogicalId = this.provider.naming.getCognitoUserPoolLogicalId(poolName); // Attach `DependsOn` for any relevant Lambdas - const DependsOn = _.map(currentPoolTriggerFunctions, (value) => this - .provider.naming.getLambdaLogicalId(value.functionName)); + const DependsOn = _.map(currentPoolTriggerFunctions, value => + this.provider.naming.getLambdaLogicalId(value.functionName) + ); return { [userPoolLogicalId]: { @@ -123,51 +128,53 @@ class AwsCompileCognitoUserPoolEvents { const userPools = result.userPools; // Generate CloudFormation templates for Cognito User Pool changes - _.forEach(userPools, (poolName) => { + _.forEach(userPools, poolName => { const currentPoolTriggerFunctions = _.filter(cognitoUserPoolTriggerFunctions, { poolName }); const userPoolCFResource = this.generateTemplateForPool( poolName, currentPoolTriggerFunctions ); - _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, - userPoolCFResource); + _.merge( + this.serverless.service.provider.compiledCloudFormationTemplate.Resources, + userPoolCFResource + ); }); // Generate CloudFormation templates for IAM permissions to allow Cognito to trigger Lambda - _.forEach(cognitoUserPoolTriggerFunctions, (cognitoUserPoolTriggerFunction) => { - const userPoolLogicalId = this.provider.naming - .getCognitoUserPoolLogicalId(cognitoUserPoolTriggerFunction.poolName); - const lambdaLogicalId = this.provider.naming - .getLambdaLogicalId(cognitoUserPoolTriggerFunction.functionName); + _.forEach(cognitoUserPoolTriggerFunctions, cognitoUserPoolTriggerFunction => { + const userPoolLogicalId = this.provider.naming.getCognitoUserPoolLogicalId( + cognitoUserPoolTriggerFunction.poolName + ); + const lambdaLogicalId = this.provider.naming.getLambdaLogicalId( + cognitoUserPoolTriggerFunction.functionName + ); const permissionTemplate = { Type: 'AWS::Lambda::Permission', Properties: { FunctionName: { - 'Fn::GetAtt': [ - lambdaLogicalId, - 'Arn', - ], + 'Fn::GetAtt': [lambdaLogicalId, 'Arn'], }, Action: 'lambda:InvokeFunction', - Principal: { 'Fn::Join': ['', ['cognito-idp.', { Ref: 'AWS::URLSuffix' }]] }, + Principal: 'cognito-idp.amazonaws.com', SourceArn: { - 'Fn::GetAtt': [ - userPoolLogicalId, - 'Arn', - ], + 'Fn::GetAtt': [userPoolLogicalId, 'Arn'], }, }, }; - const lambdaPermissionLogicalId = this.provider.naming - .getLambdaCognitoUserPoolPermissionLogicalId(cognitoUserPoolTriggerFunction.functionName, - cognitoUserPoolTriggerFunction.poolName, cognitoUserPoolTriggerFunction.triggerSource); + const lambdaPermissionLogicalId = this.provider.naming.getLambdaCognitoUserPoolPermissionLogicalId( + cognitoUserPoolTriggerFunction.functionName, + cognitoUserPoolTriggerFunction.poolName, + cognitoUserPoolTriggerFunction.triggerSource + ); const permissionCFResource = { [lambdaPermissionLogicalId]: permissionTemplate, }; - _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, - permissionCFResource); + _.merge( + this.serverless.service.provider.compiledCloudFormationTemplate.Resources, + permissionCFResource + ); }); } @@ -176,7 +183,7 @@ class AwsCompileCognitoUserPoolEvents { const cognitoUserPoolTriggerFunctions = result.cognitoUserPoolTriggerFunctions; const userPools = result.userPools; - _.forEach(userPools, (poolName) => { + _.forEach(userPools, poolName => { const currentPoolTriggerFunctions = _.filter(cognitoUserPoolTriggerFunctions, { poolName }); const userPoolLogicalId = this.provider.naming.getCognitoUserPoolLogicalId(poolName); @@ -193,17 +200,14 @@ class AwsCompileCognitoUserPoolEvents { const DependsOn = generatedUserPool.DependsOn.concat(customUserPoolDependsOn); // Merge default and custom resources, and `DependsOn` clause - const mergedTemplate = Object.assign( - {}, - _.merge(generatedUserPool, customUserPool), - { DependsOn } - ); + const mergedTemplate = Object.assign({}, _.merge(generatedUserPool, customUserPool), { + DependsOn, + }); // Merge resource back into `Resources` - _.merge( - this.serverless.service.provider.compiledCloudFormationTemplate.Resources, - { [userPoolLogicalId]: mergedTemplate } - ); + _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, { + [userPoolLogicalId]: mergedTemplate, + }); } }); } diff --git a/lib/plugins/aws/package/compile/events/cognitoUserPool/index.test.js b/lib/plugins/aws/package/compile/events/cognitoUserPool/index.test.js index a67e80bb7..a639bf0f7 100644 --- a/lib/plugins/aws/package/compile/events/cognitoUserPool/index.test.js +++ b/lib/plugins/aws/package/compile/events/cognitoUserPool/index.test.js @@ -113,25 +113,30 @@ describe('AwsCompileCognitoUserPoolEvents', () => { awsCompileCognitoUserPoolEvents.compileCognitoUserPoolEvents(); - expect(awsCompileCognitoUserPoolEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources.CognitoUserPoolMyUserPool1.Type + expect( + awsCompileCognitoUserPoolEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.CognitoUserPoolMyUserPool1.Type ).to.equal('AWS::Cognito::UserPool'); - expect(awsCompileCognitoUserPoolEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources.CognitoUserPoolMyUserPool1.DependsOn + expect( + awsCompileCognitoUserPoolEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.CognitoUserPoolMyUserPool1.DependsOn ).to.have.lengthOf(1); - expect(awsCompileCognitoUserPoolEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources.CognitoUserPoolMyUserPool2.Type + expect( + awsCompileCognitoUserPoolEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.CognitoUserPoolMyUserPool2.Type ).to.equal('AWS::Cognito::UserPool'); - expect(awsCompileCognitoUserPoolEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources.CognitoUserPoolMyUserPool2.DependsOn + expect( + awsCompileCognitoUserPoolEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.CognitoUserPoolMyUserPool2.DependsOn ).to.have.lengthOf(1); - expect(awsCompileCognitoUserPoolEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionCognitoUserPoolMyUserPool1TriggerSourcePreSignUp.Type + expect( + awsCompileCognitoUserPoolEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLambdaPermissionCognitoUserPoolMyUserPool1TriggerSourcePreSignUp.Type ).to.equal('AWS::Lambda::Permission'); - expect(awsCompileCognitoUserPoolEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources - .SecondLambdaPermissionCognitoUserPoolMyUserPool2TriggerSourcePostConfirmation.Type + expect( + awsCompileCognitoUserPoolEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.SecondLambdaPermissionCognitoUserPoolMyUserPool2TriggerSourcePostConfirmation + .Type ).to.equal('AWS::Lambda::Permission'); }); @@ -157,25 +162,30 @@ describe('AwsCompileCognitoUserPoolEvents', () => { awsCompileCognitoUserPoolEvents.compileCognitoUserPoolEvents(); - expect(awsCompileCognitoUserPoolEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources.CognitoUserPoolMyUserPool1.Type + expect( + awsCompileCognitoUserPoolEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.CognitoUserPoolMyUserPool1.Type ).to.equal('AWS::Cognito::UserPool'); - expect(awsCompileCognitoUserPoolEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources.CognitoUserPoolMyUserPool1.DependsOn + expect( + awsCompileCognitoUserPoolEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.CognitoUserPoolMyUserPool1.DependsOn ).to.have.lengthOf(1); - expect(awsCompileCognitoUserPoolEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources.CognitoUserPoolMyUserPool2.Type + expect( + awsCompileCognitoUserPoolEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.CognitoUserPoolMyUserPool2.Type ).to.equal('AWS::Cognito::UserPool'); - expect(awsCompileCognitoUserPoolEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources.CognitoUserPoolMyUserPool2.DependsOn + expect( + awsCompileCognitoUserPoolEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.CognitoUserPoolMyUserPool2.DependsOn ).to.have.lengthOf(1); - expect(awsCompileCognitoUserPoolEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionCognitoUserPoolMyUserPool1TriggerSourcePreSignUp.Type + expect( + awsCompileCognitoUserPoolEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLambdaPermissionCognitoUserPoolMyUserPool1TriggerSourcePreSignUp.Type ).to.equal('AWS::Lambda::Permission'); - expect(awsCompileCognitoUserPoolEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionCognitoUserPoolMyUserPool2TriggerSourcePostConfirmation.Type + expect( + awsCompileCognitoUserPoolEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLambdaPermissionCognitoUserPoolMyUserPool2TriggerSourcePostConfirmation + .Type ).to.equal('AWS::Lambda::Permission'); }); @@ -205,37 +215,43 @@ describe('AwsCompileCognitoUserPoolEvents', () => { awsCompileCognitoUserPoolEvents.compileCognitoUserPoolEvents(); - expect(awsCompileCognitoUserPoolEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources.CognitoUserPoolMyUserPool1.Type + expect( + awsCompileCognitoUserPoolEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.CognitoUserPoolMyUserPool1.Type ).to.equal('AWS::Cognito::UserPool'); - expect(awsCompileCognitoUserPoolEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources.CognitoUserPoolMyUserPool1.DependsOn + expect( + awsCompileCognitoUserPoolEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.CognitoUserPoolMyUserPool1.DependsOn ).to.have.lengthOf(1); - expect(awsCompileCognitoUserPoolEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources.CognitoUserPoolMyUserPool1 - .Properties.LambdaConfig.PreSignUp['Fn::GetAtt'][0] - ).to.equal(serverless.service.serverless.getProvider('aws') - .naming.getLambdaLogicalId('first')); + expect( + awsCompileCognitoUserPoolEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.CognitoUserPoolMyUserPool1.Properties.LambdaConfig.PreSignUp['Fn::GetAtt'][0] + ).to.equal( + serverless.service.serverless.getProvider('aws').naming.getLambdaLogicalId('first') + ); - expect(awsCompileCognitoUserPoolEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources.CognitoUserPoolMyUserPool2.Type + expect( + awsCompileCognitoUserPoolEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.CognitoUserPoolMyUserPool2.Type ).to.equal('AWS::Cognito::UserPool'); - expect(awsCompileCognitoUserPoolEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources.CognitoUserPoolMyUserPool2.DependsOn + expect( + awsCompileCognitoUserPoolEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.CognitoUserPoolMyUserPool2.DependsOn ).to.have.lengthOf(1); - expect(awsCompileCognitoUserPoolEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources.CognitoUserPoolMyUserPool2 - .Properties.LambdaConfig.PreSignUp['Fn::GetAtt'][0] - ).to.equal(serverless.service.serverless.getProvider('aws') - .naming.getLambdaLogicalId('second')); + expect( + awsCompileCognitoUserPoolEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.CognitoUserPoolMyUserPool2.Properties.LambdaConfig.PreSignUp['Fn::GetAtt'][0] + ).to.equal( + serverless.service.serverless.getProvider('aws').naming.getLambdaLogicalId('second') + ); - expect(awsCompileCognitoUserPoolEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionCognitoUserPoolMyUserPool1TriggerSourcePreSignUp.Type + expect( + awsCompileCognitoUserPoolEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLambdaPermissionCognitoUserPoolMyUserPool1TriggerSourcePreSignUp.Type ).to.equal('AWS::Lambda::Permission'); - expect(awsCompileCognitoUserPoolEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources - .SecondLambdaPermissionCognitoUserPoolMyUserPool2TriggerSourcePreSignUp.Type + expect( + awsCompileCognitoUserPoolEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.SecondLambdaPermissionCognitoUserPoolMyUserPool2TriggerSourcePreSignUp.Type ).to.equal('AWS::Lambda::Permission'); }); @@ -265,25 +281,28 @@ describe('AwsCompileCognitoUserPoolEvents', () => { awsCompileCognitoUserPoolEvents.compileCognitoUserPoolEvents(); - expect(awsCompileCognitoUserPoolEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources - .CognitoUserPoolMyUserPool.Type + expect( + awsCompileCognitoUserPoolEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.CognitoUserPoolMyUserPool.Type ).to.equal('AWS::Cognito::UserPool'); - expect(_.keys(awsCompileCognitoUserPoolEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources - .CognitoUserPoolMyUserPool.Properties.LambdaConfig) + expect( + _.keys( + awsCompileCognitoUserPoolEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.CognitoUserPoolMyUserPool.Properties.LambdaConfig + ) ).to.have.lengthOf(2); - expect(awsCompileCognitoUserPoolEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources - .CognitoUserPoolMyUserPool.DependsOn + expect( + awsCompileCognitoUserPoolEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.CognitoUserPoolMyUserPool.DependsOn ).to.have.lengthOf(2); - expect(awsCompileCognitoUserPoolEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionCognitoUserPoolMyUserPoolTriggerSourcePreSignUp.Type + expect( + awsCompileCognitoUserPoolEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLambdaPermissionCognitoUserPoolMyUserPoolTriggerSourcePreSignUp.Type ).to.equal('AWS::Lambda::Permission'); - expect(awsCompileCognitoUserPoolEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources - .SecondLambdaPermissionCognitoUserPoolMyUserPoolTriggerSourcePostConfirmation.Type + expect( + awsCompileCognitoUserPoolEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.SecondLambdaPermissionCognitoUserPoolMyUserPoolTriggerSourcePostConfirmation + .Type ).to.equal('AWS::Lambda::Permission'); }); @@ -297,8 +316,8 @@ describe('AwsCompileCognitoUserPoolEvents', () => { awsCompileCognitoUserPoolEvents.compileCognitoUserPoolEvents(); expect( - awsCompileCognitoUserPoolEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources + awsCompileCognitoUserPoolEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources ).to.deep.equal({}); }); }); @@ -322,21 +341,25 @@ describe('AwsCompileCognitoUserPoolEvents', () => { awsCompileCognitoUserPoolEvents.compileCognitoUserPoolEvents(); awsCompileCognitoUserPoolEvents.mergeWithCustomResources(); - expect(awsCompileCognitoUserPoolEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources - .CognitoUserPoolMyUserPool.Type + expect( + awsCompileCognitoUserPoolEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.CognitoUserPoolMyUserPool.Type ).to.equal('AWS::Cognito::UserPool'); - expect(_.keys(awsCompileCognitoUserPoolEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources - .CognitoUserPoolMyUserPool.Properties) + expect( + _.keys( + awsCompileCognitoUserPoolEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.CognitoUserPoolMyUserPool.Properties + ) ).to.have.lengthOf(2); - expect(_.keys(awsCompileCognitoUserPoolEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources - .CognitoUserPoolMyUserPool.Properties.LambdaConfig) + expect( + _.keys( + awsCompileCognitoUserPoolEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.CognitoUserPoolMyUserPool.Properties.LambdaConfig + ) ).to.have.lengthOf(1); - expect(awsCompileCognitoUserPoolEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionCognitoUserPoolMyUserPoolTriggerSourcePreSignUp.Type + expect( + awsCompileCognitoUserPoolEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLambdaPermissionCognitoUserPoolMyUserPoolTriggerSourcePreSignUp.Type ).to.equal('AWS::Lambda::Permission'); }); @@ -369,25 +392,29 @@ describe('AwsCompileCognitoUserPoolEvents', () => { awsCompileCognitoUserPoolEvents.compileCognitoUserPoolEvents(); awsCompileCognitoUserPoolEvents.mergeWithCustomResources(); - expect(awsCompileCognitoUserPoolEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources - .CognitoUserPoolMyUserPool.Type + expect( + awsCompileCognitoUserPoolEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.CognitoUserPoolMyUserPool.Type ).to.equal('AWS::Cognito::UserPool'); - expect(_.keys(awsCompileCognitoUserPoolEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources - .CognitoUserPoolMyUserPool.Properties) + expect( + _.keys( + awsCompileCognitoUserPoolEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.CognitoUserPoolMyUserPool.Properties + ) ).to.have.lengthOf(6); - expect(awsCompileCognitoUserPoolEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources - .CognitoUserPoolMyUserPool.DependsOn + expect( + awsCompileCognitoUserPoolEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.CognitoUserPoolMyUserPool.DependsOn ).to.have.lengthOf(1); - expect(_.keys(awsCompileCognitoUserPoolEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources - .CognitoUserPoolMyUserPool.Properties.LambdaConfig) + expect( + _.keys( + awsCompileCognitoUserPoolEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.CognitoUserPoolMyUserPool.Properties.LambdaConfig + ) ).to.have.lengthOf(1); - expect(awsCompileCognitoUserPoolEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionCognitoUserPoolMyUserPoolTriggerSourcePreSignUp.Type + expect( + awsCompileCognitoUserPoolEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLambdaPermissionCognitoUserPoolMyUserPoolTriggerSourcePreSignUp.Type ).to.equal('AWS::Lambda::Permission'); }); @@ -421,25 +448,29 @@ describe('AwsCompileCognitoUserPoolEvents', () => { awsCompileCognitoUserPoolEvents.compileCognitoUserPoolEvents(); awsCompileCognitoUserPoolEvents.mergeWithCustomResources(); - expect(awsCompileCognitoUserPoolEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources - .CognitoUserPoolMyUserPool.Type + expect( + awsCompileCognitoUserPoolEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.CognitoUserPoolMyUserPool.Type ).to.equal('AWS::Cognito::UserPool'); - expect(awsCompileCognitoUserPoolEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources - .CognitoUserPoolMyUserPool.DependsOn + expect( + awsCompileCognitoUserPoolEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.CognitoUserPoolMyUserPool.DependsOn ).to.have.lengthOf(4); - expect(_.keys(awsCompileCognitoUserPoolEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources - .CognitoUserPoolMyUserPool.Properties) + expect( + _.keys( + awsCompileCognitoUserPoolEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.CognitoUserPoolMyUserPool.Properties + ) ).to.have.lengthOf(6); - expect(_.keys(awsCompileCognitoUserPoolEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources - .CognitoUserPoolMyUserPool.Properties.LambdaConfig) + expect( + _.keys( + awsCompileCognitoUserPoolEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.CognitoUserPoolMyUserPool.Properties.LambdaConfig + ) ).to.have.lengthOf(1); - expect(awsCompileCognitoUserPoolEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionCognitoUserPoolMyUserPoolTriggerSourcePreSignUp.Type + expect( + awsCompileCognitoUserPoolEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLambdaPermissionCognitoUserPoolMyUserPoolTriggerSourcePreSignUp.Type ).to.equal('AWS::Lambda::Permission'); }); }); diff --git a/lib/plugins/aws/package/compile/events/iot/index.js b/lib/plugins/aws/package/compile/events/iot/index.js index d8d5ef892..0f86e8b7b 100644 --- a/lib/plugins/aws/package/compile/events/iot/index.js +++ b/lib/plugins/aws/package/compile/events/iot/index.js @@ -13,7 +13,7 @@ class AwsCompileIoTEvents { } compileIoTEvents() { - this.serverless.service.getAllFunctions().forEach((functionName) => { + this.serverless.service.getAllFunctions().forEach(functionName => { const functionObj = this.serverless.service.getFunction(functionName); let iotNumberInFunction = 0; @@ -41,24 +41,30 @@ class AwsCompileIoTEvents { `IoT event of function "${functionName}" is not an object`, ' Please check the docs for more info.', ].join(''); - throw new this.serverless.classes - .Error(errorMessage); + throw new this.serverless.classes.Error(errorMessage); } - const lambdaLogicalId = this.provider.naming - .getLambdaLogicalId(functionName); - const iotLogicalId = this.provider.naming - .getIotLogicalId(functionName, iotNumberInFunction); - const lambdaPermissionLogicalId = this.provider.naming - .getLambdaIotPermissionLogicalId(functionName, iotNumberInFunction); + const lambdaLogicalId = this.provider.naming.getLambdaLogicalId(functionName); + const iotLogicalId = this.provider.naming.getIotLogicalId( + functionName, + iotNumberInFunction + ); + const lambdaPermissionLogicalId = this.provider.naming.getLambdaIotPermissionLogicalId( + functionName, + iotNumberInFunction + ); const iotTemplate = ` { "Type": "AWS::IoT::TopicRule", "Properties": { ${RuleName ? `"RuleName": "${RuleName.replace(/\r?\n/g, '')}",` : ''} "TopicRulePayload": { - ${AwsIotSqlVersion ? `"AwsIotSqlVersion": - "${AwsIotSqlVersion.replace(/\r?\n/g, '')}",` : ''} + ${ + AwsIotSqlVersion + ? `"AwsIotSqlVersion": + "${AwsIotSqlVersion.replace(/\r?\n/g, '')}",` + : '' + } ${Description ? `"Description": "${Description.replace(/\r?\n/g, '')}",` : ''} "RuleDisabled": "${RuleDisabled}", "Sql": "${Sql.replace(/\r?\n/g, '')}", @@ -80,7 +86,7 @@ class AwsCompileIoTEvents { "Properties": { "FunctionName": { "Fn::GetAtt": ["${lambdaLogicalId}", "Arn"] }, "Action": "lambda:InvokeFunction", - "Principal": { "Fn::Join": ["", [ "iot.", { "Ref": "AWS::URLSuffix" } ]] }, + "Principal": "iot.amazonaws.com", "SourceArn": { "Fn::Join": ["", [ "arn:", @@ -105,8 +111,11 @@ class AwsCompileIoTEvents { [lambdaPermissionLogicalId]: JSON.parse(permissionTemplate), }; - _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, - newIotObject, newPermissionObject); + _.merge( + this.serverless.service.provider.compiledCloudFormationTemplate.Resources, + newIotObject, + newPermissionObject + ); } }); } diff --git a/lib/plugins/aws/package/compile/events/iot/index.test.js b/lib/plugins/aws/package/compile/events/iot/index.test.js index 44e9c7561..0fe860fbe 100644 --- a/lib/plugins/aws/package/compile/events/iot/index.test.js +++ b/lib/plugins/aws/package/compile/events/iot/index.test.js @@ -57,35 +57,37 @@ describe('AwsCompileIoTEvents', () => { awsCompileIoTEvents.compileIoTEvents(); - expect(awsCompileIoTEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstIotTopicRule1.Type + expect( + awsCompileIoTEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstIotTopicRule1.Type ).to.equal('AWS::IoT::TopicRule'); - expect(awsCompileIoTEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstIotTopicRule2.Type + expect( + awsCompileIoTEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstIotTopicRule2.Type ).to.equal('AWS::IoT::TopicRule'); - expect(awsCompileIoTEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstIotTopicRule1.Properties.TopicRulePayload.Sql - ).to.equal('SELECT * FROM \'topic_1\''); - expect(awsCompileIoTEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstIotTopicRule2.Properties.TopicRulePayload.Sql - ).to.equal('SELECT * FROM \'topic_2\''); - expect(awsCompileIoTEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstIotTopicRule1.Properties.TopicRulePayload.RuleDisabled + expect( + awsCompileIoTEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstIotTopicRule1.Properties.TopicRulePayload.Sql + ).to.equal("SELECT * FROM 'topic_1'"); + expect( + awsCompileIoTEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstIotTopicRule2.Properties.TopicRulePayload.Sql + ).to.equal("SELECT * FROM 'topic_2'"); + expect( + awsCompileIoTEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstIotTopicRule1.Properties.TopicRulePayload.RuleDisabled ).to.equal('false'); - expect(awsCompileIoTEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstIotTopicRule2.Properties.TopicRulePayload.RuleDisabled + expect( + awsCompileIoTEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstIotTopicRule2.Properties.TopicRulePayload.RuleDisabled ).to.equal('false'); - expect(awsCompileIoTEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionIotTopicRule1.Type + expect( + awsCompileIoTEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstLambdaPermissionIotTopicRule1.Type ).to.equal('AWS::Lambda::Permission'); - expect(awsCompileIoTEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionIotTopicRule2.Type + expect( + awsCompileIoTEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstLambdaPermissionIotTopicRule2.Type ).to.equal('AWS::Lambda::Permission'); }); @@ -105,9 +107,9 @@ describe('AwsCompileIoTEvents', () => { awsCompileIoTEvents.compileIoTEvents(); - expect(awsCompileIoTEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstIotTopicRule1.Properties.RuleName + expect( + awsCompileIoTEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstIotTopicRule1.Properties.RuleName ).to.equal('iotEventName'); }); @@ -137,13 +139,13 @@ describe('AwsCompileIoTEvents', () => { awsCompileIoTEvents.compileIoTEvents(); - expect(awsCompileIoTEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstIotTopicRule1.Properties.TopicRulePayload.RuleDisabled + expect( + awsCompileIoTEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstIotTopicRule1.Properties.TopicRulePayload.RuleDisabled ).to.equal('true'); - expect(awsCompileIoTEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .SecondIotTopicRule1.Properties.TopicRulePayload.RuleDisabled + expect( + awsCompileIoTEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .SecondIotTopicRule1.Properties.TopicRulePayload.RuleDisabled ).to.equal('false'); }); @@ -163,9 +165,9 @@ describe('AwsCompileIoTEvents', () => { awsCompileIoTEvents.compileIoTEvents(); - expect(awsCompileIoTEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstIotTopicRule1.Properties.TopicRulePayload.AwsIotSqlVersion + expect( + awsCompileIoTEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstIotTopicRule1.Properties.TopicRulePayload.AwsIotSqlVersion ).to.equal('2016-03-23'); }); @@ -185,9 +187,9 @@ describe('AwsCompileIoTEvents', () => { awsCompileIoTEvents.compileIoTEvents(); - expect(awsCompileIoTEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstIotTopicRule1.Properties.TopicRulePayload.Description + expect( + awsCompileIoTEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstIotTopicRule1.Properties.TopicRulePayload.Description ).to.equal('iot event description'); }); @@ -206,9 +208,9 @@ describe('AwsCompileIoTEvents', () => { awsCompileIoTEvents.compileIoTEvents(); - expect(awsCompileIoTEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstIotTopicRule1.Properties.TopicRulePayload.RuleDisabled + expect( + awsCompileIoTEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstIotTopicRule1.Properties.TopicRulePayload.RuleDisabled ).to.equal('false'); }); @@ -229,21 +231,21 @@ describe('AwsCompileIoTEvents', () => { }; awsCompileIoTEvents.compileIoTEvents(); - expect(awsCompileIoTEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstIotTopicRule1.Properties.TopicRulePayload.Sql + expect( + awsCompileIoTEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstIotTopicRule1.Properties.TopicRulePayload.Sql ).to.equal("SELECT * FROM 'topic_1' WHERE value = 2"); - expect(awsCompileIoTEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstIotTopicRule1.Properties.TopicRulePayload.AwsIotSqlVersion + expect( + awsCompileIoTEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstIotTopicRule1.Properties.TopicRulePayload.AwsIotSqlVersion ).to.equal('beta'); - expect(awsCompileIoTEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstIotTopicRule1.Properties.TopicRulePayload.Description + expect( + awsCompileIoTEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstIotTopicRule1.Properties.TopicRulePayload.Description ).to.equal('iot event description with newline'); - expect(awsCompileIoTEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstIotTopicRule1.Properties.RuleName + expect( + awsCompileIoTEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstIotTopicRule1.Properties.RuleName ).to.equal('iotEventName'); }); @@ -256,8 +258,8 @@ describe('AwsCompileIoTEvents', () => { awsCompileIoTEvents.compileIoTEvents(); - expect(awsCompileIoTEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources + expect( + awsCompileIoTEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources ).to.deep.equal({}); }); }); diff --git a/lib/plugins/aws/package/compile/events/s3/index.js b/lib/plugins/aws/package/compile/events/s3/index.js index 4c2c9dc4a..4bcf943f5 100644 --- a/lib/plugins/aws/package/compile/events/s3/index.js +++ b/lib/plugins/aws/package/compile/events/s3/index.js @@ -1,26 +1,36 @@ 'use strict'; const _ = require('lodash'); +const BbPromise = require('bluebird'); +const { addCustomResourceToService } = require('../../../../customResources'); class AwsCompileS3Events { - constructor(serverless) { + constructor(serverless, options) { this.serverless = serverless; + this.options = options; this.provider = this.serverless.getProvider('aws'); this.hooks = { - 'package:compileEvents': this.compileS3Events.bind(this), + 'package:compileEvents': () => { + return BbPromise.bind(this) + .then(this.newS3Buckets) + .then(this.existingS3Buckets); + }, }; } - compileS3Events() { + newS3Buckets() { const bucketsLambdaConfigurations = {}; const s3EnabledFunctions = []; - this.serverless.service.getAllFunctions().forEach((functionName) => { + this.serverless.service.getAllFunctions().forEach(functionName => { const functionObj = this.serverless.service.getFunction(functionName); if (functionObj.events) { functionObj.events.forEach(event => { if (event.s3) { + // return immediately if it's an existing S3 event since we treat them differently + if (event.s3.existing) return null; + let bucketName; let notificationEvent = 's3:ObjectCreated:*'; let filter = {}; @@ -32,8 +42,7 @@ class AwsCompileS3Events { ' The correct syntax is: s3: bucketName OR an object with "bucket" property.', ' Please check the docs for more info.', ].join(''); - throw new this.serverless.classes - .Error(errorMessage); + throw new this.serverless.classes.Error(errorMessage); } bucketName = event.s3.bucket; if (event.s3.event) { @@ -46,8 +55,7 @@ class AwsCompileS3Events { ' The correct syntax is: rules: [{ Name: Value }]', ' Please check the docs for more info.', ].join(''); - throw new this.serverless.classes - .Error(errorMessage); + throw new this.serverless.classes.Error(errorMessage); } const rules = []; event.s3.rules.forEach(rule => { @@ -57,8 +65,7 @@ class AwsCompileS3Events { ' The correct syntax is: { Name: Value }', ' Please check the docs for more info.', ].join(''); - throw new this.serverless.classes - .Error(errorMessage); + throw new this.serverless.classes.Error(errorMessage); } const name = Object.keys(rule)[0]; const value = rule[name]; @@ -74,12 +81,10 @@ class AwsCompileS3Events { ' The correct syntax is: s3: bucketName OR an object with "bucket" property.', ' Please check the docs for more info.', ].join(''); - throw new this.serverless.classes - .Error(errorMessage); + throw new this.serverless.classes.Error(errorMessage); } - const lambdaLogicalId = this.provider.naming - .getLambdaLogicalId(functionName); + const lambdaLogicalId = this.provider.naming.getLambdaLogicalId(functionName); // check if the bucket already defined // in another S3 event in the service @@ -87,29 +92,19 @@ class AwsCompileS3Events { let newLambdaConfiguration = { Event: notificationEvent, Function: { - 'Fn::GetAtt': [ - lambdaLogicalId, - 'Arn', - ], + 'Fn::GetAtt': [lambdaLogicalId, 'Arn'], }, }; // Assign 'filter' if not empty - newLambdaConfiguration = _.assign( - newLambdaConfiguration, - filter - ); - bucketsLambdaConfigurations[bucketName] - .push(newLambdaConfiguration); + newLambdaConfiguration = _.assign(newLambdaConfiguration, filter); + bucketsLambdaConfigurations[bucketName].push(newLambdaConfiguration); } else { bucketsLambdaConfigurations[bucketName] = [ { Event: notificationEvent, Function: { - 'Fn::GetAtt': [ - lambdaLogicalId, - 'Arn', - ], + 'Fn::GetAtt': [lambdaLogicalId, 'Arn'], }, }, ]; @@ -122,6 +117,8 @@ class AwsCompileS3Events { const s3EnabledFunction = { functionName, bucketName }; s3EnabledFunctions.push(s3EnabledFunction); } + + return null; }); } }); @@ -141,64 +138,144 @@ class AwsCompileS3Events { }; // create the DependsOn properties for the buckets permissions (which are created later on) - const dependsOnToCreate = s3EnabledFunctions - .filter(func => func.bucketName === bucketName); + const dependsOnToCreate = s3EnabledFunctions.filter(func => func.bucketName === bucketName); - _.forEach(dependsOnToCreate, (item) => { - const lambdaPermissionLogicalId = this.provider.naming - .getLambdaS3PermissionLogicalId(item.functionName, - item.bucketName); + _.forEach(dependsOnToCreate, item => { + const lambdaPermissionLogicalId = this.provider.naming.getLambdaS3PermissionLogicalId( + item.functionName, + item.bucketName + ); bucketTemplate.DependsOn.push(lambdaPermissionLogicalId); }); - const bucketLogicalId = this.provider.naming - .getBucketLogicalId(bucketName); + const bucketLogicalId = this.provider.naming.getBucketLogicalId(bucketName); const bucketCFResource = { [bucketLogicalId]: bucketTemplate, }; - _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, - bucketCFResource); + _.merge( + this.serverless.service.provider.compiledCloudFormationTemplate.Resources, + bucketCFResource + ); }); // iterate over all functions with S3 events // and give S3 permission to invoke them all // by adding Lambda::Permission resource for each s3EnabledFunctions.forEach(s3EnabledFunction => { - const lambdaLogicalId = this.provider.naming - .getLambdaLogicalId(s3EnabledFunction.functionName); + const lambdaLogicalId = this.provider.naming.getLambdaLogicalId( + s3EnabledFunction.functionName + ); const permissionTemplate = { Type: 'AWS::Lambda::Permission', Properties: { FunctionName: { - 'Fn::GetAtt': [ - lambdaLogicalId, - 'Arn', - ], + 'Fn::GetAtt': [lambdaLogicalId, 'Arn'], }, Action: 'lambda:InvokeFunction', - Principal: { 'Fn::Join': ['', ['s3.', { Ref: 'AWS::URLSuffix' }]] }, + Principal: 's3.amazonaws.com', SourceArn: { - 'Fn::Join': ['', - [ - 'arn:', - { Ref: 'AWS::Partition' }, - `:s3:::${s3EnabledFunction.bucketName}`, - ], + 'Fn::Join': [ + '', + ['arn:', { Ref: 'AWS::Partition' }, `:s3:::${s3EnabledFunction.bucketName}`], ], }, }, }; - const lambdaPermissionLogicalId = this.provider.naming - .getLambdaS3PermissionLogicalId(s3EnabledFunction.functionName, - s3EnabledFunction.bucketName); + const lambdaPermissionLogicalId = this.provider.naming.getLambdaS3PermissionLogicalId( + s3EnabledFunction.functionName, + s3EnabledFunction.bucketName + ); const permissionCFResource = { [lambdaPermissionLogicalId]: permissionTemplate, }; - _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, - permissionCFResource); + _.merge( + this.serverless.service.provider.compiledCloudFormationTemplate.Resources, + permissionCFResource + ); }); } + + existingS3Buckets() { + const { service } = this.serverless; + const { provider } = service; + const { compiledCloudFormationTemplate } = provider; + const iamRoleStatements = []; + + service.getAllFunctions().forEach(functionName => { + let funcUsesExistingS3Bucket = false; + const functionObj = service.getFunction(functionName); + const FunctionName = functionObj.name; + + if (functionObj.events) { + functionObj.events.forEach((event, idx) => { + if (event.s3 && _.isObject(event.s3) && event.s3.existing) { + let rules = null; + idx = idx += 1; + const bucket = event.s3.bucket; + const notificationEvent = event.s3.event || 's3:ObjectCreated:*'; + funcUsesExistingS3Bucket = true; + + rules = _.map(event.s3.rules, rule => { + const key = Object.keys(rule)[0]; + const value = rule[key]; + return { + [_.startCase(key)]: value, + }; + }); + + const eventFunctionLogicalId = this.provider.naming.getLambdaLogicalId(functionName); + const customResourceFunctionLogicalId = this.provider.naming.getCustomResourceS3HandlerFunctionLogicalId(); + const customS3ResourceLogicalId = this.provider.naming.getCustomResourceS3ResourceLogicalId( + functionName, + idx + ); + + const customS3Resource = { + [customS3ResourceLogicalId]: { + Type: 'Custom::S3', + Version: 1.0, + DependsOn: [eventFunctionLogicalId, customResourceFunctionLogicalId], + Properties: { + ServiceToken: { + 'Fn::GetAtt': [customResourceFunctionLogicalId, 'Arn'], + }, + FunctionName, + BucketName: bucket, + BucketConfig: { + Event: notificationEvent, + Rules: rules, + }, + }, + }, + }; + + _.merge(compiledCloudFormationTemplate.Resources, customS3Resource); + + iamRoleStatements.push({ + Effect: 'Allow', + Resource: `arn:aws:s3:::${bucket}`, + Action: ['s3:PutBucketNotification', 's3:GetBucketNotification'], + }); + } + }); + } + + if (funcUsesExistingS3Bucket) { + iamRoleStatements.push({ + Effect: 'Allow', + Resource: `arn:aws:lambda:*:*:function:${FunctionName}`, + Action: ['lambda:AddPermission', 'lambda:RemovePermission'], + }); + } + }); + + if (iamRoleStatements.length) { + return addCustomResourceToService.call(this, 's3', iamRoleStatements); + } + + return null; + } } module.exports = AwsCompileS3Events; diff --git a/lib/plugins/aws/package/compile/events/s3/index.test.js b/lib/plugins/aws/package/compile/events/s3/index.test.js index 00c085d83..496d94cb0 100644 --- a/lib/plugins/aws/package/compile/events/s3/index.test.js +++ b/lib/plugins/aws/package/compile/events/s3/index.test.js @@ -1,9 +1,16 @@ 'use strict'; -const expect = require('chai').expect; +/* eslint-disable no-unused-expressions */ + +const sinon = require('sinon'); +const chai = require('chai'); const AwsProvider = require('../../../../provider/awsProvider'); const AwsCompileS3Events = require('./index'); const Serverless = require('../../../../../../Serverless'); +const customResources = require('../../../../customResources'); + +const { expect } = chai; +chai.use(require('sinon-chai')); describe('AwsCompileS3Events', () => { let serverless; @@ -22,7 +29,7 @@ describe('AwsCompileS3Events', () => { expect(awsCompileS3Events.provider).to.be.instanceof(AwsProvider)); }); - describe('#compileS3Events()', () => { + describe('#newS3Buckets()', () => { it('should throw an error if s3 event type is not a string or an object', () => { awsCompileS3Events.serverless.service.functions = { first: { @@ -34,7 +41,7 @@ describe('AwsCompileS3Events', () => { }, }; - expect(() => awsCompileS3Events.compileS3Events()).to.throw(Error); + expect(() => awsCompileS3Events.newS3Buckets()).to.throw(Error); }); it('should throw an error if the "bucket" property is not given', () => { @@ -50,7 +57,7 @@ describe('AwsCompileS3Events', () => { }, }; - expect(() => awsCompileS3Events.compileS3Events()).to.throw(Error); + expect(() => awsCompileS3Events.newS3Buckets()).to.throw(Error); }); it('should throw an error if the "rules" property is not an array', () => { @@ -68,7 +75,7 @@ describe('AwsCompileS3Events', () => { }, }; - expect(() => awsCompileS3Events.compileS3Events()).to.throw(Error); + expect(() => awsCompileS3Events.newS3Buckets()).to.throw(Error); }); it('should throw an error if the "rules" property is invalid', () => { @@ -86,7 +93,7 @@ describe('AwsCompileS3Events', () => { }, }; - expect(() => awsCompileS3Events.compileS3Events()).to.throw(Error); + expect(() => awsCompileS3Events.newS3Buckets()).to.throw(Error); }); it('should create corresponding resources when S3 events are given', () => { @@ -100,34 +107,38 @@ describe('AwsCompileS3Events', () => { s3: { bucket: 'first-function-bucket-two', event: 's3:ObjectCreated:Put', - rules: [ - { prefix: 'subfolder/' }, - ], + rules: [{ prefix: 'subfolder/' }], }, }, ], }, }; - awsCompileS3Events.compileS3Events(); + awsCompileS3Events.newS3Buckets(); - expect(awsCompileS3Events.serverless.service.provider.compiledCloudFormationTemplate - .Resources.S3BucketFirstfunctionbucketone.Type + expect( + awsCompileS3Events.serverless.service.provider.compiledCloudFormationTemplate.Resources + .S3BucketFirstfunctionbucketone.Type ).to.equal('AWS::S3::Bucket'); - expect(awsCompileS3Events.serverless.service.provider.compiledCloudFormationTemplate - .Resources.S3BucketFirstfunctionbuckettwo.Type + expect( + awsCompileS3Events.serverless.service.provider.compiledCloudFormationTemplate.Resources + .S3BucketFirstfunctionbuckettwo.Type ).to.equal('AWS::S3::Bucket'); - expect(awsCompileS3Events.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FirstLambdaPermissionFirstfunctionbucketoneS3.Type + expect( + awsCompileS3Events.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstLambdaPermissionFirstfunctionbucketoneS3.Type ).to.equal('AWS::Lambda::Permission'); - expect(awsCompileS3Events.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FirstLambdaPermissionFirstfunctionbuckettwoS3.Type + expect( + awsCompileS3Events.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstLambdaPermissionFirstfunctionbuckettwoS3.Type ).to.equal('AWS::Lambda::Permission'); - expect(awsCompileS3Events.serverless.service.provider.compiledCloudFormationTemplate - .Resources.S3BucketFirstfunctionbuckettwo.Properties.NotificationConfiguration - .LambdaConfigurations[0].Filter).to.deep.equal({ - S3Key: { Rules: [{ Name: 'prefix', Value: 'subfolder/' }] }, - }); + expect( + awsCompileS3Events.serverless.service.provider.compiledCloudFormationTemplate.Resources + .S3BucketFirstfunctionbuckettwo.Properties.NotificationConfiguration + .LambdaConfigurations[0].Filter + ).to.deep.equal({ + S3Key: { Rules: [{ Name: 'prefix', Value: 'subfolder/' }] }, + }); }); it('should create single bucket resource when the same bucket referenced repeatedly', () => { @@ -141,25 +152,27 @@ describe('AwsCompileS3Events', () => { s3: { bucket: 'first-function-bucket-one', event: 's3:ObjectCreated:Put', - rules: [ - { prefix: 'subfolder/' }, - ], + rules: [{ prefix: 'subfolder/' }], }, }, ], }, }; - awsCompileS3Events.compileS3Events(); + awsCompileS3Events.newS3Buckets(); - expect(awsCompileS3Events.serverless.service.provider.compiledCloudFormationTemplate - .Resources.S3BucketFirstfunctionbucketone.Type + expect( + awsCompileS3Events.serverless.service.provider.compiledCloudFormationTemplate.Resources + .S3BucketFirstfunctionbucketone.Type ).to.equal('AWS::S3::Bucket'); - expect(awsCompileS3Events.serverless.service.provider.compiledCloudFormationTemplate - .Resources.S3BucketFirstfunctionbucketone.Properties.NotificationConfiguration - .LambdaConfigurations.length).to.equal(2); - expect(awsCompileS3Events.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FirstLambdaPermissionFirstfunctionbucketoneS3.Type + expect( + awsCompileS3Events.serverless.service.provider.compiledCloudFormationTemplate.Resources + .S3BucketFirstfunctionbucketone.Properties.NotificationConfiguration.LambdaConfigurations + .length + ).to.equal(2); + expect( + awsCompileS3Events.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstLambdaPermissionFirstfunctionbucketoneS3.Type ).to.equal('AWS::Lambda::Permission'); }); @@ -179,25 +192,31 @@ describe('AwsCompileS3Events', () => { }, }; - awsCompileS3Events.compileS3Events(); + awsCompileS3Events.newS3Buckets(); - expect(awsCompileS3Events.serverless.service.provider.compiledCloudFormationTemplate - .Resources.S3BucketFirstfunctionbucketone.Type + expect( + awsCompileS3Events.serverless.service.provider.compiledCloudFormationTemplate.Resources + .S3BucketFirstfunctionbucketone.Type ).to.equal('AWS::S3::Bucket'); - expect(awsCompileS3Events.serverless.service.provider.compiledCloudFormationTemplate - .Resources.S3BucketFirstfunctionbuckettwo.Type + expect( + awsCompileS3Events.serverless.service.provider.compiledCloudFormationTemplate.Resources + .S3BucketFirstfunctionbuckettwo.Type ).to.equal('AWS::S3::Bucket'); - expect(awsCompileS3Events.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FirstLambdaPermissionFirstfunctionbucketoneS3.Type + expect( + awsCompileS3Events.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstLambdaPermissionFirstfunctionbucketoneS3.Type ).to.equal('AWS::Lambda::Permission'); - expect(awsCompileS3Events.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FirstLambdaPermissionFirstfunctionbuckettwoS3.Type + expect( + awsCompileS3Events.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstLambdaPermissionFirstfunctionbuckettwoS3.Type ).to.equal('AWS::Lambda::Permission'); - expect(awsCompileS3Events.serverless.service.provider.compiledCloudFormationTemplate - .Resources.S3BucketFirstfunctionbucketone.DependsOn + expect( + awsCompileS3Events.serverless.service.provider.compiledCloudFormationTemplate.Resources + .S3BucketFirstfunctionbucketone.DependsOn ).to.deep.equal(['FirstLambdaPermissionFirstfunctionbucketoneS3']); - expect(awsCompileS3Events.serverless.service.provider.compiledCloudFormationTemplate - .Resources.S3BucketFirstfunctionbuckettwo.DependsOn + expect( + awsCompileS3Events.serverless.service.provider.compiledCloudFormationTemplate.Resources + .S3BucketFirstfunctionbuckettwo.DependsOn ).to.deep.equal(['FirstLambdaPermissionFirstfunctionbuckettwoS3']); }); @@ -208,11 +227,106 @@ describe('AwsCompileS3Events', () => { }, }; - awsCompileS3Events.compileS3Events(); + awsCompileS3Events.newS3Buckets(); expect( awsCompileS3Events.serverless.service.provider.compiledCloudFormationTemplate.Resources ).to.deep.equal({}); }); }); + + describe('#existingS3Buckets()', () => { + let addCustomResourceToServiceStub; + + beforeEach(() => { + addCustomResourceToServiceStub = sinon + .stub(customResources.addCustomResourceToService, 'call') + .resolves(); + }); + + afterEach(() => { + customResources.addCustomResourceToService.call.restore(); + }); + + it('should create the necessary resources for the most minimal configuration', () => { + awsCompileS3Events.serverless.service.functions = { + first: { + name: 'first', + events: [ + { + s3: { + bucket: 'existing-s3-bucket', + existing: true, + }, + }, + ], + }, + }; + + awsCompileS3Events.existingS3Buckets(); + + const { + Resources, + } = awsCompileS3Events.serverless.service.provider.compiledCloudFormationTemplate; + + expect(addCustomResourceToServiceStub).to.have.been.calledOnce; + expect(addCustomResourceToServiceStub.args[0][1]).to.equal('s3'); + expect(Resources.FirstCustomS31).to.deep.equal({ + Type: 'Custom::S3', + Version: 1, + DependsOn: ['FirstLambdaFunction', 'CustomDashresourceDashexistingDashs3LambdaFunction'], + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomDashresourceDashexistingDashs3LambdaFunction', 'Arn'], + }, + FunctionName: 'first', + BucketName: 'existing-s3-bucket', + BucketConfig: { Event: 's3:ObjectCreated:*', Rules: [] }, + }, + }); + }); + + it('should create the necessary resources for a service using different config parameters', () => { + awsCompileS3Events.serverless.service.functions = { + first: { + name: 'second', + events: [ + { + s3: { + bucket: 'existing-s3-bucket', + event: 's3:ObjectCreated:Put', + rules: [{ prefix: 'uploads' }, { suffix: '.jpg' }], + existing: true, + }, + }, + ], + }, + }; + + awsCompileS3Events.existingS3Buckets(); + + const { + Resources, + } = awsCompileS3Events.serverless.service.provider.compiledCloudFormationTemplate; + + expect(addCustomResourceToServiceStub).to.have.been.calledOnce; + expect(addCustomResourceToServiceStub.args[0][1]).to.equal('s3'); + expect(Resources.FirstCustomS31).to.deep.equal({ + Type: 'Custom::S3', + Version: 1, + DependsOn: ['FirstLambdaFunction', 'CustomDashresourceDashexistingDashs3LambdaFunction'], + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomDashresourceDashexistingDashs3LambdaFunction', 'Arn'], + }, + FunctionName: 'second', + BucketName: 'existing-s3-bucket', + BucketConfig: { + Event: 's3:ObjectCreated:Put', + Rules: [{ Prefix: 'uploads' }, { Suffix: '.jpg' }], + }, + }, + }); + }); + }); }); diff --git a/lib/plugins/aws/package/compile/events/schedule/index.js b/lib/plugins/aws/package/compile/events/schedule/index.js index e44e53506..938ab925e 100644 --- a/lib/plugins/aws/package/compile/events/schedule/index.js +++ b/lib/plugins/aws/package/compile/events/schedule/index.js @@ -2,10 +2,8 @@ const _ = require('lodash'); -const rateSyntaxPattern = - /^rate\((?:1 (?:minute|hour|day)|(?:1\d+|[2-9]\d*) (?:minute|hour|day)s)\)$/; -const cronSyntaxPattern = - /^cron\(\S+ \S+ \S+ \S+ \S+ \S+\)$/; +const rateSyntaxPattern = /^rate\((?:1 (?:minute|hour|day)|(?:1\d+|[2-9]\d*) (?:minute|hour|day)s)\)$/; +const cronSyntaxPattern = /^cron\(\S+ \S+ \S+ \S+ \S+ \S+\)$/; class AwsCompileScheduledEvents { constructor(serverless) { @@ -28,7 +26,7 @@ class AwsCompileScheduledEvents { } compileScheduledEvents() { - this.serverless.service.getAllFunctions().forEach((functionName) => { + this.serverless.service.getAllFunctions().forEach(functionName => { const functionObj = this.serverless.service.getFunction(functionName); let scheduleNumberInFunction = 0; @@ -47,8 +45,7 @@ class AwsCompileScheduledEvents { if (typeof event.schedule === 'object') { if (!this.validateScheduleSyntax(event.schedule.rate)) { const errorMessage = this.buildValidationErrorMessage(functionName); - throw new this.serverless.classes - .Error(errorMessage); + throw new this.serverless.classes.Error(errorMessage); } ScheduleExpression = event.schedule.rate; State = 'ENABLED'; @@ -68,8 +65,7 @@ class AwsCompileScheduledEvents { 'properties at the same time for schedule events. ', 'Please check the AWS docs for more info', ].join(''); - throw new this.serverless.classes - .Error(errorMessage); + throw new this.serverless.classes.Error(errorMessage); } if (Input && typeof Input === 'object') { @@ -83,8 +79,7 @@ class AwsCompileScheduledEvents { ' but it failed to parse to a JSON object.', ' Please check the docs for more info.', ].join(''); - throw new this.serverless.classes - .Error(errorMessage); + throw new this.serverless.classes.Error(errorMessage); } } Input = JSON.stringify(Input); @@ -101,16 +96,18 @@ class AwsCompileScheduledEvents { State = 'ENABLED'; } else { const errorMessage = this.buildValidationErrorMessage(functionName); - throw new this.serverless.classes - .Error(errorMessage); + throw new this.serverless.classes.Error(errorMessage); } - const lambdaLogicalId = this.provider.naming - .getLambdaLogicalId(functionName); - const scheduleLogicalId = this.provider.naming - .getScheduleLogicalId(functionName, scheduleNumberInFunction); - const lambdaPermissionLogicalId = this.provider.naming - .getLambdaSchedulePermissionLogicalId(functionName, scheduleNumberInFunction); + const lambdaLogicalId = this.provider.naming.getLambdaLogicalId(functionName); + const scheduleLogicalId = this.provider.naming.getScheduleLogicalId( + functionName, + scheduleNumberInFunction + ); + const lambdaPermissionLogicalId = this.provider.naming.getLambdaSchedulePermissionLogicalId( + functionName, + scheduleNumberInFunction + ); const scheduleId = this.provider.naming.getScheduleId(functionName); const scheduleTemplate = ` @@ -136,10 +133,9 @@ class AwsCompileScheduledEvents { { "Type": "AWS::Lambda::Permission", "Properties": { - "FunctionName": { "Fn::GetAtt": ["${ - lambdaLogicalId}", "Arn"] }, + "FunctionName": { "Fn::GetAtt": ["${lambdaLogicalId}", "Arn"] }, "Action": "lambda:InvokeFunction", - "Principal": { "Fn::Join": ["", [ "events.", { "Ref": "AWS::URLSuffix" } ]] }, + "Principal": "events.amazonaws.com", "SourceArn": { "Fn::GetAtt": ["${scheduleLogicalId}", "Arn"] } } } @@ -153,8 +149,11 @@ class AwsCompileScheduledEvents { [lambdaPermissionLogicalId]: JSON.parse(permissionTemplate), }; - _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, - newScheduleObject, newPermissionObject); + _.merge( + this.serverless.service.provider.compiledCloudFormationTemplate.Resources, + newScheduleObject, + newPermissionObject + ); } }); } @@ -162,15 +161,16 @@ class AwsCompileScheduledEvents { } validateScheduleSyntax(input) { - return typeof input === 'string' && - (rateSyntaxPattern.test(input) || cronSyntaxPattern.test(input)); + return ( + typeof input === 'string' && (rateSyntaxPattern.test(input) || cronSyntaxPattern.test(input)) + ); } formatInputTransformer(inputTransformer) { if (!inputTransformer.inputTemplate) { throw new this.serverless.classes.Error( 'The inputTemplate key is required when specifying an ' + - 'inputTransformer for a schedule event' + 'inputTransformer for a schedule event' ); } const cfmOutput = { diff --git a/lib/plugins/aws/package/compile/events/schedule/index.test.js b/lib/plugins/aws/package/compile/events/schedule/index.test.js index 1be50220e..0ea7e2fb4 100644 --- a/lib/plugins/aws/package/compile/events/schedule/index.test.js +++ b/lib/plugins/aws/package/compile/events/schedule/index.test.js @@ -213,30 +213,33 @@ describe('AwsCompileScheduledEvents', () => { awsCompileScheduledEvents.compileScheduledEvents(); - expect(awsCompileScheduledEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventsRuleSchedule1.Type + expect( + awsCompileScheduledEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventsRuleSchedule1.Type ).to.equal('AWS::Events::Rule'); - expect(awsCompileScheduledEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventsRuleSchedule2.Type + expect( + awsCompileScheduledEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventsRuleSchedule2.Type ).to.equal('AWS::Events::Rule'); - expect(awsCompileScheduledEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventsRuleSchedule3.Type + expect( + awsCompileScheduledEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventsRuleSchedule3.Type ).to.equal('AWS::Events::Rule'); - expect(awsCompileScheduledEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionEventsRuleSchedule1.Type + expect( + awsCompileScheduledEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLambdaPermissionEventsRuleSchedule1.Type ).to.equal('AWS::Lambda::Permission'); - expect(awsCompileScheduledEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionEventsRuleSchedule2.Type + expect( + awsCompileScheduledEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLambdaPermissionEventsRuleSchedule2.Type ).to.equal('AWS::Lambda::Permission'); - expect(awsCompileScheduledEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionEventsRuleSchedule3.Type + expect( + awsCompileScheduledEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLambdaPermissionEventsRuleSchedule3.Type ).to.equal('AWS::Lambda::Permission'); - expect(awsCompileScheduledEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstLambdaPermissionEventsRuleSchedule4.Type + expect( + awsCompileScheduledEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstLambdaPermissionEventsRuleSchedule4.Type ).to.equal('AWS::Lambda::Permission'); }); @@ -270,21 +273,21 @@ describe('AwsCompileScheduledEvents', () => { awsCompileScheduledEvents.compileScheduledEvents(); - expect(awsCompileScheduledEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventsRuleSchedule1 - .Properties.State + expect( + awsCompileScheduledEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventsRuleSchedule1.Properties.State ).to.equal('DISABLED'); - expect(awsCompileScheduledEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventsRuleSchedule2 - .Properties.State + expect( + awsCompileScheduledEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventsRuleSchedule2.Properties.State ).to.equal('ENABLED'); - expect(awsCompileScheduledEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventsRuleSchedule3 - .Properties.State + expect( + awsCompileScheduledEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventsRuleSchedule3.Properties.State ).to.equal('ENABLED'); - expect(awsCompileScheduledEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventsRuleSchedule4 - .Properties.State + expect( + awsCompileScheduledEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventsRuleSchedule4.Properties.State ).to.equal('ENABLED'); }); @@ -305,9 +308,9 @@ describe('AwsCompileScheduledEvents', () => { awsCompileScheduledEvents.compileScheduledEvents(); - expect(awsCompileScheduledEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventsRuleSchedule1 - .Properties.Name + expect( + awsCompileScheduledEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventsRuleSchedule1.Properties.Name ).to.equal('your-scheduled-event-name'); }); @@ -328,9 +331,9 @@ describe('AwsCompileScheduledEvents', () => { awsCompileScheduledEvents.compileScheduledEvents(); - expect(awsCompileScheduledEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventsRuleSchedule1 - .Properties.Description + expect( + awsCompileScheduledEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventsRuleSchedule1.Properties.Description ).to.equal('your scheduled event description'); }); @@ -351,9 +354,9 @@ describe('AwsCompileScheduledEvents', () => { awsCompileScheduledEvents.compileScheduledEvents(); - expect(awsCompileScheduledEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventsRuleSchedule1 - .Properties.Targets[0].InputPath + expect( + awsCompileScheduledEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventsRuleSchedule1.Properties.Targets[0].InputPath ).to.equal('$.stageVariables'); }); @@ -374,9 +377,9 @@ describe('AwsCompileScheduledEvents', () => { awsCompileScheduledEvents.compileScheduledEvents(); - expect(awsCompileScheduledEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventsRuleSchedule1 - .Properties.Targets[0].Input + expect( + awsCompileScheduledEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventsRuleSchedule1.Properties.Targets[0].Input ).to.equal('{"key":"value"}'); }); @@ -399,9 +402,9 @@ describe('AwsCompileScheduledEvents', () => { awsCompileScheduledEvents.compileScheduledEvents(); - expect(awsCompileScheduledEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventsRuleSchedule1 - .Properties.Targets[0].Input + expect( + awsCompileScheduledEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventsRuleSchedule1.Properties.Targets[0].Input ).to.equal('{"key":"value"}'); }); @@ -427,16 +430,15 @@ describe('AwsCompileScheduledEvents', () => { awsCompileScheduledEvents.compileScheduledEvents(); - expect(awsCompileScheduledEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventsRuleSchedule1 - .Properties.Targets[0].InputTransformer + expect( + awsCompileScheduledEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventsRuleSchedule1.Properties.Targets[0].InputTransformer ).to.eql({ InputTemplate: '{"time": , "key1": "value1"}', InputPathsMap: { eventTime: '$.time' }, }); }); - it('should throw an error when both Input and InputPath are set', () => { awsCompileScheduledEvents.serverless.service.functions = { first: { diff --git a/lib/plugins/aws/package/compile/events/sns/index.js b/lib/plugins/aws/package/compile/events/sns/index.js index 09cb21a6b..4d065c677 100644 --- a/lib/plugins/aws/package/compile/events/sns/index.js +++ b/lib/plugins/aws/package/compile/events/sns/index.js @@ -28,8 +28,10 @@ class AwsCompileSNSEvents { if (Object.keys(variable).length !== 1) { return false; } - if (_.has(variable, 'Fn::ImportValue') && - (_.has(variable, 'Fn::ImportValue.Fn::GetAtt') || _.has(variable, 'Fn::ImportValue.Ref'))) { + if ( + _.has(variable, 'Fn::ImportValue') && + (_.has(variable, 'Fn::ImportValue.Fn::GetAtt') || _.has(variable, 'Fn::ImportValue.Ref')) + ) { return false; } const intrinsicFunctions = ['Fn::ImportValue', 'Ref', 'Fn::GetAtt', 'Fn::Sub', 'Fn::Join']; @@ -39,7 +41,7 @@ class AwsCompileSNSEvents { compileSNSEvents() { const template = this.serverless.service.provider.compiledCloudFormationTemplate; - this.serverless.service.getAllFunctions().forEach((functionName) => { + this.serverless.service.getAllFunctions().forEach(functionName => { const functionObj = this.serverless.service.getFunction(functionName); if (functionObj.events) { @@ -54,33 +56,38 @@ class AwsCompileSNSEvents { topicArn = event.sns.arn; if (typeof topicArn === 'object') { if (!this.isValidStackImport(topicArn)) { - throw new this.serverless.classes - .Error(this.invalidPropertyErrorMessage(functionName, 'arn')); + throw new this.serverless.classes.Error( + this.invalidPropertyErrorMessage(functionName, 'arn') + ); } } else if (typeof topicArn === 'string') { if (topicArn.indexOf('arn:') === 0) { const splitArn = topicArn.split(':'); topicName = splitArn[splitArn.length - 1]; } else { - throw new this.serverless.classes - .Error(this.invalidPropertyErrorMessage(functionName, 'arn')); + throw new this.serverless.classes.Error( + this.invalidPropertyErrorMessage(functionName, 'arn') + ); } } else { - throw new this.serverless.classes - .Error(this.invalidPropertyErrorMessage(functionName, 'arn')); + throw new this.serverless.classes.Error( + this.invalidPropertyErrorMessage(functionName, 'arn') + ); } topicName = topicName || event.sns.topicName; if (!topicName || typeof topicName !== 'string') { - throw new this.serverless.classes - .Error(this.invalidPropertyErrorMessage(functionName, 'topicName')); + throw new this.serverless.classes.Error( + this.invalidPropertyErrorMessage(functionName, 'topicName') + ); } } else { - ['topicName', 'displayName'].forEach((property) => { + ['topicName', 'displayName'].forEach(property => { if (typeof event.sns[property] === 'string') { return; } - throw new this.serverless.classes - .Error(this.invalidPropertyErrorMessage(functionName, property)); + throw new this.serverless.classes.Error( + this.invalidPropertyErrorMessage(functionName, property) + ); }); displayName = event.sns.displayName; topicName = event.sns.topicName; @@ -102,19 +109,19 @@ class AwsCompileSNSEvents { ' topicName and displayName.', ' Please check the docs for more info.', ].join(''); - throw new this.serverless.classes - .Error(errorMessage); + throw new this.serverless.classes.Error(errorMessage); } - const lambdaLogicalId = this.provider.naming - .getLambdaLogicalId(functionName); + const lambdaLogicalId = this.provider.naming.getLambdaLogicalId(functionName); const endpoint = { 'Fn::GetAtt': [lambdaLogicalId, 'Arn'], }; - const subscriptionLogicalId = this.provider.naming - .getLambdaSnsSubscriptionLogicalId(functionName, topicName); + const subscriptionLogicalId = this.provider.naming.getLambdaSnsSubscriptionLogicalId( + functionName, + topicName + ); if (topicArn) { _.merge(template.Resources, { @@ -130,7 +137,8 @@ class AwsCompileSNSEvents { }); } else { topicArn = { - 'Fn::Join': ['', + 'Fn::Join': [ + '', [ 'arn:', { Ref: 'AWS::Partition' }, @@ -144,8 +152,7 @@ class AwsCompileSNSEvents { ], }; - const topicLogicalId = this.provider.naming - .getTopicLogicalId(topicName); + const topicLogicalId = this.provider.naming.getTopicLogicalId(topicName); const subscription = { Endpoint: endpoint, @@ -168,26 +175,26 @@ class AwsCompileSNSEvents { _.merge(template.Resources, { [subscriptionLogicalId]: { Type: 'AWS::SNS::Subscription', - Properties: - _.merge(subscription, { - TopicArn: { - Ref: topicLogicalId, - }, - FilterPolicy: event.sns.filterPolicy, - }), + Properties: _.merge(subscription, { + TopicArn: { + Ref: topicLogicalId, + }, + FilterPolicy: event.sns.filterPolicy, + }), }, }); } else { if (!template.Resources[topicLogicalId].Properties.Subscription) { template.Resources[topicLogicalId].Properties.Subscription = []; } - template.Resources[topicLogicalId] - .Properties.Subscription.push(subscription); + template.Resources[topicLogicalId].Properties.Subscription.push(subscription); } } - const lambdaPermissionLogicalId = this.provider.naming - .getLambdaSnsPermissionLogicalId(functionName, topicName); + const lambdaPermissionLogicalId = this.provider.naming.getLambdaSnsPermissionLogicalId( + functionName, + topicName + ); _.merge(template.Resources, { [lambdaPermissionLogicalId]: { @@ -195,7 +202,7 @@ class AwsCompileSNSEvents { Properties: { FunctionName: endpoint, Action: 'lambda:InvokeFunction', - Principal: { 'Fn::Join': ['', ['sns.', { Ref: 'AWS::URLSuffix' }]] }, + Principal: 'sns.amazonaws.com', SourceArn: topicArn, }, }, diff --git a/lib/plugins/aws/package/compile/events/sns/index.test.js b/lib/plugins/aws/package/compile/events/sns/index.test.js index 1603f0b2f..c312459c4 100644 --- a/lib/plugins/aws/package/compile/events/sns/index.test.js +++ b/lib/plugins/aws/package/compile/events/sns/index.test.js @@ -58,24 +58,29 @@ describe('AwsCompileSNSEvents', () => { awsCompileSNSEvents.compileSNSEvents(); - expect(awsCompileSNSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.SNSTopicTopic1.Type + expect( + awsCompileSNSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .SNSTopicTopic1.Type ).to.equal('AWS::SNS::Topic'); - expect(awsCompileSNSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.SNSTopicTopic2.Type + expect( + awsCompileSNSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .SNSTopicTopic2.Type ).to.equal('AWS::SNS::Topic'); - expect(awsCompileSNSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstLambdaPermissionTopic1SNS.Type + expect( + awsCompileSNSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstLambdaPermissionTopic1SNS.Type ).to.equal('AWS::Lambda::Permission'); - expect(awsCompileSNSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstLambdaPermissionTopic2SNS.Type + expect( + awsCompileSNSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstLambdaPermissionTopic2SNS.Type ).to.equal('AWS::Lambda::Permission'); - expect(awsCompileSNSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstSnsSubscriptionTopic1.Type + expect( + awsCompileSNSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstSnsSubscriptionTopic1.Type ).to.equal('AWS::SNS::Subscription'); - expect(awsCompileSNSEvents.serverless.service - .provider.compiledCloudFormationTemplate - .Resources.FirstSnsSubscriptionTopic1.Properties.FilterPolicy + expect( + awsCompileSNSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstSnsSubscriptionTopic1.Properties.FilterPolicy ).to.eql({ pet: ['dog', 'cat'] }); }); @@ -99,8 +104,9 @@ describe('AwsCompileSNSEvents', () => { }, }; - Object.assign(awsCompileSNSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources, { + Object.assign( + awsCompileSNSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources, + { SNSTopicTopic2: { Type: 'AWS::SNS::Topic', Properties: { @@ -108,28 +114,34 @@ describe('AwsCompileSNSEvents', () => { DisplayName: 'Display name for topic 2', }, }, - }); + } + ); awsCompileSNSEvents.compileSNSEvents(); - expect(awsCompileSNSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.SNSTopicTopic1.Type + expect( + awsCompileSNSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .SNSTopicTopic1.Type ).to.equal('AWS::SNS::Topic'); - expect(awsCompileSNSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.SNSTopicTopic2.Type + expect( + awsCompileSNSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .SNSTopicTopic2.Type ).to.equal('AWS::SNS::Topic'); - expect(awsCompileSNSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstLambdaPermissionTopic1SNS.Type + expect( + awsCompileSNSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstLambdaPermissionTopic1SNS.Type ).to.equal('AWS::Lambda::Permission'); - expect(awsCompileSNSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstLambdaPermissionTopic2SNS.Type + expect( + awsCompileSNSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstLambdaPermissionTopic2SNS.Type ).to.equal('AWS::Lambda::Permission'); - expect(awsCompileSNSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstSnsSubscriptionTopic1.Type + expect( + awsCompileSNSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstSnsSubscriptionTopic1.Type ).to.equal('AWS::SNS::Subscription'); - expect(awsCompileSNSEvents.serverless.service - .provider.compiledCloudFormationTemplate - .Resources.FirstSnsSubscriptionTopic1.Properties.FilterPolicy + expect( + awsCompileSNSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstSnsSubscriptionTopic1.Properties.FilterPolicy ).to.eql({ pet: ['dog', 'cat'] }); }); @@ -152,14 +164,18 @@ describe('AwsCompileSNSEvents', () => { awsCompileSNSEvents.compileSNSEvents(); - expect(Object.keys(awsCompileSNSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources) + expect( + Object.keys( + awsCompileSNSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + ) ).to.have.length(2); - expect(awsCompileSNSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.SNSTopicTopic1.Type + expect( + awsCompileSNSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .SNSTopicTopic1.Type ).to.equal('AWS::SNS::Topic'); - expect(awsCompileSNSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstLambdaPermissionTopic1SNS.Type + expect( + awsCompileSNSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstLambdaPermissionTopic1SNS.Type ).to.equal('AWS::Lambda::Permission'); }); @@ -176,7 +192,9 @@ describe('AwsCompileSNSEvents', () => { }, }; - expect(() => { awsCompileSNSEvents.compileSNSEvents(); }).to.throw(Error); + expect(() => { + awsCompileSNSEvents.compileSNSEvents(); + }).to.throw(Error); }); it('should not create corresponding resources when SNS events are not given', () => { @@ -189,8 +207,7 @@ describe('AwsCompileSNSEvents', () => { awsCompileSNSEvents.compileSNSEvents(); expect( - awsCompileSNSEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources + awsCompileSNSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources ).to.deep.equal({}); }); @@ -207,18 +224,22 @@ describe('AwsCompileSNSEvents', () => { awsCompileSNSEvents.compileSNSEvents(); - expect(Object.keys(awsCompileSNSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources) + expect( + Object.keys( + awsCompileSNSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + ) ).to.have.length(2); - expect(awsCompileSNSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstSnsSubscriptionFoo.Type + expect( + awsCompileSNSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstSnsSubscriptionFoo.Type ).to.equal('AWS::SNS::Subscription'); - expect(awsCompileSNSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstLambdaPermissionFooSNS.Type + expect( + awsCompileSNSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstLambdaPermissionFooSNS.Type ).to.equal('AWS::Lambda::Permission'); - expect(awsCompileSNSEvents.serverless.service - .provider.compiledCloudFormationTemplate - .Resources.FirstSnsSubscriptionFoo.Properties.FilterPolicy + expect( + awsCompileSNSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstSnsSubscriptionFoo.Properties.FilterPolicy ).to.equal(undefined); }); @@ -237,14 +258,18 @@ describe('AwsCompileSNSEvents', () => { awsCompileSNSEvents.compileSNSEvents(); - expect(Object.keys(awsCompileSNSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources) + expect( + Object.keys( + awsCompileSNSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + ) ).to.have.length(2); - expect(awsCompileSNSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstSnsSubscriptionFoo.Type + expect( + awsCompileSNSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstSnsSubscriptionFoo.Type ).to.equal('AWS::SNS::Subscription'); - expect(awsCompileSNSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstLambdaPermissionFooSNS.Type + expect( + awsCompileSNSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstLambdaPermissionFooSNS.Type ).to.equal('AWS::Lambda::Permission'); }); @@ -261,7 +286,9 @@ describe('AwsCompileSNSEvents', () => { }, }; - expect(() => { awsCompileSNSEvents.compileSNSEvents(); }).to.throw(Error); + expect(() => { + awsCompileSNSEvents.compileSNSEvents(); + }).to.throw(Error); }); it('should create SNS topic when arn and topicName are given as object properties', () => { @@ -280,14 +307,18 @@ describe('AwsCompileSNSEvents', () => { awsCompileSNSEvents.compileSNSEvents(); - expect(Object.keys(awsCompileSNSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources) + expect( + Object.keys( + awsCompileSNSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + ) ).to.have.length(2); - expect(awsCompileSNSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstSnsSubscriptionBar.Type + expect( + awsCompileSNSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstSnsSubscriptionBar.Type ).to.equal('AWS::SNS::Subscription'); - expect(awsCompileSNSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstLambdaPermissionBarSNS.Type + expect( + awsCompileSNSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstLambdaPermissionBarSNS.Type ).to.equal('AWS::Lambda::Permission'); }); @@ -300,15 +331,7 @@ describe('AwsCompileSNSEvents', () => { sns: { topicName: 'bar', arn: { - 'Fn::Join': [ - ':', - [ - 'arn:aws:sns', - '${AWS::Region}', - '${AWS::AccountId}', - 'bar', - ], - ], + 'Fn::Join': [':', ['arn:aws:sns', '${AWS::Region}', '${AWS::AccountId}', 'bar']], }, }, }, @@ -318,14 +341,18 @@ describe('AwsCompileSNSEvents', () => { awsCompileSNSEvents.compileSNSEvents(); - expect(Object.keys(awsCompileSNSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources) + expect( + Object.keys( + awsCompileSNSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + ) ).to.have.length(2); - expect(awsCompileSNSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstSnsSubscriptionBar.Type + expect( + awsCompileSNSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstSnsSubscriptionBar.Type ).to.equal('AWS::SNS::Subscription'); - expect(awsCompileSNSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstLambdaPermissionBarSNS.Type + expect( + awsCompileSNSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstLambdaPermissionBarSNS.Type ).to.equal('AWS::Lambda::Permission'); }); @@ -337,15 +364,7 @@ describe('AwsCompileSNSEvents', () => { { sns: { arn: { - 'Fn::Join': [ - ':', - [ - 'arn:aws:sns', - '${AWS::Region}', - '${AWS::AccountId}', - 'bar', - ], - ], + 'Fn::Join': [':', ['arn:aws:sns', '${AWS::Region}', '${AWS::AccountId}', 'bar']], }, }, }, @@ -353,7 +372,9 @@ describe('AwsCompileSNSEvents', () => { }, }; - expect(() => { awsCompileSNSEvents.compileSNSEvents(); }).to.throw(Error); + expect(() => { + awsCompileSNSEvents.compileSNSEvents(); + }).to.throw(Error); }); // eslint-disable-next-line max-len @@ -375,7 +396,9 @@ describe('AwsCompileSNSEvents', () => { }, }; - expect(() => { awsCompileSNSEvents.compileSNSEvents(); }).to.throw(Error); + expect(() => { + awsCompileSNSEvents.compileSNSEvents(); + }).to.throw(Error); awsCompileSNSEvents.serverless.service.functions = { first: { @@ -385,10 +408,7 @@ describe('AwsCompileSNSEvents', () => { topicName: 'bar', arn: { 'Fn::ImportValue': { - 'Fn::GetAtt': [ - 'BarTopic', - 'Arn', - ], + 'Fn::GetAtt': ['BarTopic', 'Arn'], }, }, }, @@ -397,7 +417,9 @@ describe('AwsCompileSNSEvents', () => { }, }; - expect(() => { awsCompileSNSEvents.compileSNSEvents(); }).to.throw(Error); + expect(() => { + awsCompileSNSEvents.compileSNSEvents(); + }).to.throw(Error); }); it('should create SNS topic when arn, topicName, and filterPolicy are given as object', () => { @@ -419,18 +441,22 @@ describe('AwsCompileSNSEvents', () => { awsCompileSNSEvents.compileSNSEvents(); - expect(Object.keys(awsCompileSNSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources) + expect( + Object.keys( + awsCompileSNSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + ) ).to.have.length(2); - expect(awsCompileSNSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstSnsSubscriptionBar.Type + expect( + awsCompileSNSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstSnsSubscriptionBar.Type ).to.equal('AWS::SNS::Subscription'); - expect(awsCompileSNSEvents.serverless.service - .provider.compiledCloudFormationTemplate - .Resources.FirstSnsSubscriptionBar.Properties.FilterPolicy + expect( + awsCompileSNSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstSnsSubscriptionBar.Properties.FilterPolicy ).to.eql({ pet: ['dog', 'cat'] }); - expect(awsCompileSNSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstLambdaPermissionBarSNS.Type + expect( + awsCompileSNSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstLambdaPermissionBarSNS.Type ).to.equal('AWS::Lambda::Permission'); }); }); diff --git a/lib/plugins/aws/package/compile/events/sqs/index.js b/lib/plugins/aws/package/compile/events/sqs/index.js index 3a4904e27..2b03923dd 100644 --- a/lib/plugins/aws/package/compile/events/sqs/index.js +++ b/lib/plugins/aws/package/compile/events/sqs/index.js @@ -13,17 +13,13 @@ class AwsCompileSQSEvents { } compileSQSEvents() { - this.serverless.service.getAllFunctions().forEach((functionName) => { + this.serverless.service.getAllFunctions().forEach(functionName => { const functionObj = this.serverless.service.getFunction(functionName); if (functionObj.events) { const sqsStatement = { Effect: 'Allow', - Action: [ - 'sqs:ReceiveMessage', - 'sqs:DeleteMessage', - 'sqs:GetQueueAttributes', - ], + Action: ['sqs:ReceiveMessage', 'sqs:DeleteMessage', 'sqs:GetQueueAttributes'], Resource: [], }; @@ -42,28 +38,29 @@ class AwsCompileSQSEvents { ' OR an object with an "arn" property.', ' Please check the docs for more info.', ].join(''); - throw new this.serverless.classes - .Error(errorMessage); + throw new this.serverless.classes.Error(errorMessage); } if (typeof event.sqs.arn !== 'string') { // for dynamic arns (GetAtt/ImportValue) - if (Object.keys(event.sqs.arn).length !== 1 - || !(_.has(event.sqs.arn, 'Fn::ImportValue') - || _.has(event.sqs.arn, 'Fn::GetAtt') - || _.has(event.sqs.arn, 'Fn::Join'))) { + if ( + Object.keys(event.sqs.arn).length !== 1 || + !( + _.has(event.sqs.arn, 'Fn::ImportValue') || + _.has(event.sqs.arn, 'Fn::GetAtt') || + _.has(event.sqs.arn, 'Fn::Join') + ) + ) { const errorMessage = [ `Bad dynamic ARN property on sqs event in function "${functionName}"`, ' If you use a dynamic "arn" (such as with Fn::GetAtt or Fn::ImportValue)', ' there must only be one key (either Fn::GetAtt or Fn::ImportValue) in the arn', ' object. Please check the docs for more info.', ].join(''); - throw new this.serverless.classes - .Error(errorMessage); + throw new this.serverless.classes.Error(errorMessage); } } EventSourceArn = event.sqs.arn; - BatchSize = event.sqs.batchSize - || BatchSize; + BatchSize = event.sqs.batchSize || BatchSize; if (typeof event.sqs.enabled !== 'undefined') { Enabled = event.sqs.enabled ? 'True' : 'False'; } @@ -76,11 +73,10 @@ class AwsCompileSQSEvents { ' OR an object with an "arn" property.', ' Please check the docs for more info.', ].join(''); - throw new this.serverless.classes - .Error(errorMessage); + throw new this.serverless.classes.Error(errorMessage); } - const queueName = (function () { + const queueName = (function() { if (EventSourceArn['Fn::GetAtt']) { return EventSourceArn['Fn::GetAtt'][0]; } else if (EventSourceArn['Fn::ImportValue']) { @@ -90,22 +86,22 @@ class AwsCompileSQSEvents { return EventSourceArn['Fn::Join'][1].slice(-1).pop(); } return EventSourceArn.split(':').pop(); - }()); + })(); - const lambdaLogicalId = this.provider.naming - .getLambdaLogicalId(functionName); - const queueLogicalId = this.provider.naming - .getQueueLogicalId(functionName, queueName); + const lambdaLogicalId = this.provider.naming.getLambdaLogicalId(functionName); + const queueLogicalId = this.provider.naming.getQueueLogicalId(functionName, queueName); const funcRole = functionObj.role || this.serverless.service.provider.role; let dependsOn = '"IamRoleLambdaExecution"'; if (funcRole) { - if ( // check whether the custom role is an ARN + if ( + // check whether the custom role is an ARN typeof funcRole === 'string' && funcRole.indexOf(':') !== -1 ) { dependsOn = '[]'; - } else if ( // otherwise, check if we have an in-service reference to a role ARN + } else if ( + // otherwise, check if we have an in-service reference to a role ARN typeof funcRole === 'object' && 'Fn::GetAtt' in funcRole && Array.isArray(funcRole['Fn::GetAtt']) && @@ -115,7 +111,8 @@ class AwsCompileSQSEvents { funcRole['Fn::GetAtt'][1] === 'Arn' ) { dependsOn = `"${funcRole['Fn::GetAtt'][0]}"`; - } else if ( // otherwise, check if we have an import + } else if ( + // otherwise, check if we have an import typeof funcRole === 'object' && 'Fn::ImportValue' in funcRole ) { @@ -149,21 +146,20 @@ class AwsCompileSQSEvents { [queueLogicalId]: JSON.parse(sqsTemplate), }; - _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, - newSQSObject); + _.merge( + this.serverless.service.provider.compiledCloudFormationTemplate.Resources, + newSQSObject + ); } }); // update the PolicyDocument statements (if default policy is used) - if (this.serverless.service.provider.compiledCloudFormationTemplate - .Resources.IamRoleLambdaExecution) { - const statement = this.serverless.service.provider.compiledCloudFormationTemplate - .Resources + if ( + this.serverless.service.provider.compiledCloudFormationTemplate.Resources .IamRoleLambdaExecution - .Properties - .Policies[0] - .PolicyDocument - .Statement; + ) { + const statement = this.serverless.service.provider.compiledCloudFormationTemplate + .Resources.IamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement; if (sqsStatement.Resource.length) { statement.push(sqsStatement); } diff --git a/lib/plugins/aws/package/compile/events/sqs/index.test.js b/lib/plugins/aws/package/compile/events/sqs/index.test.js index 2e21dfa15..b2cbe301f 100644 --- a/lib/plugins/aws/package/compile/events/sqs/index.test.js +++ b/lib/plugins/aws/package/compile/events/sqs/index.test.js @@ -79,14 +79,14 @@ describe('AwsCompileSQSEvents', () => { }; // pretend that the default IamRoleLambdaExecution is not in place - awsCompileSQSEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources - .IamRoleLambdaExecution = null; + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources.IamRoleLambdaExecution = null; - expect(() => { awsCompileSQSEvents.compileSQSEvents(); }).to.not.throw(Error); - expect(awsCompileSQSEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources - .IamRoleLambdaExecution + expect(() => { + awsCompileSQSEvents.compileSQSEvents(); + }).to.not.throw(Error); + expect( + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .IamRoleLambdaExecution ).to.equal(null); }); @@ -103,18 +103,22 @@ describe('AwsCompileSQSEvents', () => { }; // pretend that the default IamRoleLambdaExecution is not in place - awsCompileSQSEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources - .IamRoleLambdaExecution = null; + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources.IamRoleLambdaExecution = null; - expect(() => { awsCompileSQSEvents.compileSQSEvents(); }).to.not.throw(Error); - expect(awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FirstEventSourceMappingSQSMyQueue.DependsOn).to.be.instanceof(Array); - expect(awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FirstEventSourceMappingSQSMyQueue.DependsOn.length).to.equal(0); - expect(awsCompileSQSEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources - .IamRoleLambdaExecution + expect(() => { + awsCompileSQSEvents.compileSQSEvents(); + }).to.not.throw(Error); + expect( + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstEventSourceMappingSQSMyQueue.DependsOn + ).to.be.instanceof(Array); + expect( + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstEventSourceMappingSQSMyQueue.DependsOn.length + ).to.equal(0); + expect( + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .IamRoleLambdaExecution ).to.equal(null); }); @@ -132,14 +136,19 @@ describe('AwsCompileSQSEvents', () => { }; // pretend that the default IamRoleLambdaExecution is not in place - awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.IamRoleLambdaExecution = null; + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources.IamRoleLambdaExecution = null; - expect(() => { awsCompileSQSEvents.compileSQSEvents(); }).to.not.throw(Error); - expect(awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FirstEventSourceMappingSQSMyQueue.DependsOn).to.equal(roleLogicalId); - expect(awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.IamRoleLambdaExecution).to.equal(null); + expect(() => { + awsCompileSQSEvents.compileSQSEvents(); + }).to.not.throw(Error); + expect( + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstEventSourceMappingSQSMyQueue.DependsOn + ).to.equal(roleLogicalId); + expect( + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .IamRoleLambdaExecution + ).to.equal(null); }); it('should not throw error if custom IAM role reference is set in function', () => { @@ -156,14 +165,19 @@ describe('AwsCompileSQSEvents', () => { }; // pretend that the default IamRoleLambdaExecution is not in place - awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.IamRoleLambdaExecution = null; + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources.IamRoleLambdaExecution = null; - expect(() => { awsCompileSQSEvents.compileSQSEvents(); }).to.not.throw(Error); - expect(awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FirstEventSourceMappingSQSMyQueue.DependsOn).to.equal(roleLogicalId); - expect(awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.IamRoleLambdaExecution).to.equal(null); + expect(() => { + awsCompileSQSEvents.compileSQSEvents(); + }).to.not.throw(Error); + expect( + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstEventSourceMappingSQSMyQueue.DependsOn + ).to.equal(roleLogicalId); + expect( + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .IamRoleLambdaExecution + ).to.equal(null); }); it('should not throw error if custom IAM role is set in provider', () => { @@ -178,21 +192,24 @@ describe('AwsCompileSQSEvents', () => { }; // pretend that the default IamRoleLambdaExecution is not in place - awsCompileSQSEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources - .IamRoleLambdaExecution = null; + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources.IamRoleLambdaExecution = null; - awsCompileSQSEvents.serverless.service.provider - .role = 'arn:aws:iam::account:role/foo'; + awsCompileSQSEvents.serverless.service.provider.role = 'arn:aws:iam::account:role/foo'; - expect(() => { awsCompileSQSEvents.compileSQSEvents(); }).to.not.throw(Error); - expect(awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FirstEventSourceMappingSQSMyQueue.DependsOn).to.be.instanceof(Array); - expect(awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FirstEventSourceMappingSQSMyQueue.DependsOn.length).to.equal(0); - expect(awsCompileSQSEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources - .IamRoleLambdaExecution + expect(() => { + awsCompileSQSEvents.compileSQSEvents(); + }).to.not.throw(Error); + expect( + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstEventSourceMappingSQSMyQueue.DependsOn + ).to.be.instanceof(Array); + expect( + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstEventSourceMappingSQSMyQueue.DependsOn.length + ).to.equal(0); + expect( + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .IamRoleLambdaExecution ).to.equal(null); }); @@ -209,17 +226,21 @@ describe('AwsCompileSQSEvents', () => { }; // pretend that the default IamRoleLambdaExecution is not in place - awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.IamRoleLambdaExecution = null; + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources.IamRoleLambdaExecution = null; - expect(() => { awsCompileSQSEvents.compileSQSEvents(); }).to.not.throw(Error); - expect(awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FirstEventSourceMappingSQSMyQueue.DependsOn.length).to.equal(0); - expect(awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.IamRoleLambdaExecution).to.equal(null); + expect(() => { + awsCompileSQSEvents.compileSQSEvents(); + }).to.not.throw(Error); + expect( + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstEventSourceMappingSQSMyQueue.DependsOn.length + ).to.equal(0); + expect( + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .IamRoleLambdaExecution + ).to.equal(null); }); - it('should not throw error if custom IAM role reference is set in provider', () => { const roleLogicalId = 'RoleLogicalId'; awsCompileSQSEvents.serverless.service.functions = { @@ -233,17 +254,23 @@ describe('AwsCompileSQSEvents', () => { }; // pretend that the default IamRoleLambdaExecution is not in place - awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.IamRoleLambdaExecution = null; + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources.IamRoleLambdaExecution = null; - awsCompileSQSEvents.serverless.service.provider - .role = { 'Fn::GetAtt': [roleLogicalId, 'Arn'] }; + awsCompileSQSEvents.serverless.service.provider.role = { + 'Fn::GetAtt': [roleLogicalId, 'Arn'], + }; - expect(() => { awsCompileSQSEvents.compileSQSEvents(); }).to.not.throw(Error); - expect(awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FirstEventSourceMappingSQSMyQueue.DependsOn).to.equal(roleLogicalId); - expect(awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.IamRoleLambdaExecution).to.equal(null); + expect(() => { + awsCompileSQSEvents.compileSQSEvents(); + }).to.not.throw(Error); + expect( + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstEventSourceMappingSQSMyQueue.DependsOn + ).to.equal(roleLogicalId); + expect( + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .IamRoleLambdaExecution + ).to.equal(null); }); it('should not throw error if custom IAM role name reference is set in provider', () => { @@ -259,17 +286,21 @@ describe('AwsCompileSQSEvents', () => { }; // pretend that the default IamRoleLambdaExecution is not in place - awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.IamRoleLambdaExecution = null; + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources.IamRoleLambdaExecution = null; - awsCompileSQSEvents.serverless.service.provider - .role = roleLogicalId; + awsCompileSQSEvents.serverless.service.provider.role = roleLogicalId; - expect(() => { awsCompileSQSEvents.compileSQSEvents(); }).to.not.throw(Error); - expect(awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FirstEventSourceMappingSQSMyQueue.DependsOn).to.equal(roleLogicalId); - expect(awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.IamRoleLambdaExecution).to.equal(null); + expect(() => { + awsCompileSQSEvents.compileSQSEvents(); + }).to.not.throw(Error); + expect( + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstEventSourceMappingSQSMyQueue.DependsOn + ).to.equal(roleLogicalId); + expect( + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .IamRoleLambdaExecution + ).to.equal(null); }); describe('when a queue ARN is given', () => { @@ -299,81 +330,69 @@ describe('AwsCompileSQSEvents', () => { awsCompileSQSEvents.compileSQSEvents(); // event 1 - expect(awsCompileSQSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingSQSMyFirstQueue - .Type + expect( + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstEventSourceMappingSQSMyFirstQueue.Type ).to.equal('AWS::Lambda::EventSourceMapping'); - expect(awsCompileSQSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingSQSMyFirstQueue - .DependsOn + expect( + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstEventSourceMappingSQSMyFirstQueue.DependsOn ).to.equal('IamRoleLambdaExecution'); - expect(awsCompileSQSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingSQSMyFirstQueue - .Properties.EventSourceArn - ).to.equal( - awsCompileSQSEvents.serverless.service.functions.first.events[0] - .sqs.arn - ); - expect(awsCompileSQSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingSQSMyFirstQueue - .Properties.BatchSize - ).to.equal( - awsCompileSQSEvents.serverless.service.functions.first.events[0] - .sqs.batchSize - ); - expect(awsCompileSQSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingSQSMyFirstQueue - .Properties.Enabled + expect( + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstEventSourceMappingSQSMyFirstQueue.Properties.EventSourceArn + ).to.equal(awsCompileSQSEvents.serverless.service.functions.first.events[0].sqs.arn); + expect( + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstEventSourceMappingSQSMyFirstQueue.Properties.BatchSize + ).to.equal(awsCompileSQSEvents.serverless.service.functions.first.events[0].sqs.batchSize); + expect( + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstEventSourceMappingSQSMyFirstQueue.Properties.Enabled ).to.equal('False'); // event 2 - expect(awsCompileSQSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingSQSMySecondQueue - .Type + expect( + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstEventSourceMappingSQSMySecondQueue.Type ).to.equal('AWS::Lambda::EventSourceMapping'); - expect(awsCompileSQSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingSQSMySecondQueue - .DependsOn + expect( + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstEventSourceMappingSQSMySecondQueue.DependsOn ).to.equal('IamRoleLambdaExecution'); - expect(awsCompileSQSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingSQSMySecondQueue - .Properties.EventSourceArn - ).to.equal( - awsCompileSQSEvents.serverless.service.functions.first.events[1] - .sqs.arn - ); - expect(awsCompileSQSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingSQSMySecondQueue - .Properties.BatchSize + expect( + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstEventSourceMappingSQSMySecondQueue.Properties.EventSourceArn + ).to.equal(awsCompileSQSEvents.serverless.service.functions.first.events[1].sqs.arn); + expect( + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstEventSourceMappingSQSMySecondQueue.Properties.BatchSize ).to.equal(10); - expect(awsCompileSQSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingSQSMySecondQueue - .Properties.Enabled + expect( + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstEventSourceMappingSQSMySecondQueue.Properties.Enabled ).to.equal('True'); // event 3 - expect(awsCompileSQSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingSQSMyThirdQueue - .Type + expect( + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstEventSourceMappingSQSMyThirdQueue.Type ).to.equal('AWS::Lambda::EventSourceMapping'); - expect(awsCompileSQSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingSQSMyThirdQueue - .DependsOn + expect( + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstEventSourceMappingSQSMyThirdQueue.DependsOn ).to.equal('IamRoleLambdaExecution'); - expect(awsCompileSQSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingSQSMyThirdQueue - .Properties.EventSourceArn - ).to.equal( - awsCompileSQSEvents.serverless.service.functions.first.events[2] - .sqs - ); - expect(awsCompileSQSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingSQSMyThirdQueue - .Properties.BatchSize + expect( + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstEventSourceMappingSQSMyThirdQueue.Properties.EventSourceArn + ).to.equal(awsCompileSQSEvents.serverless.service.functions.first.events[2].sqs); + expect( + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstEventSourceMappingSQSMyThirdQueue.Properties.BatchSize ).to.equal(10); - expect(awsCompileSQSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingSQSMyThirdQueue - .Properties.Enabled + expect( + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstEventSourceMappingSQSMyThirdQueue.Properties.Enabled ).to.equal('True'); }); @@ -395,8 +414,12 @@ describe('AwsCompileSQSEvents', () => { sqs: { arn: { 'Fn::Join': [ - ':', [ - 'arn', 'aws', 'sqs', { + ':', + [ + 'arn', + 'aws', + 'sqs', + { Ref: 'AWS::Region', }, { @@ -414,80 +437,66 @@ describe('AwsCompileSQSEvents', () => { awsCompileSQSEvents.compileSQSEvents(); - expect(awsCompileSQSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.IamRoleLambdaExecution - .Properties.Policies[0].PolicyDocument.Statement[0] - ).to.deep.equal( - { - Action: [ - 'sqs:ReceiveMessage', - 'sqs:DeleteMessage', - 'sqs:GetQueueAttributes', - ], - Effect: 'Allow', - Resource: [ - { - 'Fn::GetAtt': [ - 'SomeQueue', - 'Arn', + expect( + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .IamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement[0] + ).to.deep.equal({ + Action: ['sqs:ReceiveMessage', 'sqs:DeleteMessage', 'sqs:GetQueueAttributes'], + Effect: 'Allow', + Resource: [ + { + 'Fn::GetAtt': ['SomeQueue', 'Arn'], + }, + { + 'Fn::ImportValue': 'ForeignQueue', + }, + { + 'Fn::Join': [ + ':', + [ + 'arn', + 'aws', + 'sqs', + { + Ref: 'AWS::Region', + }, + { + Ref: 'AWS::AccountId', + }, + 'MyQueue', ], - }, - { - 'Fn::ImportValue': 'ForeignQueue', - }, - { - 'Fn::Join': [ - ':', - [ - 'arn', - 'aws', - 'sqs', - { - Ref: 'AWS::Region', - }, - { - Ref: 'AWS::AccountId', - }, - 'MyQueue', - ], - ], - }, - ], - } - ); - expect(awsCompileSQSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstEventSourceMappingSQSSomeQueue.Properties.EventSourceArn - ).to.deep.equal( - { 'Fn::GetAtt': ['SomeQueue', 'Arn'] } - ); - expect(awsCompileSQSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstEventSourceMappingSQSForeignQueue.Properties.EventSourceArn - ).to.deep.equal( - { 'Fn::ImportValue': 'ForeignQueue' } - ); - expect(awsCompileSQSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstEventSourceMappingSQSMyQueue.Properties.EventSourceArn - ).to.deep.equal( - { - 'Fn::Join': [ - ':', - [ - 'arn', - 'aws', - 'sqs', - { - Ref: 'AWS::Region', - }, - { - Ref: 'AWS::AccountId', - }, - 'MyQueue', ], + }, + ], + }); + expect( + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstEventSourceMappingSQSSomeQueue.Properties.EventSourceArn + ).to.deep.equal({ 'Fn::GetAtt': ['SomeQueue', 'Arn'] }); + expect( + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstEventSourceMappingSQSForeignQueue.Properties.EventSourceArn + ).to.deep.equal({ 'Fn::ImportValue': 'ForeignQueue' }); + expect( + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstEventSourceMappingSQSMyQueue.Properties.EventSourceArn + ).to.deep.equal({ + 'Fn::Join': [ + ':', + [ + 'arn', + 'aws', + 'sqs', + { + Ref: 'AWS::Region', + }, + { + Ref: 'AWS::AccountId', + }, + 'MyQueue', ], - }); + ], + }); }); it('fails if keys other than Fn::GetAtt/ImportValue/Join are used for dynamic ARNs', () => { @@ -498,7 +507,7 @@ describe('AwsCompileSQSEvents', () => { sqs: { arn: { 'Fn::GetAtt': ['SomeQueue', 'Arn'], - batchSize: 1, + 'batchSize': 1, }, }, }, @@ -526,11 +535,7 @@ describe('AwsCompileSQSEvents', () => { const iamRoleStatements = [ { Effect: 'Allow', - Action: [ - 'sqs:ReceiveMessage', - 'sqs:DeleteMessage', - 'sqs:GetQueueAttributes', - ], + Action: ['sqs:ReceiveMessage', 'sqs:DeleteMessage', 'sqs:GetQueueAttributes'], Resource: [ 'arn:aws:sqs:region:account:MyFirstQueue', 'arn:aws:sqs:region:account:MySecondQueue', @@ -540,10 +545,9 @@ describe('AwsCompileSQSEvents', () => { awsCompileSQSEvents.compileSQSEvents(); - expect(awsCompileSQSEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources - .IamRoleLambdaExecution.Properties.Policies[0] - .PolicyDocument.Statement + expect( + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .IamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement ).to.deep.equal(iamRoleStatements); }); }); @@ -559,8 +563,9 @@ describe('AwsCompileSQSEvents', () => { // should be 1 because we've mocked the IamRoleLambdaExecution above expect( - Object.keys(awsCompileSQSEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources).length + Object.keys( + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + ).length ).to.equal(1); }); @@ -574,10 +579,8 @@ describe('AwsCompileSQSEvents', () => { awsCompileSQSEvents.compileSQSEvents(); expect( - awsCompileSQSEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources - .IamRoleLambdaExecution.Properties.Policies[0] - .PolicyDocument.Statement.length + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .IamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement.length ).to.equal(0); }); @@ -594,8 +597,8 @@ describe('AwsCompileSQSEvents', () => { awsCompileSQSEvents.compileSQSEvents(); - expect(awsCompileSQSEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources + expect( + awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources ).to.have.any.keys('FirstEventSourceMappingSQSSomequeuename'); }); }); diff --git a/lib/plugins/aws/package/compile/events/stream/index.js b/lib/plugins/aws/package/compile/events/stream/index.js index bf550a7ed..bf65ed3ad 100644 --- a/lib/plugins/aws/package/compile/events/stream/index.js +++ b/lib/plugins/aws/package/compile/events/stream/index.js @@ -13,7 +13,7 @@ class AwsCompileStreamEvents { } compileStreamEvents() { - this.serverless.service.getAllFunctions().forEach((functionName) => { + this.serverless.service.getAllFunctions().forEach(functionName => { const functionObj = this.serverless.service.getFunction(functionName); if (functionObj.events) { @@ -54,8 +54,7 @@ class AwsCompileStreamEvents { ' OR an object with an "arn" property.', ' Please check the docs for more info.', ].join(''); - throw new this.serverless.classes - .Error(errorMessage); + throw new this.serverless.classes.Error(errorMessage); } if (typeof event.stream.arn !== 'string') { // for dynamic arns (GetAtt/ImportValue) @@ -66,28 +65,28 @@ class AwsCompileStreamEvents { ' then a "type" must be provided for the stream, either "kinesis" or,', ' "dynamodb". Please check the docs for more info.', ].join(''); - throw new this.serverless.classes - .Error(errorMessage); + throw new this.serverless.classes.Error(errorMessage); } - if (Object.keys(event.stream.arn).length !== 1 - || !(_.has(event.stream.arn, 'Fn::ImportValue') - || _.has(event.stream.arn, 'Fn::GetAtt') - || _.has(event.stream.arn, 'Fn::Join'))) { + if ( + Object.keys(event.stream.arn).length !== 1 || + !( + _.has(event.stream.arn, 'Fn::ImportValue') || + _.has(event.stream.arn, 'Fn::GetAtt') || + _.has(event.stream.arn, 'Fn::Join') + ) + ) { const errorMessage = [ `Bad dynamic ARN property on stream event in function "${functionName}"`, ' If you use a dynamic "arn" (such as with Fn::GetAtt, Fn::Join', ' or Fn::ImportValue) there must only be one key (either Fn::GetAtt, Fn::Join', ' or Fn::ImportValue) in the arn object. Please check the docs for more info.', ].join(''); - throw new this.serverless.classes - .Error(errorMessage); + throw new this.serverless.classes.Error(errorMessage); } } EventSourceArn = event.stream.arn; - BatchSize = event.stream.batchSize - || BatchSize; - StartingPosition = event.stream.startingPosition - || StartingPosition; + BatchSize = event.stream.batchSize || BatchSize; + StartingPosition = event.stream.startingPosition || StartingPosition; if (typeof event.stream.enabled !== 'undefined') { Enabled = event.stream.enabled ? 'True' : 'False'; } @@ -100,12 +99,11 @@ class AwsCompileStreamEvents { ' OR an object with an "arn" property.', ' Please check the docs for more info.', ].join(''); - throw new this.serverless.classes - .Error(errorMessage); + throw new this.serverless.classes.Error(errorMessage); } const streamType = event.stream.type || EventSourceArn.split(':')[2]; - const streamName = (function () { + const streamName = (function() { if (EventSourceArn['Fn::GetAtt']) { return EventSourceArn['Fn::GetAtt'][0]; } else if (EventSourceArn['Fn::ImportValue']) { @@ -119,22 +117,26 @@ class AwsCompileStreamEvents { return name; } return EventSourceArn.split('/')[1]; - }()); + })(); - const lambdaLogicalId = this.provider.naming - .getLambdaLogicalId(functionName); - const streamLogicalId = this.provider.naming - .getStreamLogicalId(functionName, streamType, streamName); + const lambdaLogicalId = this.provider.naming.getLambdaLogicalId(functionName); + const streamLogicalId = this.provider.naming.getStreamLogicalId( + functionName, + streamType, + streamName + ); const funcRole = functionObj.role || this.serverless.service.provider.role; let dependsOn = '"IamRoleLambdaExecution"'; if (funcRole) { - if ( // check whether the custom role is an ARN + if ( + // check whether the custom role is an ARN typeof funcRole === 'string' && funcRole.indexOf(':') !== -1 ) { dependsOn = '[]'; - } else if ( // otherwise, check if we have an in-service reference to a role ARN + } else if ( + // otherwise, check if we have an in-service reference to a role ARN typeof funcRole === 'object' && 'Fn::GetAtt' in funcRole && Array.isArray(funcRole['Fn::GetAtt']) && @@ -144,7 +146,8 @@ class AwsCompileStreamEvents { funcRole['Fn::GetAtt'][1] === 'Arn' ) { dependsOn = `"${funcRole['Fn::GetAtt'][0]}"`; - } else if ( // otherwise, check if we have an import + } else if ( + // otherwise, check if we have an import typeof funcRole === 'object' && 'Fn::ImportValue' in funcRole ) { @@ -181,34 +184,31 @@ class AwsCompileStreamEvents { const errorMessage = [ `Stream event of function '${functionName}' had unsupported stream type of`, ` '${streamType}'. Valid stream event source types include 'dynamodb' and`, - ' \'kinesis\'. Please check the docs for more info.', + " 'kinesis'. Please check the docs for more info.", ].join(''); - throw new this.serverless.classes - .Properties - .Policies[0] - .PolicyDocument - .Error(errorMessage); + throw new this.serverless.classes.Properties.Policies[0].PolicyDocument.Error( + errorMessage + ); } const newStreamObject = { [streamLogicalId]: JSON.parse(streamTemplate), }; - _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, - newStreamObject); + _.merge( + this.serverless.service.provider.compiledCloudFormationTemplate.Resources, + newStreamObject + ); } }); // update the PolicyDocument statements (if default policy is used) - if (this.serverless.service.provider.compiledCloudFormationTemplate - .Resources.IamRoleLambdaExecution) { - const statement = this.serverless.service.provider.compiledCloudFormationTemplate - .Resources + if ( + this.serverless.service.provider.compiledCloudFormationTemplate.Resources .IamRoleLambdaExecution - .Properties - .Policies[0] - .PolicyDocument - .Statement; + ) { + const statement = this.serverless.service.provider.compiledCloudFormationTemplate + .Resources.IamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement; if (dynamodbStreamStatement.Resource.length) { statement.push(dynamodbStreamStatement); } diff --git a/lib/plugins/aws/package/compile/events/stream/index.test.js b/lib/plugins/aws/package/compile/events/stream/index.test.js index cd15293da..b5d1b6666 100644 --- a/lib/plugins/aws/package/compile/events/stream/index.test.js +++ b/lib/plugins/aws/package/compile/events/stream/index.test.js @@ -96,14 +96,14 @@ describe('AwsCompileStreamEvents', () => { }; // pretend that the default IamRoleLambdaExecution is not in place - awsCompileStreamEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources - .IamRoleLambdaExecution = null; + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources.IamRoleLambdaExecution = null; - expect(() => { awsCompileStreamEvents.compileStreamEvents(); }).to.not.throw(Error); - expect(awsCompileStreamEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources - .IamRoleLambdaExecution + expect(() => { + awsCompileStreamEvents.compileStreamEvents(); + }).to.not.throw(Error); + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .IamRoleLambdaExecution ).to.equal(null); }); @@ -121,18 +121,22 @@ describe('AwsCompileStreamEvents', () => { }; // pretend that the default IamRoleLambdaExecution is not in place - awsCompileStreamEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources - .IamRoleLambdaExecution = null; + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources.IamRoleLambdaExecution = null; - expect(() => { awsCompileStreamEvents.compileStreamEvents(); }).to.not.throw(Error); - expect(awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FirstEventSourceMappingDynamodbFoo.DependsOn).to.be.instanceof(Array); - expect(awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FirstEventSourceMappingDynamodbFoo.DependsOn.length).to.equal(0); - expect(awsCompileStreamEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources - .IamRoleLambdaExecution + expect(() => { + awsCompileStreamEvents.compileStreamEvents(); + }).to.not.throw(Error); + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstEventSourceMappingDynamodbFoo.DependsOn + ).to.be.instanceof(Array); + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstEventSourceMappingDynamodbFoo.DependsOn.length + ).to.equal(0); + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .IamRoleLambdaExecution ).to.equal(null); }); @@ -151,14 +155,19 @@ describe('AwsCompileStreamEvents', () => { }; // pretend that the default IamRoleLambdaExecution is not in place - awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.IamRoleLambdaExecution = null; + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources.IamRoleLambdaExecution = null; - expect(() => { awsCompileStreamEvents.compileStreamEvents(); }).to.not.throw(Error); - expect(awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FirstEventSourceMappingDynamodbFoo.DependsOn).to.equal(roleLogicalId); - expect(awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.IamRoleLambdaExecution).to.equal(null); + expect(() => { + awsCompileStreamEvents.compileStreamEvents(); + }).to.not.throw(Error); + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstEventSourceMappingDynamodbFoo.DependsOn + ).to.equal(roleLogicalId); + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .IamRoleLambdaExecution + ).to.equal(null); }); it('should not throw error if custom IAM role reference is set in function', () => { @@ -176,14 +185,19 @@ describe('AwsCompileStreamEvents', () => { }; // pretend that the default IamRoleLambdaExecution is not in place - awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.IamRoleLambdaExecution = null; + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources.IamRoleLambdaExecution = null; - expect(() => { awsCompileStreamEvents.compileStreamEvents(); }).to.not.throw(Error); - expect(awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FirstEventSourceMappingDynamodbFoo.DependsOn).to.equal(roleLogicalId); - expect(awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.IamRoleLambdaExecution).to.equal(null); + expect(() => { + awsCompileStreamEvents.compileStreamEvents(); + }).to.not.throw(Error); + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstEventSourceMappingDynamodbFoo.DependsOn + ).to.equal(roleLogicalId); + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .IamRoleLambdaExecution + ).to.equal(null); }); it('should not throw error if custom IAM role is set in provider', () => { @@ -199,21 +213,24 @@ describe('AwsCompileStreamEvents', () => { }; // pretend that the default IamRoleLambdaExecution is not in place - awsCompileStreamEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources - .IamRoleLambdaExecution = null; + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources.IamRoleLambdaExecution = null; - awsCompileStreamEvents.serverless.service.provider - .role = 'arn:aws:iam::account:role/foo'; + awsCompileStreamEvents.serverless.service.provider.role = 'arn:aws:iam::account:role/foo'; - expect(() => { awsCompileStreamEvents.compileStreamEvents(); }).to.not.throw(Error); - expect(awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FirstEventSourceMappingDynamodbFoo.DependsOn).to.be.instanceof(Array); - expect(awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FirstEventSourceMappingDynamodbFoo.DependsOn.length).to.equal(0); - expect(awsCompileStreamEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources - .IamRoleLambdaExecution + expect(() => { + awsCompileStreamEvents.compileStreamEvents(); + }).to.not.throw(Error); + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstEventSourceMappingDynamodbFoo.DependsOn + ).to.be.instanceof(Array); + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstEventSourceMappingDynamodbFoo.DependsOn.length + ).to.equal(0); + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .IamRoleLambdaExecution ).to.equal(null); }); @@ -231,17 +248,21 @@ describe('AwsCompileStreamEvents', () => { }; // pretend that the default IamRoleLambdaExecution is not in place - awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.IamRoleLambdaExecution = null; + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources.IamRoleLambdaExecution = null; - expect(() => { awsCompileStreamEvents.compileStreamEvents(); }).to.not.throw(Error); - expect(awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FirstEventSourceMappingDynamodbFoo.DependsOn.length).to.equal(0); - expect(awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.IamRoleLambdaExecution).to.equal(null); + expect(() => { + awsCompileStreamEvents.compileStreamEvents(); + }).to.not.throw(Error); + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstEventSourceMappingDynamodbFoo.DependsOn.length + ).to.equal(0); + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .IamRoleLambdaExecution + ).to.equal(null); }); - it('should not throw error if custom IAM role reference is set in provider', () => { const roleLogicalId = 'RoleLogicalId'; awsCompileStreamEvents.serverless.service.functions = { @@ -256,17 +277,23 @@ describe('AwsCompileStreamEvents', () => { }; // pretend that the default IamRoleLambdaExecution is not in place - awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.IamRoleLambdaExecution = null; + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources.IamRoleLambdaExecution = null; - awsCompileStreamEvents.serverless.service.provider - .role = { 'Fn::GetAtt': [roleLogicalId, 'Arn'] }; + awsCompileStreamEvents.serverless.service.provider.role = { + 'Fn::GetAtt': [roleLogicalId, 'Arn'], + }; - expect(() => { awsCompileStreamEvents.compileStreamEvents(); }).to.not.throw(Error); - expect(awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FirstEventSourceMappingDynamodbFoo.DependsOn).to.equal(roleLogicalId); - expect(awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.IamRoleLambdaExecution).to.equal(null); + expect(() => { + awsCompileStreamEvents.compileStreamEvents(); + }).to.not.throw(Error); + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstEventSourceMappingDynamodbFoo.DependsOn + ).to.equal(roleLogicalId); + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .IamRoleLambdaExecution + ).to.equal(null); }); it('should not throw error if custom IAM role name reference is set in provider', () => { @@ -283,17 +310,21 @@ describe('AwsCompileStreamEvents', () => { }; // pretend that the default IamRoleLambdaExecution is not in place - awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.IamRoleLambdaExecution = null; + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources.IamRoleLambdaExecution = null; - awsCompileStreamEvents.serverless.service.provider - .role = roleLogicalId; + awsCompileStreamEvents.serverless.service.provider.role = roleLogicalId; - expect(() => { awsCompileStreamEvents.compileStreamEvents(); }).to.not.throw(Error); - expect(awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FirstEventSourceMappingDynamodbFoo.DependsOn).to.equal(roleLogicalId); - expect(awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.IamRoleLambdaExecution).to.equal(null); + expect(() => { + awsCompileStreamEvents.compileStreamEvents(); + }).to.not.throw(Error); + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FirstEventSourceMappingDynamodbFoo.DependsOn + ).to.equal(roleLogicalId); + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .IamRoleLambdaExecution + ).to.equal(null); }); describe('when a DynamoDB stream ARN is given', () => { @@ -324,96 +355,86 @@ describe('AwsCompileStreamEvents', () => { awsCompileStreamEvents.compileStreamEvents(); // event 1 - expect(awsCompileStreamEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbFoo - .Type + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventSourceMappingDynamodbFoo.Type ).to.equal('AWS::Lambda::EventSourceMapping'); - expect(awsCompileStreamEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbFoo - .DependsOn + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventSourceMappingDynamodbFoo.DependsOn ).to.equal('IamRoleLambdaExecution'); - expect(awsCompileStreamEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbFoo - .Properties.EventSourceArn + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventSourceMappingDynamodbFoo.Properties.EventSourceArn + ).to.equal(awsCompileStreamEvents.serverless.service.functions.first.events[0].stream.arn); + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventSourceMappingDynamodbFoo.Properties.BatchSize ).to.equal( - awsCompileStreamEvents.serverless.service.functions.first.events[0] - .stream.arn + awsCompileStreamEvents.serverless.service.functions.first.events[0].stream.batchSize ); - expect(awsCompileStreamEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbFoo - .Properties.BatchSize + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventSourceMappingDynamodbFoo.Properties.StartingPosition ).to.equal( - awsCompileStreamEvents.serverless.service.functions.first.events[0] - .stream.batchSize + awsCompileStreamEvents.serverless.service.functions.first.events[0].stream + .startingPosition ); - expect(awsCompileStreamEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbFoo - .Properties.StartingPosition - ).to.equal( - awsCompileStreamEvents.serverless.service.functions.first.events[0] - .stream.startingPosition - ); - expect(awsCompileStreamEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbFoo - .Properties.Enabled + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventSourceMappingDynamodbFoo.Properties.Enabled ).to.equal('False'); // event 2 - expect(awsCompileStreamEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbBar - .Type + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventSourceMappingDynamodbBar.Type ).to.equal('AWS::Lambda::EventSourceMapping'); - expect(awsCompileStreamEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbBar - .DependsOn + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventSourceMappingDynamodbBar.DependsOn ).to.equal('IamRoleLambdaExecution'); - expect(awsCompileStreamEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbBar - .Properties.EventSourceArn - ).to.equal( - awsCompileStreamEvents.serverless.service.functions.first.events[1] - .stream.arn - ); - expect(awsCompileStreamEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbBar - .Properties.BatchSize + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventSourceMappingDynamodbBar.Properties.EventSourceArn + ).to.equal(awsCompileStreamEvents.serverless.service.functions.first.events[1].stream.arn); + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventSourceMappingDynamodbBar.Properties.BatchSize ).to.equal(10); - expect(awsCompileStreamEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbBar - .Properties.StartingPosition + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventSourceMappingDynamodbBar.Properties.StartingPosition ).to.equal('TRIM_HORIZON'); - expect(awsCompileStreamEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbBar - .Properties.Enabled + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventSourceMappingDynamodbBar.Properties.Enabled ).to.equal('True'); // event 3 - expect(awsCompileStreamEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbBaz - .Type + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventSourceMappingDynamodbBaz.Type ).to.equal('AWS::Lambda::EventSourceMapping'); - expect(awsCompileStreamEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbBaz - .DependsOn + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventSourceMappingDynamodbBaz.DependsOn ).to.equal('IamRoleLambdaExecution'); - expect(awsCompileStreamEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbBaz - .Properties.EventSourceArn - ).to.equal( - awsCompileStreamEvents.serverless.service.functions.first.events[2] - .stream - ); - expect(awsCompileStreamEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbBaz - .Properties.BatchSize + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventSourceMappingDynamodbBaz.Properties.EventSourceArn + ).to.equal(awsCompileStreamEvents.serverless.service.functions.first.events[2].stream); + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventSourceMappingDynamodbBaz.Properties.BatchSize ).to.equal(10); - expect(awsCompileStreamEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbBaz - .Properties.StartingPosition + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventSourceMappingDynamodbBaz.Properties.StartingPosition ).to.equal('TRIM_HORIZON'); - expect(awsCompileStreamEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbBaz - .Properties.Enabled + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventSourceMappingDynamodbBaz.Properties.Enabled ).to.equal('True'); }); @@ -437,10 +458,15 @@ describe('AwsCompileStreamEvents', () => { stream: { arn: { 'Fn::Join': [ - ':', [ - 'arn', 'aws', 'kinesis', { + ':', + [ + 'arn', + 'aws', + 'kinesis', + { Ref: 'AWS::Region', - }, { + }, + { Ref: 'AWS::AccountId', }, 'stream/MyStream', @@ -457,64 +483,55 @@ describe('AwsCompileStreamEvents', () => { awsCompileStreamEvents.compileStreamEvents(); // dynamodb with Fn::GetAtt - expect(awsCompileStreamEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstEventSourceMappingDynamodbSomeDdbTable.Properties.EventSourceArn - ).to.deep.equal( - { 'Fn::GetAtt': ['SomeDdbTable', 'StreamArn'] } - ); - expect(awsCompileStreamEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.IamRoleLambdaExecution - .Properties.Policies[0].PolicyDocument.Statement[0] - ).to.deep.equal( - { - Action: [ - 'dynamodb:GetRecords', - 'dynamodb:GetShardIterator', - 'dynamodb:DescribeStream', - 'dynamodb:ListStreams', - ], - Effect: 'Allow', - Resource: [ - { - 'Fn::GetAtt': [ - 'SomeDdbTable', - 'StreamArn', - ], - }, - ], - } - ); + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventSourceMappingDynamodbSomeDdbTable.Properties.EventSourceArn + ).to.deep.equal({ 'Fn::GetAtt': ['SomeDdbTable', 'StreamArn'] }); + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.IamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement[0] + ).to.deep.equal({ + Action: [ + 'dynamodb:GetRecords', + 'dynamodb:GetShardIterator', + 'dynamodb:DescribeStream', + 'dynamodb:ListStreams', + ], + Effect: 'Allow', + Resource: [ + { + 'Fn::GetAtt': ['SomeDdbTable', 'StreamArn'], + }, + ], + }); // kinesis with Fn::ImportValue - expect(awsCompileStreamEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstEventSourceMappingKinesisForeignKinesis.Properties.EventSourceArn - ).to.deep.equal( - { 'Fn::ImportValue': 'ForeignKinesis' } - ); + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventSourceMappingKinesisForeignKinesis.Properties.EventSourceArn + ).to.deep.equal({ 'Fn::ImportValue': 'ForeignKinesis' }); // kinesis with Fn::Join - expect(awsCompileStreamEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .FirstEventSourceMappingKinesisMyStream.Properties.EventSourceArn - ).to.deep.equal( - { - 'Fn::Join': [ - ':', [ - 'arn', - 'aws', - 'kinesis', - { - Ref: 'AWS::Region', - }, { - Ref: 'AWS::AccountId', - }, - 'stream/MyStream', - ], + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventSourceMappingKinesisMyStream.Properties.EventSourceArn + ).to.deep.equal({ + 'Fn::Join': [ + ':', + [ + 'arn', + 'aws', + 'kinesis', + { + Ref: 'AWS::Region', + }, + { + Ref: 'AWS::AccountId', + }, + 'stream/MyStream', ], - } - ); + ], + }); }); it('fails if Fn::GetAtt/dynamic stream ARN is used without a type', () => { @@ -533,26 +550,25 @@ describe('AwsCompileStreamEvents', () => { expect(() => awsCompileStreamEvents.compileStreamEvents()).to.throw(Error); }); - it('fails if keys other than Fn::GetAtt/ImportValue/Join are used for dynamic stream ARN', - () => { - awsCompileStreamEvents.serverless.service.functions = { - first: { - events: [ - { - stream: { - type: 'dynamodb', - arn: { - 'Fn::GetAtt': ['SomeDdbTable', 'StreamArn'], - batchSize: 1, - }, + it('fails if keys other than Fn::GetAtt/ImportValue/Join are used for dynamic stream ARN', () => { + awsCompileStreamEvents.serverless.service.functions = { + first: { + events: [ + { + stream: { + type: 'dynamodb', + arn: { + 'Fn::GetAtt': ['SomeDdbTable', 'StreamArn'], + 'batchSize': 1, }, }, - ], - }, - }; + }, + ], + }, + }; - expect(() => awsCompileStreamEvents.compileStreamEvents()).to.throw(Error); - }); + expect(() => awsCompileStreamEvents.compileStreamEvents()).to.throw(Error); + }); it('should add the necessary IAM role statements', () => { awsCompileStreamEvents.serverless.service.functions = { @@ -586,10 +602,9 @@ describe('AwsCompileStreamEvents', () => { awsCompileStreamEvents.compileStreamEvents(); - expect(awsCompileStreamEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources - .IamRoleLambdaExecution.Properties.Policies[0] - .PolicyDocument.Statement + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.IamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement ).to.deep.equal(iamRoleStatements); }); }); @@ -622,96 +637,86 @@ describe('AwsCompileStreamEvents', () => { awsCompileStreamEvents.compileStreamEvents(); // event 1 - expect(awsCompileStreamEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisFoo - .Type + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventSourceMappingKinesisFoo.Type ).to.equal('AWS::Lambda::EventSourceMapping'); - expect(awsCompileStreamEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisFoo - .DependsOn + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventSourceMappingKinesisFoo.DependsOn ).to.equal('IamRoleLambdaExecution'); - expect(awsCompileStreamEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisFoo - .Properties.EventSourceArn + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventSourceMappingKinesisFoo.Properties.EventSourceArn + ).to.equal(awsCompileStreamEvents.serverless.service.functions.first.events[0].stream.arn); + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventSourceMappingKinesisFoo.Properties.BatchSize ).to.equal( - awsCompileStreamEvents.serverless.service.functions.first.events[0] - .stream.arn + awsCompileStreamEvents.serverless.service.functions.first.events[0].stream.batchSize ); - expect(awsCompileStreamEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisFoo - .Properties.BatchSize + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventSourceMappingKinesisFoo.Properties.StartingPosition ).to.equal( - awsCompileStreamEvents.serverless.service.functions.first.events[0] - .stream.batchSize + awsCompileStreamEvents.serverless.service.functions.first.events[0].stream + .startingPosition ); - expect(awsCompileStreamEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisFoo - .Properties.StartingPosition - ).to.equal( - awsCompileStreamEvents.serverless.service.functions.first.events[0] - .stream.startingPosition - ); - expect(awsCompileStreamEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisFoo - .Properties.Enabled + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventSourceMappingKinesisFoo.Properties.Enabled ).to.equal('False'); // event 2 - expect(awsCompileStreamEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisBar - .Type + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventSourceMappingKinesisBar.Type ).to.equal('AWS::Lambda::EventSourceMapping'); - expect(awsCompileStreamEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisBar - .DependsOn + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventSourceMappingKinesisBar.DependsOn ).to.equal('IamRoleLambdaExecution'); - expect(awsCompileStreamEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisBar - .Properties.EventSourceArn - ).to.equal( - awsCompileStreamEvents.serverless.service.functions.first.events[1] - .stream.arn - ); - expect(awsCompileStreamEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisBar - .Properties.BatchSize + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventSourceMappingKinesisBar.Properties.EventSourceArn + ).to.equal(awsCompileStreamEvents.serverless.service.functions.first.events[1].stream.arn); + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventSourceMappingKinesisBar.Properties.BatchSize ).to.equal(10); - expect(awsCompileStreamEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisBar - .Properties.StartingPosition + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventSourceMappingKinesisBar.Properties.StartingPosition ).to.equal('TRIM_HORIZON'); - expect(awsCompileStreamEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisBar - .Properties.Enabled + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventSourceMappingKinesisBar.Properties.Enabled ).to.equal('True'); // event 3 - expect(awsCompileStreamEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisBaz - .Type + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventSourceMappingKinesisBaz.Type ).to.equal('AWS::Lambda::EventSourceMapping'); - expect(awsCompileStreamEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisBaz - .DependsOn + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventSourceMappingKinesisBaz.DependsOn ).to.equal('IamRoleLambdaExecution'); - expect(awsCompileStreamEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisBaz - .Properties.EventSourceArn - ).to.equal( - awsCompileStreamEvents.serverless.service.functions.first.events[2] - .stream - ); - expect(awsCompileStreamEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisBaz - .Properties.BatchSize + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventSourceMappingKinesisBaz.Properties.EventSourceArn + ).to.equal(awsCompileStreamEvents.serverless.service.functions.first.events[2].stream); + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventSourceMappingKinesisBaz.Properties.BatchSize ).to.equal(10); - expect(awsCompileStreamEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisBaz - .Properties.StartingPosition + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventSourceMappingKinesisBaz.Properties.StartingPosition ).to.equal('TRIM_HORIZON'); - expect(awsCompileStreamEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisBaz - .Properties.Enabled + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FirstEventSourceMappingKinesisBaz.Properties.Enabled ).to.equal('True'); }); @@ -747,10 +752,9 @@ describe('AwsCompileStreamEvents', () => { awsCompileStreamEvents.compileStreamEvents(); - expect(awsCompileStreamEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources - .IamRoleLambdaExecution.Properties.Policies[0] - .PolicyDocument.Statement + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.IamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement ).to.deep.equal(iamRoleStatements); }); }); @@ -766,8 +770,10 @@ describe('AwsCompileStreamEvents', () => { // should be 1 because we've mocked the IamRoleLambdaExecution above expect( - Object.keys(awsCompileStreamEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources).length + Object.keys( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources + ).length ).to.equal(1); }); @@ -781,10 +787,8 @@ describe('AwsCompileStreamEvents', () => { awsCompileStreamEvents.compileStreamEvents(); expect( - awsCompileStreamEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources - .IamRoleLambdaExecution.Properties.Policies[0] - .PolicyDocument.Statement.length + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .IamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement.length ).to.equal(0); }); @@ -801,8 +805,8 @@ describe('AwsCompileStreamEvents', () => { awsCompileStreamEvents.compileStreamEvents(); - expect(awsCompileStreamEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources + expect( + awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources ).to.have.any.keys('FirstEventSourceMappingKinesisSomelongname'); }); }); diff --git a/lib/plugins/aws/package/compile/events/websockets/index.test.js b/lib/plugins/aws/package/compile/events/websockets/index.test.js index ad3826e25..ee1978186 100644 --- a/lib/plugins/aws/package/compile/events/websockets/index.test.js +++ b/lib/plugins/aws/package/compile/events/websockets/index.test.js @@ -42,20 +42,21 @@ describe('AwsCompileWebsocketsEvents', () => { let compileStageStub; beforeEach(() => { - compileApiStub = sinon - .stub(awsCompileWebsocketsEvents, 'compileApi').resolves(); + compileApiStub = sinon.stub(awsCompileWebsocketsEvents, 'compileApi').resolves(); compileIntegrationsStub = sinon - .stub(awsCompileWebsocketsEvents, 'compileIntegrations').resolves(); + .stub(awsCompileWebsocketsEvents, 'compileIntegrations') + .resolves(); compileAuthorizersStub = sinon - .stub(awsCompileWebsocketsEvents, 'compileAuthorizers').resolves(); + .stub(awsCompileWebsocketsEvents, 'compileAuthorizers') + .resolves(); compilePermissionsStub = sinon - .stub(awsCompileWebsocketsEvents, 'compilePermissions').resolves(); - compileRoutesStub = sinon - .stub(awsCompileWebsocketsEvents, 'compileRoutes').resolves(); + .stub(awsCompileWebsocketsEvents, 'compilePermissions') + .resolves(); + compileRoutesStub = sinon.stub(awsCompileWebsocketsEvents, 'compileRoutes').resolves(); compileDeploymentStub = sinon - .stub(awsCompileWebsocketsEvents, 'compileDeployment').resolves(); - compileStageStub = sinon - .stub(awsCompileWebsocketsEvents, 'compileStage').resolves(); + .stub(awsCompileWebsocketsEvents, 'compileDeployment') + .resolves(); + compileStageStub = sinon.stub(awsCompileWebsocketsEvents, 'compileStage').resolves(); }); afterEach(() => { @@ -79,17 +80,16 @@ describe('AwsCompileWebsocketsEvents', () => { }); it('should run the promise chain in order', () => { - const validateStub = sinon - .stub(awsCompileWebsocketsEvents, 'validate').returns({ - events: [ - { - functionName: 'first', - websocket: { - route: 'echo', - }, + const validateStub = sinon.stub(awsCompileWebsocketsEvents, 'validate').returns({ + events: [ + { + functionName: 'first', + websocket: { + route: 'echo', }, - ], - }); + }, + ], + }); return awsCompileWebsocketsEvents.hooks['package:compileEvents']().then(() => { expect(validateStub.calledOnce).to.be.equal(true); diff --git a/lib/plugins/aws/package/compile/events/websockets/lib/api.js b/lib/plugins/aws/package/compile/events/websockets/lib/api.js index f52067a6a..5ec6bb1e1 100644 --- a/lib/plugins/aws/package/compile/events/websockets/lib/api.js +++ b/lib/plugins/aws/package/compile/events/websockets/lib/api.js @@ -5,10 +5,18 @@ const BbPromise = require('bluebird'); module.exports = { compileApi() { + const apiGateway = this.serverless.service.provider.apiGateway || {}; + + // immediately return if we're using an external websocket API id + if (apiGateway.websocketApiId) { + return BbPromise.resolve(); + } + this.websocketsApiLogicalId = this.provider.naming.getWebsocketsApiLogicalId(); - const RouteSelectionExpression = this.serverless.service.provider - .websocketsApiRouteSelectionExpression || '$request.body.action'; + const RouteSelectionExpression = + this.serverless.service.provider.websocketsApiRouteSelectionExpression || + '$request.body.action'; _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, { [this.websocketsApiLogicalId]: { @@ -16,8 +24,8 @@ module.exports = { Properties: { Name: this.provider.naming.getWebsocketsApiName(), RouteSelectionExpression, - Description: this.serverless.service.provider - .websocketsDescription || 'Serverless Websockets', + Description: + this.serverless.service.provider.websocketsDescription || 'Serverless Websockets', ProtocolType: 'WEBSOCKET', }, }, @@ -34,13 +42,9 @@ module.exports = { Resource: ['arn:aws:execute-api:*:*:*/@connections/*'], }; - this.serverless.service.provider.compiledCloudFormationTemplate - .Resources[this.provider.naming.getRoleLogicalId()] - .Properties - .Policies[0] - .PolicyDocument - .Statement - .push(websocketsPolicy); + this.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + this.provider.naming.getRoleLogicalId() + ].Properties.Policies[0].PolicyDocument.Statement.push(websocketsPolicy); } return BbPromise.resolve(); diff --git a/lib/plugins/aws/package/compile/events/websockets/lib/api.test.js b/lib/plugins/aws/package/compile/events/websockets/lib/api.test.js index a3ed46155..86328168a 100644 --- a/lib/plugins/aws/package/compile/events/websockets/lib/api.test.js +++ b/lib/plugins/aws/package/compile/events/websockets/lib/api.test.js @@ -18,26 +18,26 @@ describe('#compileApi()', () => { awsCompileWebsocketsEvents = new AwsCompileWebsocketsEvents(serverless); roleLogicalId = awsCompileWebsocketsEvents.provider.naming.getRoleLogicalId(); - awsCompileWebsocketsEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources = { - [roleLogicalId]: { - Properties: { - Policies: [ - { - PolicyDocument: { - Statement: [], - }, + awsCompileWebsocketsEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources = { + [roleLogicalId]: { + Properties: { + Policies: [ + { + PolicyDocument: { + Statement: [], }, - ], - }, + }, + ], }, - }; + }, + }; }); - it('should create a websocket api resource', () => awsCompileWebsocketsEvents - .compileApi().then(() => { - const resources = awsCompileWebsocketsEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources; + it('should create a websocket api resource', () => + awsCompileWebsocketsEvents.compileApi().then(() => { + const resources = + awsCompileWebsocketsEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources; expect(resources.WebsocketsApi).to.deep.equal({ Type: 'AWS::ApiGatewayV2::Api', @@ -50,10 +50,24 @@ describe('#compileApi()', () => { }); })); - it('should add the websockets policy', () => awsCompileWebsocketsEvents - .compileApi().then(() => { - const resources = awsCompileWebsocketsEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources; + it('should ignore API resource creation if there is predefined websocketApi config', () => { + awsCompileWebsocketsEvents.serverless.service.provider.apiGateway = { + websocketApiId: '5ezys3sght', + }; + return awsCompileWebsocketsEvents.compileApi().then(() => { + const resources = + awsCompileWebsocketsEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources; + + expect(resources).to.not.have.property('WebsocketsApi'); + }); + }); + + it('should add the websockets policy', () => + awsCompileWebsocketsEvents.compileApi().then(() => { + const resources = + awsCompileWebsocketsEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources; expect(resources[roleLogicalId]).to.deep.equal({ Properties: { @@ -62,13 +76,9 @@ describe('#compileApi()', () => { PolicyDocument: { Statement: [ { - Action: [ - 'execute-api:ManageConnections', - ], + Action: ['execute-api:ManageConnections'], Effect: 'Allow', - Resource: [ - 'arn:aws:execute-api:*:*:*/@connections/*', - ], + Resource: ['arn:aws:execute-api:*:*:*/@connections/*'], }, ], }, @@ -79,15 +89,14 @@ describe('#compileApi()', () => { })); it('should NOT add the websockets policy if role resource does not exist', () => { - awsCompileWebsocketsEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources = {}; + awsCompileWebsocketsEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources = {}; - return awsCompileWebsocketsEvents - .compileApi().then(() => { - const resources = awsCompileWebsocketsEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources; + return awsCompileWebsocketsEvents.compileApi().then(() => { + const resources = + awsCompileWebsocketsEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources; - expect(resources[roleLogicalId]).to.deep.equal(undefined); - }); + expect(resources[roleLogicalId]).to.deep.equal(undefined); + }); }); }); diff --git a/lib/plugins/aws/package/compile/events/websockets/lib/authorizers.js b/lib/plugins/aws/package/compile/events/websockets/lib/authorizers.js index 31062df82..ba277f7a7 100644 --- a/lib/plugins/aws/package/compile/events/websockets/lib/authorizers.js +++ b/lib/plugins/aws/package/compile/events/websockets/lib/authorizers.js @@ -9,16 +9,15 @@ module.exports = { if (!event.authorizer) { return; } - const websocketsAuthorizerLogicalId = this.provider.naming - .getWebsocketsAuthorizerLogicalId(event.authorizer.name); + const websocketsAuthorizerLogicalId = this.provider.naming.getWebsocketsAuthorizerLogicalId( + event.authorizer.name + ); _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, { [websocketsAuthorizerLogicalId]: { Type: 'AWS::ApiGatewayV2::Authorizer', Properties: { - ApiId: { - Ref: this.websocketsApiLogicalId, - }, + ApiId: this.provider.getApiGatewayWebsocketApiId(), Name: event.authorizer.name, AuthorizerType: 'REQUEST', AuthorizerUri: event.authorizer.uri, diff --git a/lib/plugins/aws/package/compile/events/websockets/lib/authorizers.test.js b/lib/plugins/aws/package/compile/events/websockets/lib/authorizers.test.js index 1925ae6ce..697dd283f 100644 --- a/lib/plugins/aws/package/compile/events/websockets/lib/authorizers.test.js +++ b/lib/plugins/aws/package/compile/events/websockets/lib/authorizers.test.js @@ -8,102 +8,140 @@ const AwsProvider = require('../../../../../provider/awsProvider'); describe('#compileAuthorizers()', () => { let awsCompileWebsocketsEvents; - beforeEach(() => { - const serverless = new Serverless(); - serverless.setProvider('aws', new AwsProvider(serverless)); - serverless.service.provider.compiledCloudFormationTemplate = { Resources: {} }; + describe('for routes with authorizer definition', () => { + beforeEach(() => { + const serverless = new Serverless(); + serverless.setProvider('aws', new AwsProvider(serverless)); + serverless.service.provider.compiledCloudFormationTemplate = { Resources: {} }; - awsCompileWebsocketsEvents = new AwsCompileWebsocketsEvents(serverless); + awsCompileWebsocketsEvents = new AwsCompileWebsocketsEvents(serverless); - awsCompileWebsocketsEvents.websocketsApiLogicalId - = awsCompileWebsocketsEvents.provider.naming.getWebsocketsApiLogicalId(); - }); + awsCompileWebsocketsEvents.websocketsApiLogicalId = awsCompileWebsocketsEvents.provider.naming.getWebsocketsApiLogicalId(); - it('should create an authorizer resource for routes with authorizer definition', () => { - awsCompileWebsocketsEvents.validated = { - events: [ - { - functionName: 'First', - route: '$connect', - authorizer: { - name: 'auth', - uri: { - 'Fn::Join': ['', - [ - 'arn:', - { Ref: 'AWS::Partition' }, - ':apigateway:', - { Ref: 'AWS::Region' }, - ':lambda:path/2015-03-31/functions/', - { 'Fn::GetAtt': ['AuthLambdaFunction', 'Arn'] }, - '/invocations', + awsCompileWebsocketsEvents.validated = { + events: [ + { + functionName: 'First', + route: '$connect', + authorizer: { + name: 'auth', + uri: { + 'Fn::Join': [ + '', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':apigateway:', + { Ref: 'AWS::Region' }, + ':lambda:path/2015-03-31/functions/', + { 'Fn::GetAtt': ['AuthLambdaFunction', 'Arn'] }, + '/invocations', + ], ], - ], + }, + identitySource: ['route.request.header.Auth'], }, - identitySource: ['route.request.header.Auth'], }, - }, - ], - }; + ], + }; + }); - return awsCompileWebsocketsEvents.compileAuthorizers().then(() => { - const resources = awsCompileWebsocketsEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources; + it('should create an authorizer resource', () => { + return awsCompileWebsocketsEvents.compileAuthorizers().then(() => { + const resources = + awsCompileWebsocketsEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources; - expect(resources).to.deep.equal({ - AuthWebsocketsAuthorizer: { - Type: 'AWS::ApiGatewayV2::Authorizer', - Properties: { - ApiId: { - Ref: 'WebsocketsApi', - }, - Name: 'auth', - AuthorizerType: 'REQUEST', - AuthorizerUri: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':apigateway:', - { - Ref: 'AWS::Region', - }, - ':lambda:path/2015-03-31/functions/', - { - 'Fn::GetAtt': [ - 'AuthLambdaFunction', - 'Arn', - ], - }, - '/invocations', + expect(resources).to.deep.equal({ + AuthWebsocketsAuthorizer: { + Type: 'AWS::ApiGatewayV2::Authorizer', + Properties: { + ApiId: { + Ref: 'WebsocketsApi', + }, + Name: 'auth', + AuthorizerType: 'REQUEST', + AuthorizerUri: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':apigateway:', + { + Ref: 'AWS::Region', + }, + ':lambda:path/2015-03-31/functions/', + { + 'Fn::GetAtt': ['AuthLambdaFunction', 'Arn'], + }, + '/invocations', + ], ], - ], + }, + IdentitySource: ['route.request.header.Auth'], }, - IdentitySource: ['route.request.header.Auth'], }, - }, + }); + }); + }); + + it('should use existing Api if there is predefined websocketApi config', () => { + awsCompileWebsocketsEvents.serverless.service.provider.apiGateway = { + websocketApiId: '5ezys3sght', + }; + + return awsCompileWebsocketsEvents.compileAuthorizers().then(() => { + const resources = + awsCompileWebsocketsEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources; + + expect(resources.AuthWebsocketsAuthorizer.Properties).to.contain({ + ApiId: '5ezys3sght', + }); }); }); }); - it('should NOT create an authorizer resource for routes with not authorizer definition', () => { - awsCompileWebsocketsEvents.validated = { - events: [ - { - functionName: 'First', - route: '$connect', - }, - ], - }; + describe('for routes without authorizer definition', () => { + beforeEach(() => { + const serverless = new Serverless(); + serverless.setProvider('aws', new AwsProvider(serverless)); + serverless.service.provider.compiledCloudFormationTemplate = { Resources: {} }; - return awsCompileWebsocketsEvents.compileAuthorizers().then(() => { - const resources = awsCompileWebsocketsEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources; + awsCompileWebsocketsEvents = new AwsCompileWebsocketsEvents(serverless); - expect(resources).to.deep.equal({}); + awsCompileWebsocketsEvents.websocketsApiLogicalId = awsCompileWebsocketsEvents.provider.naming.getWebsocketsApiLogicalId(); + + awsCompileWebsocketsEvents.validated = { + events: [ + { + functionName: 'First', + route: '$connect', + }, + ], + }; + }); + + it('should NOT create an authorizer resource for routes with not authorizer definition', () => { + awsCompileWebsocketsEvents.validated = { + events: [ + { + functionName: 'First', + route: '$connect', + }, + ], + }; + + return awsCompileWebsocketsEvents.compileAuthorizers().then(() => { + const resources = + awsCompileWebsocketsEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources; + + expect(resources).to.deep.equal({}); + }); }); }); }); diff --git a/lib/plugins/aws/package/compile/events/websockets/lib/deployment.js b/lib/plugins/aws/package/compile/events/websockets/lib/deployment.js index 341d42870..f7ced266e 100644 --- a/lib/plugins/aws/package/compile/events/websockets/lib/deployment.js +++ b/lib/plugins/aws/package/compile/events/websockets/lib/deployment.js @@ -6,23 +6,21 @@ const BbPromise = require('bluebird'); module.exports = { compileDeployment() { const routeLogicalIds = this.validated.events.map(event => { - const routeLogicalId = this.provider.naming - .getWebsocketsRouteLogicalId(event.route); + const routeLogicalId = this.provider.naming.getWebsocketsRouteLogicalId(event.route); return routeLogicalId; }); - this.websocketsDeploymentLogicalId = this.provider.naming - .getWebsocketsDeploymentLogicalId(this.serverless.instanceId); + this.websocketsDeploymentLogicalId = this.provider.naming.getWebsocketsDeploymentLogicalId( + this.serverless.instanceId + ); _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, { [this.websocketsDeploymentLogicalId]: { Type: 'AWS::ApiGatewayV2::Deployment', DependsOn: routeLogicalIds, Properties: { - ApiId: { - Ref: this.websocketsApiLogicalId, - }, - Description: this.serverless.service.provider - .websocketsDescription || 'Serverless Websockets', + ApiId: this.provider.getApiGatewayWebsocketApiId(), + Description: + this.serverless.service.provider.websocketsDescription || 'Serverless Websockets', }, }, }); @@ -31,10 +29,11 @@ module.exports = { ServiceEndpointWebsocket: { Description: 'URL of the service endpoint', Value: { - 'Fn::Join': ['', + 'Fn::Join': [ + '', [ 'wss://', - { Ref: this.provider.naming.getWebsocketsApiLogicalId() }, + this.provider.getApiGatewayWebsocketApiId(), '.execute-api.', { Ref: 'AWS::Region' }, '.', diff --git a/lib/plugins/aws/package/compile/events/websockets/lib/deployment.test.js b/lib/plugins/aws/package/compile/events/websockets/lib/deployment.test.js index 5137cafdb..1e5876eb9 100644 --- a/lib/plugins/aws/package/compile/events/websockets/lib/deployment.test.js +++ b/lib/plugins/aws/package/compile/events/websockets/lib/deployment.test.js @@ -18,8 +18,7 @@ describe('#compileDeployment()', () => { awsCompileWebsocketsEvents = new AwsCompileWebsocketsEvents(serverless); - awsCompileWebsocketsEvents.websocketsApiLogicalId - = awsCompileWebsocketsEvents.provider.naming.getWebsocketsApiLogicalId(); + awsCompileWebsocketsEvents.websocketsApiLogicalId = awsCompileWebsocketsEvents.provider.naming.getWebsocketsApiLogicalId(); }); it('should create a deployment resource and output', () => { @@ -37,20 +36,19 @@ describe('#compileDeployment()', () => { }; return awsCompileWebsocketsEvents.compileDeployment().then(() => { - const resources = awsCompileWebsocketsEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources; - const outputs = awsCompileWebsocketsEvents.serverless.service.provider - .compiledCloudFormationTemplate.Outputs; + const resources = + awsCompileWebsocketsEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources; + const outputs = + awsCompileWebsocketsEvents.serverless.service.provider.compiledCloudFormationTemplate + .Outputs; const deploymentLogicalId = Object.keys(resources)[0]; expect(deploymentLogicalId).to.match(/WebsocketsDeployment/); expect(resources[deploymentLogicalId]).to.deep.equal({ Type: 'AWS::ApiGatewayV2::Deployment', - DependsOn: [ - 'SconnectWebsocketsRoute', - 'SdisconnectWebsocketsRoute', - ], + DependsOn: ['SconnectWebsocketsRoute', 'SdisconnectWebsocketsRoute'], Properties: { ApiId: { Ref: 'WebsocketsApi', diff --git a/lib/plugins/aws/package/compile/events/websockets/lib/integrations.js b/lib/plugins/aws/package/compile/events/websockets/lib/integrations.js index c892ee302..5eaf0d25b 100644 --- a/lib/plugins/aws/package/compile/events/websockets/lib/integrations.js +++ b/lib/plugins/aws/package/compile/events/websockets/lib/integrations.js @@ -6,20 +6,20 @@ const BbPromise = require('bluebird'); module.exports = { compileIntegrations() { this.validated.events.forEach(event => { - const websocketsIntegrationLogicalId = this.provider.naming - .getWebsocketsIntegrationLogicalId(event.functionName); + const websocketsIntegrationLogicalId = this.provider.naming.getWebsocketsIntegrationLogicalId( + event.functionName + ); const lambdaLogicalId = this.provider.naming.getLambdaLogicalId(event.functionName); _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, { [websocketsIntegrationLogicalId]: { Type: 'AWS::ApiGatewayV2::Integration', Properties: { - ApiId: { - Ref: this.websocketsApiLogicalId, - }, + ApiId: this.provider.getApiGatewayWebsocketApiId(), IntegrationType: 'AWS_PROXY', IntegrationUri: { - 'Fn::Join': ['', + 'Fn::Join': [ + '', [ 'arn:', { Ref: 'AWS::Partition' }, diff --git a/lib/plugins/aws/package/compile/events/websockets/lib/integrations.test.js b/lib/plugins/aws/package/compile/events/websockets/lib/integrations.test.js index 021d07342..50613492a 100644 --- a/lib/plugins/aws/package/compile/events/websockets/lib/integrations.test.js +++ b/lib/plugins/aws/package/compile/events/websockets/lib/integrations.test.js @@ -15,8 +15,7 @@ describe('#compileIntegrations()', () => { awsCompileWebsocketsEvents = new AwsCompileWebsocketsEvents(serverless); - awsCompileWebsocketsEvents.websocketsApiLogicalId - = awsCompileWebsocketsEvents.provider.naming.getWebsocketsApiLogicalId(); + awsCompileWebsocketsEvents.websocketsApiLogicalId = awsCompileWebsocketsEvents.provider.naming.getWebsocketsApiLogicalId(); }); it('should create an integration resource for every event', () => { @@ -34,8 +33,9 @@ describe('#compileIntegrations()', () => { }; return awsCompileWebsocketsEvents.compileIntegrations().then(() => { - const resources = awsCompileWebsocketsEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources; + const resources = + awsCompileWebsocketsEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources; expect(resources).to.deep.equal({ FirstWebsocketsIntegration: { @@ -59,11 +59,7 @@ describe('#compileIntegrations()', () => { }, ':lambda:path/2015-03-31/functions/', { - 'Fn::GetAtt': - [ - 'FirstLambdaFunction', - 'Arn', - ], + 'Fn::GetAtt': ['FirstLambdaFunction', 'Arn'], }, '/invocations', ], @@ -92,10 +88,7 @@ describe('#compileIntegrations()', () => { }, ':lambda:path/2015-03-31/functions/', { - 'Fn::GetAtt': [ - 'SecondLambdaFunction', - 'Arn', - ], + 'Fn::GetAtt': ['SecondLambdaFunction', 'Arn'], }, '/invocations', ], diff --git a/lib/plugins/aws/package/compile/events/websockets/lib/permissions.js b/lib/plugins/aws/package/compile/events/websockets/lib/permissions.js index fe28e2368..6789194d4 100644 --- a/lib/plugins/aws/package/compile/events/websockets/lib/permissions.js +++ b/lib/plugins/aws/package/compile/events/websockets/lib/permissions.js @@ -6,55 +6,66 @@ const BbPromise = require('bluebird'); module.exports = { compilePermissions() { this.validated.events.forEach(event => { + const websocketApiId = this.provider.getApiGatewayWebsocketApiId(); const lambdaLogicalId = this.provider.naming.getLambdaLogicalId(event.functionName); - const websocketsPermissionLogicalId = this.provider.naming - .getLambdaWebsocketsPermissionLogicalId(event.functionName); + const websocketsPermissionLogicalId = this.provider.naming.getLambdaWebsocketsPermissionLogicalId( + event.functionName + ); _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, { [websocketsPermissionLogicalId]: { Type: 'AWS::Lambda::Permission', - DependsOn: [this.websocketsApiLogicalId, lambdaLogicalId], + DependsOn: + websocketApiId.Ref !== undefined + ? [websocketApiId.Ref, lambdaLogicalId] + : [lambdaLogicalId], Properties: { FunctionName: { 'Fn::GetAtt': [lambdaLogicalId, 'Arn'], }, Action: 'lambda:InvokeFunction', - Principal: { 'Fn::Join': ['', ['apigateway.', { Ref: 'AWS::URLSuffix' }]] }, + Principal: 'apigateway.amazonaws.com', }, }, }); if (event.authorizer) { - const websocketsAuthorizerPermissionLogicalId = this.provider.naming - .getLambdaWebsocketsPermissionLogicalId(event.authorizer.name); + const websocketsAuthorizerPermissionLogicalId = this.provider.naming.getLambdaWebsocketsPermissionLogicalId( + event.authorizer.name + ); const authorizerPermissionTemplate = { [websocketsAuthorizerPermissionLogicalId]: { Type: 'AWS::Lambda::Permission', - DependsOn: [this.websocketsApiLogicalId], + DependsOn: websocketApiId.Ref !== undefined ? [websocketApiId.Ref] : [], Properties: { Action: 'lambda:InvokeFunction', - Principal: { 'Fn::Join': ['', ['apigateway.', { Ref: 'AWS::URLSuffix' }]] }, + Principal: 'apigateway.amazonaws.com', }, }, }; if (event.authorizer.permission.includes(':')) { - authorizerPermissionTemplate[websocketsAuthorizerPermissionLogicalId] - .Properties.FunctionName = event.authorizer.permission; + authorizerPermissionTemplate[ + websocketsAuthorizerPermissionLogicalId + ].Properties.FunctionName = event.authorizer.permission; } else { - authorizerPermissionTemplate[websocketsAuthorizerPermissionLogicalId] - .Properties.FunctionName = { - 'Fn::GetAtt': [event.authorizer.permission, 'Arn'], - }; + authorizerPermissionTemplate[ + websocketsAuthorizerPermissionLogicalId + ].Properties.FunctionName = { + 'Fn::GetAtt': [event.authorizer.permission, 'Arn'], + }; - authorizerPermissionTemplate[websocketsAuthorizerPermissionLogicalId] - .DependsOn.push(event.authorizer.permission); + authorizerPermissionTemplate[websocketsAuthorizerPermissionLogicalId].DependsOn.push( + event.authorizer.permission + ); } - _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, - authorizerPermissionTemplate); + _.merge( + this.serverless.service.provider.compiledCloudFormationTemplate.Resources, + authorizerPermissionTemplate + ); } }); diff --git a/lib/plugins/aws/package/compile/events/websockets/lib/permissions.test.js b/lib/plugins/aws/package/compile/events/websockets/lib/permissions.test.js index 25e6a8663..4d9605d0f 100644 --- a/lib/plugins/aws/package/compile/events/websockets/lib/permissions.test.js +++ b/lib/plugins/aws/package/compile/events/websockets/lib/permissions.test.js @@ -15,8 +15,7 @@ describe('#compilePermissions()', () => { awsCompileWebsocketsEvents = new AwsCompileWebsocketsEvents(serverless); - awsCompileWebsocketsEvents.websocketsApiLogicalId - = awsCompileWebsocketsEvents.provider.naming.getWebsocketsApiLogicalId(); + awsCompileWebsocketsEvents.websocketsApiLogicalId = awsCompileWebsocketsEvents.provider.naming.getWebsocketsApiLogicalId(); }); it('should create a permission resource for every event', () => { @@ -34,61 +33,31 @@ describe('#compilePermissions()', () => { }; return awsCompileWebsocketsEvents.compilePermissions().then(() => { - const resources = awsCompileWebsocketsEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources; + const resources = + awsCompileWebsocketsEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources; expect(resources).to.deep.equal({ FirstLambdaPermissionWebsockets: { Type: 'AWS::Lambda::Permission', - DependsOn: [ - 'WebsocketsApi', - 'FirstLambdaFunction', - ], + DependsOn: ['WebsocketsApi', 'FirstLambdaFunction'], Properties: { FunctionName: { - 'Fn::GetAtt': [ - 'FirstLambdaFunction', 'Arn', - ], + 'Fn::GetAtt': ['FirstLambdaFunction', 'Arn'], }, Action: 'lambda:InvokeFunction', - Principal: { - 'Fn::Join': [ - '', - [ - 'apigateway.', - { - Ref: 'AWS::URLSuffix', - }, - ], - ], - }, + Principal: 'apigateway.amazonaws.com', }, }, SecondLambdaPermissionWebsockets: { Type: 'AWS::Lambda::Permission', - DependsOn: [ - 'WebsocketsApi', - 'SecondLambdaFunction', - ], + DependsOn: ['WebsocketsApi', 'SecondLambdaFunction'], Properties: { FunctionName: { - 'Fn::GetAtt': [ - 'SecondLambdaFunction', - 'Arn', - ], + 'Fn::GetAtt': ['SecondLambdaFunction', 'Arn'], }, Action: 'lambda:InvokeFunction', - Principal: { - 'Fn::Join': [ - '', - [ - 'apigateway.', - { - Ref: 'AWS::URLSuffix', - }, - ], - ], - }, + Principal: 'apigateway.amazonaws.com', }, }, }); @@ -110,61 +79,31 @@ describe('#compilePermissions()', () => { }; return awsCompileWebsocketsEvents.compilePermissions().then(() => { - const resources = awsCompileWebsocketsEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources; + const resources = + awsCompileWebsocketsEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources; expect(resources).to.deep.equal({ FirstLambdaPermissionWebsockets: { Type: 'AWS::Lambda::Permission', - DependsOn: [ - 'WebsocketsApi', - 'FirstLambdaFunction', - ], + DependsOn: ['WebsocketsApi', 'FirstLambdaFunction'], Properties: { FunctionName: { - 'Fn::GetAtt': [ - 'FirstLambdaFunction', 'Arn', - ], + 'Fn::GetAtt': ['FirstLambdaFunction', 'Arn'], }, Action: 'lambda:InvokeFunction', - Principal: { - 'Fn::Join': [ - '', - [ - 'apigateway.', - { - Ref: 'AWS::URLSuffix', - }, - ], - ], - }, + Principal: 'apigateway.amazonaws.com', }, }, AuthLambdaPermissionWebsockets: { Type: 'AWS::Lambda::Permission', - DependsOn: [ - 'WebsocketsApi', - 'AuthLambdaPermissionWebsockets', - ], + DependsOn: ['WebsocketsApi', 'AuthLambdaPermissionWebsockets'], Properties: { FunctionName: { - 'Fn::GetAtt': [ - 'AuthLambdaPermissionWebsockets', - 'Arn', - ], + 'Fn::GetAtt': ['AuthLambdaPermissionWebsockets', 'Arn'], }, Action: 'lambda:InvokeFunction', - Principal: { - 'Fn::Join': [ - '', - [ - 'apigateway.', - { - Ref: 'AWS::URLSuffix', - }, - ], - ], - }, + Principal: 'apigateway.amazonaws.com', }, }, }); diff --git a/lib/plugins/aws/package/compile/events/websockets/lib/routes.js b/lib/plugins/aws/package/compile/events/websockets/lib/routes.js index ef3aa7a86..c88f32f89 100644 --- a/lib/plugins/aws/package/compile/events/websockets/lib/routes.js +++ b/lib/plugins/aws/package/compile/events/websockets/lib/routes.js @@ -6,28 +6,23 @@ const BbPromise = require('bluebird'); module.exports = { compileRoutes() { this.validated.events.forEach(event => { - const websocketsIntegrationLogicalId = this.provider.naming - .getWebsocketsIntegrationLogicalId(event.functionName); + const websocketsIntegrationLogicalId = this.provider.naming.getWebsocketsIntegrationLogicalId( + event.functionName + ); - const websocketsRouteLogicalId = this.provider.naming - .getWebsocketsRouteLogicalId(event.route); + const websocketsRouteLogicalId = this.provider.naming.getWebsocketsRouteLogicalId( + event.route + ); const routeTemplate = { [websocketsRouteLogicalId]: { Type: 'AWS::ApiGatewayV2::Route', Properties: { - ApiId: { - Ref: this.websocketsApiLogicalId, - }, + ApiId: this.provider.getApiGatewayWebsocketApiId(), RouteKey: event.route, AuthorizationType: 'NONE', Target: { - 'Fn::Join': ['/', - [ - 'integrations', - { Ref: websocketsIntegrationLogicalId }, - ], - ], + 'Fn::Join': ['/', ['integrations', { Ref: websocketsIntegrationLogicalId }]], }, }, }, @@ -36,12 +31,13 @@ module.exports = { if (event.authorizer) { routeTemplate[websocketsRouteLogicalId].Properties.AuthorizationType = 'CUSTOM'; routeTemplate[websocketsRouteLogicalId].Properties.AuthorizerId = { - Ref: this.provider.naming - .getWebsocketsAuthorizerLogicalId(event.authorizer.name), + Ref: this.provider.naming.getWebsocketsAuthorizerLogicalId(event.authorizer.name), }; } - _.merge(this.serverless.service.provider - .compiledCloudFormationTemplate.Resources, routeTemplate); + _.merge( + this.serverless.service.provider.compiledCloudFormationTemplate.Resources, + routeTemplate + ); }); return BbPromise.resolve(); diff --git a/lib/plugins/aws/package/compile/events/websockets/lib/routes.test.js b/lib/plugins/aws/package/compile/events/websockets/lib/routes.test.js index 5a4fb203a..7a3d1ee3a 100644 --- a/lib/plugins/aws/package/compile/events/websockets/lib/routes.test.js +++ b/lib/plugins/aws/package/compile/events/websockets/lib/routes.test.js @@ -15,8 +15,7 @@ describe('#compileRoutes()', () => { awsCompileWebsocketsEvents = new AwsCompileWebsocketsEvents(serverless); - awsCompileWebsocketsEvents.websocketsApiLogicalId - = awsCompileWebsocketsEvents.provider.naming.getWebsocketsApiLogicalId(); + awsCompileWebsocketsEvents.websocketsApiLogicalId = awsCompileWebsocketsEvents.provider.naming.getWebsocketsApiLogicalId(); }); it('should create a route resource for every event', () => { @@ -34,8 +33,9 @@ describe('#compileRoutes()', () => { }; return awsCompileWebsocketsEvents.compileRoutes().then(() => { - const resources = awsCompileWebsocketsEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources; + const resources = + awsCompileWebsocketsEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources; expect(resources).to.deep.equal({ SconnectWebsocketsRoute: { @@ -50,7 +50,8 @@ describe('#compileRoutes()', () => { 'Fn::Join': [ '/', [ - 'integrations', { + 'integrations', + { Ref: 'FirstWebsocketsIntegration', }, ], @@ -97,8 +98,9 @@ describe('#compileRoutes()', () => { }; return awsCompileWebsocketsEvents.compileRoutes().then(() => { - const resources = awsCompileWebsocketsEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources; + const resources = + awsCompileWebsocketsEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources; expect(resources).to.deep.equal({ SconnectWebsocketsRoute: { @@ -110,14 +112,16 @@ describe('#compileRoutes()', () => { RouteKey: '$connect', AuthorizationType: 'CUSTOM', AuthorizerId: { - Ref: awsCompileWebsocketsEvents.provider.naming - .getWebsocketsAuthorizerLogicalId('auth'), + Ref: awsCompileWebsocketsEvents.provider.naming.getWebsocketsAuthorizerLogicalId( + 'auth' + ), }, Target: { 'Fn::Join': [ '/', [ - 'integrations', { + 'integrations', + { Ref: 'FirstWebsocketsIntegration', }, ], diff --git a/lib/plugins/aws/package/compile/events/websockets/lib/stage.js b/lib/plugins/aws/package/compile/events/websockets/lib/stage.js index eecaa1579..af5b81e1e 100644 --- a/lib/plugins/aws/package/compile/events/websockets/lib/stage.js +++ b/lib/plugins/aws/package/compile/events/websockets/lib/stage.js @@ -1,30 +1,123 @@ 'use strict'; -const _ = require('lodash'); const BbPromise = require('bluebird'); module.exports = { compileStage() { - const websocketsStageLogicalId = this.provider.naming - .getWebsocketsStageLogicalId(); + const { service, provider } = this.serverless.service; + const stage = this.options.stage; + const cfTemplate = provider.compiledCloudFormationTemplate; - _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, { - [websocketsStageLogicalId]: { - Type: 'AWS::ApiGatewayV2::Stage', - Properties: { - ApiId: { - Ref: this.websocketsApiLogicalId, - }, - DeploymentId: { - Ref: this.websocketsDeploymentLogicalId, - }, - StageName: this.provider.getStage(), - Description: this.serverless.service.provider - .websocketsDescription || 'Serverless Websockets', + // logs + const logsEnabled = provider.logs && provider.logs.websocket; + + const stageLogicalId = this.provider.naming.getWebsocketsStageLogicalId(); + const logGroupLogicalId = this.provider.naming.getWebsocketsLogGroupLogicalId(); + const logsRoleLogicalId = this.provider.naming.getWebsocketsLogsRoleLogicalId(); + const accountLogicalid = this.provider.naming.getWebsocketsAccountLogicalId(); + + const stageResource = { + Type: 'AWS::ApiGatewayV2::Stage', + Properties: { + ApiId: this.provider.getApiGatewayWebsocketApiId(), + DeploymentId: { + Ref: this.websocketsDeploymentLogicalId, }, + StageName: this.provider.getStage(), + Description: + this.serverless.service.provider.websocketsDescription || 'Serverless Websockets', }, - }); + }; + + // create log-specific resources + if (logsEnabled) { + Object.assign(stageResource.Properties, { + AccessLogSettings: { + DestinationArn: { + 'Fn::Sub': `arn:aws:logs:\${AWS::Region}:\${AWS::AccountId}:log-group:\${${logGroupLogicalId}}`, + }, + Format: [ + '$context.identity.sourceIp', + '$context.identity.caller', + '$context.identity.user', + '[$context.requestTime]', + '"$context.eventType $context.routeKey $context.connectionId"', + '$context.status', + '$context.requestId', + ].join(' '), + }, + DefaultRouteSettings: { + DataTraceEnabled: true, + LoggingLevel: 'INFO', + }, + }); + + Object.assign(cfTemplate.Resources, { + [logGroupLogicalId]: getLogGroupResource(service, stage), + [logsRoleLogicalId]: getIamRoleResource(service, stage), + [accountLogicalid]: getAccountResource(logsRoleLogicalId), + }); + } + + Object.assign(cfTemplate.Resources, { [stageLogicalId]: stageResource }); return BbPromise.resolve(); }, }; + +function getLogGroupResource(service, stage) { + return { + Type: 'AWS::Logs::LogGroup', + Properties: { + LogGroupName: `/aws/websocket/${service}-${stage}`, + }, + }; +} + +function getIamRoleResource(service, stage) { + return { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Principal: { + Service: ['apigateway.amazonaws.com'], + }, + Action: ['sts:AssumeRole'], + }, + ], + }, + ManagedPolicyArns: [ + 'arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs', + ], + Path: '/', + RoleName: { + 'Fn::Join': [ + '-', + [ + service, + stage, + { + Ref: 'AWS::Region', + }, + 'apiGatewayLogsRole', + ], + ], + }, + }, + }; +} + +function getAccountResource(logsRoleLogicalId) { + return { + Type: 'AWS::ApiGateway::Account', + Properties: { + CloudWatchRoleArn: { + 'Fn::GetAtt': [logsRoleLogicalId, 'Arn'], + }, + }, + }; +} diff --git a/lib/plugins/aws/package/compile/events/websockets/lib/stage.test.js b/lib/plugins/aws/package/compile/events/websockets/lib/stage.test.js index 01dff2d81..5bc91404f 100644 --- a/lib/plugins/aws/package/compile/events/websockets/lib/stage.test.js +++ b/lib/plugins/aws/package/compile/events/websockets/lib/stage.test.js @@ -7,32 +7,171 @@ const AwsProvider = require('../../../../../provider/awsProvider'); describe('#compileStage()', () => { let awsCompileWebsocketsEvents; + let stageLogicalId; + let accountLogicalid; + let logsRoleLogicalId; + let logGroupLogicalId; beforeEach(() => { + const options = { + stage: 'dev', + region: 'us-east-1', + }; const serverless = new Serverless(); serverless.setProvider('aws', new AwsProvider(serverless)); + serverless.service.service = 'my-service'; serverless.service.provider.compiledCloudFormationTemplate = { Resources: {} }; - awsCompileWebsocketsEvents = new AwsCompileWebsocketsEvents(serverless); - - awsCompileWebsocketsEvents.websocketsApiLogicalId - = awsCompileWebsocketsEvents.provider.naming.getWebsocketsApiLogicalId(); - awsCompileWebsocketsEvents.websocketsDeploymentLogicalId - = awsCompileWebsocketsEvents.provider.naming.getWebsocketsDeploymentLogicalId(1234); + awsCompileWebsocketsEvents = new AwsCompileWebsocketsEvents(serverless, options); + stageLogicalId = awsCompileWebsocketsEvents.provider.naming.getWebsocketsStageLogicalId(); + accountLogicalid = awsCompileWebsocketsEvents.provider.naming.getWebsocketsAccountLogicalId(); + logsRoleLogicalId = awsCompileWebsocketsEvents.provider.naming.getWebsocketsLogsRoleLogicalId(); + logGroupLogicalId = awsCompileWebsocketsEvents.provider.naming.getWebsocketsLogGroupLogicalId(); + awsCompileWebsocketsEvents.websocketsApiLogicalId = awsCompileWebsocketsEvents.provider.naming.getWebsocketsApiLogicalId(); + awsCompileWebsocketsEvents.websocketsDeploymentLogicalId = awsCompileWebsocketsEvents.provider.naming.getWebsocketsDeploymentLogicalId( + 1234 + ); }); - it('should create a stage resource', () => awsCompileWebsocketsEvents.compileStage().then(() => { - const resources = awsCompileWebsocketsEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources; - const resourceKeys = Object.keys(resources); + it('should create a stage resource', () => + awsCompileWebsocketsEvents.compileStage().then(() => { + const resources = + awsCompileWebsocketsEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources; + const resourceKeys = Object.keys(resources); - expect(resourceKeys[0]).to.equal('WebsocketsDeploymentStage'); - expect(resources.WebsocketsDeploymentStage.Type).to.equal('AWS::ApiGatewayV2::Stage'); - expect(resources.WebsocketsDeploymentStage.Properties.ApiId).to.deep.equal({ - Ref: 'WebsocketsApi', + expect(resourceKeys[0]).to.equal(stageLogicalId); + expect(resources.WebsocketsDeploymentStage.Type).to.equal('AWS::ApiGatewayV2::Stage'); + expect(resources.WebsocketsDeploymentStage.Properties.ApiId).to.deep.equal({ + Ref: awsCompileWebsocketsEvents.websocketsApiLogicalId, + }); + expect(resources.WebsocketsDeploymentStage.Properties.DeploymentId).to.deep.equal({ + Ref: awsCompileWebsocketsEvents.websocketsDeploymentLogicalId, + }); + expect(resources.WebsocketsDeploymentStage.Properties.StageName).to.equal('dev'); + expect(resources.WebsocketsDeploymentStage.Properties.Description).to.equal( + 'Serverless Websockets' + ); + })); + + describe('logs', () => { + beforeEach(() => { + // setting up Websocket logs + awsCompileWebsocketsEvents.serverless.service.provider.logs = { + websocket: true, + }; }); - expect(resources.WebsocketsDeploymentStage.Properties.StageName).to.equal('dev'); - expect(resources.WebsocketsDeploymentStage.Properties.Description) - .to.equal('Serverless Websockets'); - })); + + it('should create a dedicated stage resource if logs are configured', () => + awsCompileWebsocketsEvents.compileStage().then(() => { + const resources = + awsCompileWebsocketsEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources; + + expect(resources[stageLogicalId]).to.deep.equal({ + Type: 'AWS::ApiGatewayV2::Stage', + Properties: { + ApiId: { + Ref: awsCompileWebsocketsEvents.websocketsApiLogicalId, + }, + DeploymentId: { + Ref: awsCompileWebsocketsEvents.websocketsDeploymentLogicalId, + }, + StageName: 'dev', + Description: 'Serverless Websockets', + AccessLogSettings: { + DestinationArn: { + 'Fn::Sub': + 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:${WebsocketsLogGroup}', + }, + Format: [ + '$context.identity.sourceIp', + '$context.identity.caller', + '$context.identity.user', + '[$context.requestTime]', + '"$context.eventType $context.routeKey $context.connectionId"', + '$context.status', + '$context.requestId', + ].join(' '), + }, + DefaultRouteSettings: { + DataTraceEnabled: true, + LoggingLevel: 'INFO', + }, + }, + }); + })); + + it('should create a Log Group resource', () => + awsCompileWebsocketsEvents.compileStage().then(() => { + const resources = + awsCompileWebsocketsEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources; + + expect(resources[logGroupLogicalId]).to.deep.equal({ + Type: 'AWS::Logs::LogGroup', + Properties: { + LogGroupName: '/aws/websocket/my-service-dev', + }, + }); + })); + + it('should create a IAM Role resource', () => + awsCompileWebsocketsEvents.compileStage().then(() => { + const resources = + awsCompileWebsocketsEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources; + + expect(resources[logsRoleLogicalId]).to.deep.equal({ + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: ['sts:AssumeRole'], + Effect: 'Allow', + Principal: { + Service: ['apigateway.amazonaws.com'], + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + 'arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs', + ], + Path: '/', + RoleName: { + 'Fn::Join': [ + '-', + [ + 'my-service', + 'dev', + { + Ref: 'AWS::Region', + }, + 'apiGatewayLogsRole', + ], + ], + }, + }, + }); + })); + + it('should create an Account resource', () => + awsCompileWebsocketsEvents.compileStage().then(() => { + const resources = + awsCompileWebsocketsEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources; + + expect(resources[accountLogicalid]).to.deep.equal({ + Type: 'AWS::ApiGateway::Account', + Properties: { + CloudWatchRoleArn: { + 'Fn::GetAtt': [logsRoleLogicalId, 'Arn'], + }, + }, + }); + })); + }); }); diff --git a/lib/plugins/aws/package/compile/events/websockets/lib/validate.js b/lib/plugins/aws/package/compile/events/websockets/lib/validate.js index f225bdc05..6eab1856a 100644 --- a/lib/plugins/aws/package/compile/events/websockets/lib/validate.js +++ b/lib/plugins/aws/package/compile/events/websockets/lib/validate.js @@ -6,13 +6,13 @@ module.exports = { validate() { const events = []; - const getAuthorizerNameFromArn = (arn) => { + const getAuthorizerNameFromArn = arn => { const splitArn = arn.split(':'); return splitArn[splitArn.length - 1]; }; _.forEach(this.serverless.service.functions, (functionObject, functionName) => { - _.forEach(functionObject.events, (event) => { + _.forEach(functionObject.events, event => { // check if we have both, `http` and `websocket` events which is not supported if (_.has(event, 'websocket') && _.has(event, 'http')) { const errorMessage = 'The event type can either be "http" or "websocket" but not both.'; @@ -33,11 +33,13 @@ module.exports = { // authorizers if (_.isString(event.websocket.authorizer)) { - if (event.websocket.authorizer.includes(':')) { // arn + if (event.websocket.authorizer.includes(':')) { + // arn websocketObj.authorizer = { name: getAuthorizerNameFromArn(event.websocket.authorizer), uri: { - 'Fn::Join': ['', + 'Fn::Join': [ + '', [ 'arn:', { Ref: 'AWS::Partition' }, @@ -52,13 +54,16 @@ module.exports = { identitySource: ['route.request.header.Auth'], permission: event.websocket.authorizer, }; - } else { // reference function - const lambdaLogicalId = this.provider.naming - .getLambdaLogicalId(event.websocket.authorizer); + } else { + // reference function + const lambdaLogicalId = this.provider.naming.getLambdaLogicalId( + event.websocket.authorizer + ); websocketObj.authorizer = { name: event.websocket.authorizer, uri: { - 'Fn::Join': ['', + 'Fn::Join': [ + '', [ 'arn:', { Ref: 'AWS::Partition' }, @@ -77,10 +82,12 @@ module.exports = { } else if (_.isObject(event.websocket.authorizer)) { websocketObj.authorizer = {}; if (event.websocket.authorizer.arn) { - websocketObj.authorizer.name = - getAuthorizerNameFromArn(event.websocket.authorizer.arn); + websocketObj.authorizer.name = getAuthorizerNameFromArn( + event.websocket.authorizer.arn + ); websocketObj.authorizer.uri = { - 'Fn::Join': ['', + 'Fn::Join': [ + '', [ 'arn:', { Ref: 'AWS::Partition' }, @@ -95,10 +102,12 @@ module.exports = { websocketObj.authorizer.permission = event.websocket.authorizer.arn; } else if (event.websocket.authorizer.name) { websocketObj.authorizer.name = event.websocket.authorizer.name; - const lambdaLogicalId = this.provider.naming - .getLambdaLogicalId(event.websocket.authorizer.name); + const lambdaLogicalId = this.provider.naming.getLambdaLogicalId( + event.websocket.authorizer.name + ); websocketObj.authorizer.uri = { - 'Fn::Join': ['', + 'Fn::Join': [ + '', [ 'arn:', { Ref: 'AWS::Partition' }, @@ -124,7 +133,7 @@ module.exports = { } } events.push(websocketObj); - // dealing with the simplified string representation + // dealing with the simplified string representation } else if (_.isString(event.websocket)) { events.push({ functionName, diff --git a/lib/plugins/aws/package/compile/events/websockets/lib/validate.test.js b/lib/plugins/aws/package/compile/events/websockets/lib/validate.test.js index 5f03b4317..072c14a15 100644 --- a/lib/plugins/aws/package/compile/events/websockets/lib/validate.test.js +++ b/lib/plugins/aws/package/compile/events/websockets/lib/validate.test.js @@ -80,7 +80,8 @@ describe('#validate()', () => { authorizer: { name: 'auth', uri: { - 'Fn::Join': ['', + 'Fn::Join': [ + '', [ 'arn:', { Ref: 'AWS::Partition' }, @@ -120,7 +121,8 @@ describe('#validate()', () => { authorizer: { name: 'auth', uri: { - 'Fn::Join': ['', + 'Fn::Join': [ + '', [ 'arn:', { Ref: 'AWS::Partition' }, @@ -163,7 +165,8 @@ describe('#validate()', () => { authorizer: { name: 'auth', uri: { - 'Fn::Join': ['', + 'Fn::Join': [ + '', [ 'arn:', { Ref: 'AWS::Partition' }, @@ -206,7 +209,8 @@ describe('#validate()', () => { authorizer: { name: 'auth', uri: { - 'Fn::Join': ['', + 'Fn::Join': [ + '', [ 'arn:', { Ref: 'AWS::Partition' }, @@ -236,7 +240,9 @@ describe('#validate()', () => { }, }; const validated = awsCompileWebsocketsEvents.validate(); - expect(validated.events).to.be.an('Array').with.length(0); + expect(validated.events) + .to.be.an('Array') + .with.length(0); }); it('should reject a websocket event without a route', () => { diff --git a/lib/plugins/aws/package/compile/functions/index.js b/lib/plugins/aws/package/compile/functions/index.js index 410480c74..5ab76c41d 100644 --- a/lib/plugins/aws/package/compile/functions/index.js +++ b/lib/plugins/aws/package/compile/functions/index.js @@ -1,5 +1,6 @@ 'use strict'; +const AWS = require('aws-sdk'); const BbPromise = require('bluebird'); const crypto = require('crypto'); const fs = require('fs'); @@ -11,26 +12,31 @@ class AwsCompileFunctions { this.serverless = serverless; this.options = options; const servicePath = this.serverless.config.servicePath || ''; - this.packagePath = this.serverless.service.package.path || - path.join(servicePath || '.', '.serverless'); + this.packagePath = + this.serverless.service.package.path || path.join(servicePath || '.', '.serverless'); this.provider = this.serverless.getProvider('aws'); - if (this.serverless.service.provider.versionFunctions === undefined || - this.serverless.service.provider.versionFunctions === null) { + if ( + this.serverless.service.provider.versionFunctions === undefined || + this.serverless.service.provider.versionFunctions === null + ) { this.serverless.service.provider.versionFunctions = true; } this.hooks = { - 'package:compileFunctions': () => BbPromise.bind(this) - .then(this.compileFunctions), + 'package:compileFunctions': () => + BbPromise.bind(this) + .then(this.downloadPackageArtifacts) + .then(this.compileFunctions), }; } compileRole(newFunction, role) { const compiledFunction = newFunction; - const unnsupportedRoleError = new this.serverless.classes - .Error(`Unsupported role provided: "${JSON.stringify(role)}"`); + const unnsupportedRoleError = new this.serverless.classes.Error( + `Unsupported role provided: "${JSON.stringify(role)}"` + ); switch (typeof role) { case 'object': @@ -52,9 +58,7 @@ class AwsCompileFunctions { } else if (role === 'IamRoleLambdaExecution') { // role is the default role generated by the framework compiledFunction.Properties.Role = { 'Fn::GetAtt': [role, 'Arn'] }; - compiledFunction.DependsOn = [ - 'IamRoleLambdaExecution', - ]; + compiledFunction.DependsOn = ['IamRoleLambdaExecution']; } else { // role is a Logical Role Name compiledFunction.Properties.Role = { 'Fn::GetAtt': [role, 'Arn'] }; @@ -66,6 +70,48 @@ class AwsCompileFunctions { } } + downloadPackageArtifact(functionName) { + const { region } = this.options; + const S3 = new AWS.S3({ region }); + + const functionObject = this.serverless.service.getFunction(functionName); + const artifactFilePath = + _.get(functionObject, 'package.artifact') || + _.get(this, 'serverless.service.package.artifact'); + + const regex = new RegExp('.*s3.amazonaws.com/(.+)/(.+)'); + const match = artifactFilePath.match(regex); + + if (match) { + return new BbPromise((resolve, reject) => { + const tmpDir = this.serverless.utils.getTmpDirPath(); + const filePath = path.join(tmpDir, match[2]); + + const readStream = S3.getObject({ + Bucket: match[1], + Key: match[2], + }).createReadStream(); + + const writeStream = fs.createWriteStream(filePath); + + readStream.on('error', error => reject(error)); + readStream + .pipe(writeStream) + .on('error', reject) + .on('close', () => { + if (functionObject.package.artifact) { + functionObject.package.artifact = filePath; + } else { + this.serverless.service.package.artifact = filePath; + } + return resolve(filePath); + }); + }); + } + + return BbPromise.resolve(); + } + compileFunction(functionName) { const newFunction = this.cfLambdaFunctionTemplate(); const functionObject = this.serverless.service.getFunction(functionName); @@ -74,17 +120,23 @@ class AwsCompileFunctions { const serviceArtifactFileName = this.provider.naming.getServiceArtifactName(); const functionArtifactFileName = this.provider.naming.getFunctionArtifactName(functionName); - let artifactFilePath = functionObject.package.artifact || - this.serverless.service.package.artifact; - if (!artifactFilePath || - (this.serverless.service.artifact && !functionObject.package.artifact)) { + let artifactFilePath = + functionObject.package.artifact || this.serverless.service.package.artifact; + + if ( + !artifactFilePath || + (this.serverless.service.artifact && !functionObject.package.artifact) + ) { let artifactFileName = serviceArtifactFileName; if (this.serverless.service.package.individually || functionObject.package.individually) { artifactFileName = functionArtifactFileName; } - artifactFilePath = path.join(this.serverless.config.servicePath - , '.serverless', artifactFileName); + artifactFilePath = path.join( + this.serverless.config.servicePath, + '.serverless', + artifactFileName + ); } if (this.serverless.service.package.deploymentBucket) { @@ -107,15 +159,14 @@ class AwsCompileFunctions { const Handler = functionObject.handler; const FunctionName = functionObject.name; - const MemorySize = Number(functionObject.memorySize) - || Number(this.serverless.service.provider.memorySize) - || 1024; - const Timeout = Number(functionObject.timeout) - || Number(this.serverless.service.provider.timeout) - || 6; - const Runtime = functionObject.runtime - || this.serverless.service.provider.runtime - || 'nodejs4.3'; + const MemorySize = + Number(functionObject.memorySize) || + Number(this.serverless.service.provider.memorySize) || + 1024; + const Timeout = + Number(functionObject.timeout) || Number(this.serverless.service.provider.timeout) || 6; + const Runtime = + functionObject.runtime || this.serverless.service.provider.runtime || 'nodejs10.x'; newFunction.Properties.Handler = Handler; newFunction.Properties.FunctionName = FunctionName; @@ -133,11 +184,7 @@ class AwsCompileFunctions { } if (functionObject.tags || this.serverless.service.provider.tags) { - const tags = Object.assign( - {}, - this.serverless.service.provider.tags, - functionObject.tags - ); + const tags = Object.assign({}, this.serverless.service.provider.tags, functionObject.tags); newFunction.Properties.Tags = []; _.forEach(tags, (Value, Key) => { newFunction.Properties.Tags.push({ Key, Value }); @@ -162,9 +209,7 @@ class AwsCompileFunctions { if (dlqType === 'sns') { stmt = { Effect: 'Allow', - Action: [ - 'sns:Publish', - ], + Action: ['sns:Publish'], Resource: [arn], }; } else if (dlqType === 'sqs') { @@ -210,7 +255,7 @@ class AwsCompileFunctions { if (typeof arn === 'string') { const splittedArn = arn.split(':'); - if (splittedArn[0] === 'arn' && (splittedArn[2] === 'kms')) { + if (splittedArn[0] === 'arn' && splittedArn[2] === 'kms') { const iamRoleLambdaExecution = this.serverless.service.provider .compiledCloudFormationTemplate.Resources.IamRoleLambdaExecution; @@ -218,9 +263,7 @@ class AwsCompileFunctions { const stmt = { Effect: 'Allow', - Action: [ - 'kms:Decrypt', - ], + Action: ['kms:Decrypt'], Resource: [arn], }; @@ -242,9 +285,9 @@ class AwsCompileFunctions { } } - const tracing = functionObject.tracing - || (this.serverless.service.provider.tracing - && this.serverless.service.provider.tracing.lambda); + const tracing = + functionObject.tracing || + (this.serverless.service.provider.tracing && this.serverless.service.provider.tracing.lambda); if (tracing) { if (typeof tracing === 'boolean' || typeof tracing === 'string') { @@ -263,10 +306,7 @@ class AwsCompileFunctions { const stmt = { Effect: 'Allow', - Action: [ - 'xray:PutTraceSegments', - 'xray:PutTelemetryRecords', - ], + Action: ['xray:PutTraceSegments', 'xray:PutTelemetryRecords'], Resource: ['*'], }; @@ -293,25 +333,23 @@ class AwsCompileFunctions { ); let invalidEnvVar = null; - _.forEach( - _.keys(newFunction.Properties.Environment.Variables), - key => { // eslint-disable-line consistent-return - // taken from the bash man pages - if (!key.match(/^[A-Za-z_][a-zA-Z0-9_]*$/)) { - invalidEnvVar = `Invalid characters in environment variable ${key}`; - return false; // break loop with lodash - } - const value = newFunction.Properties.Environment.Variables[key]; - if (_.isObject(value)) { - const isCFRef = _.isObject(value) && - !_.some(value, (v, k) => k !== 'Ref' && !_.startsWith(k, 'Fn::')); - if (!isCFRef) { - invalidEnvVar = `Environment variable ${key} must contain string`; - return false; - } + _.forEach(_.keys(newFunction.Properties.Environment.Variables), key => { + // taken from the bash man pages + if (!key.match(/^[A-Za-z_][a-zA-Z0-9_]*$/)) { + invalidEnvVar = `Invalid characters in environment variable ${key}`; + return false; // break loop with lodash + } + const value = newFunction.Properties.Environment.Variables[key]; + if (_.isObject(value)) { + const isCFRef = + _.isObject(value) && !_.some(value, (v, k) => k !== 'Ref' && !_.startsWith(k, 'Fn::')); + if (!isCFRef) { + invalidEnvVar = `Environment variable ${key} must contain string`; + return false; } } - ); + return true; + }); if (invalidEnvVar) { return BbPromise.reject(new this.serverless.classes.Error(invalidEnvVar)); @@ -330,13 +368,16 @@ class AwsCompileFunctions { if (!this.serverless.service.provider.vpc) this.serverless.service.provider.vpc = {}; newFunction.Properties.VpcConfig = { - SecurityGroupIds: functionObject.vpc.securityGroupIds || - this.serverless.service.provider.vpc.securityGroupIds, + SecurityGroupIds: + functionObject.vpc.securityGroupIds || + this.serverless.service.provider.vpc.securityGroupIds, SubnetIds: functionObject.vpc.subnetIds || this.serverless.service.provider.vpc.subnetIds, }; - if (!newFunction.Properties.VpcConfig.SecurityGroupIds - || !newFunction.Properties.VpcConfig.SubnetIds) { + if ( + !newFunction.Properties.VpcConfig.SecurityGroupIds || + !newFunction.Properties.VpcConfig.SubnetIds + ) { delete newFunction.Properties.VpcConfig; } @@ -356,24 +397,28 @@ class AwsCompileFunctions { } } - newFunction.DependsOn = [this.provider.naming.getLogGroupLogicalId(functionName)] - .concat(newFunction.DependsOn || []); + newFunction.DependsOn = [this.provider.naming.getLogGroupLogicalId(functionName)].concat( + newFunction.DependsOn || [] + ); if (functionObject.layers && _.isArray(functionObject.layers)) { newFunction.Properties.Layers = functionObject.layers; - } else if (this.serverless.service.provider.layers && _.isArray( - this.serverless.service.provider.layers)) { + } else if ( + this.serverless.service.provider.layers && + _.isArray(this.serverless.service.provider.layers) + ) { newFunction.Properties.Layers = this.serverless.service.provider.layers; } - const functionLogicalId = this.provider.naming - .getLambdaLogicalId(functionName); + const functionLogicalId = this.provider.naming.getLambdaLogicalId(functionName); const newFunctionObject = { [functionLogicalId]: newFunction, }; - _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, - newFunctionObject); + _.merge( + this.serverless.service.provider.compiledCloudFormationTemplate.Resources, + newFunctionObject + ); const newVersion = this.cfLambdaVersionTemplate(); @@ -390,18 +435,18 @@ class AwsCompileFunctions { return BbPromise.fromCallback(cb => { const readStream = fs.createReadStream(artifactFilePath); - readStream.on('data', chunk => { - fileHash.write(chunk); - versionHash.write(chunk); - }) - .on('end', () => { - cb(); - }) - .on('error', error => { - cb(error); - }); - }) - .then(() => { + readStream + .on('data', chunk => { + fileHash.write(chunk); + versionHash.write(chunk); + }) + .on('close', () => { + cb(); + }) + .on('error', error => { + cb(error); + }); + }).then(() => { // Include function configuration in version id hash (without the Code part) const properties = _.omit(_.get(newFunction, 'Properties', {}), 'Code'); _.forOwn(properties, value => { @@ -425,19 +470,24 @@ class AwsCompileFunctions { // use the version SHA in the logical resource ID of the version because // AWS::Lambda::Version resource will not support updates const versionLogicalId = this.provider.naming.getLambdaVersionLogicalId( - functionName, versionDigest); + functionName, + versionDigest + ); const newVersionObject = { [versionLogicalId]: newVersion, }; if (this.serverless.service.provider.versionFunctions) { - _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, - newVersionObject); + _.merge( + this.serverless.service.provider.compiledCloudFormationTemplate.Resources, + newVersionObject + ); } // Add function versions to Outputs section - const functionVersionOutputLogicalId = this.provider.naming - .getLambdaVersionOutputLogicalId(functionName); + const functionVersionOutputLogicalId = this.provider.naming.getLambdaVersionOutputLogicalId( + functionName + ); const newVersionOutput = this.cfOutputLatestVersionTemplate(); newVersionOutput.Value = { Ref: versionLogicalId }; @@ -452,18 +502,22 @@ class AwsCompileFunctions { }); } + downloadPackageArtifacts() { + const allFunctions = this.serverless.service.getAllFunctions(); + return BbPromise.each(allFunctions, functionName => this.downloadPackageArtifact(functionName)); + } + compileFunctions() { const allFunctions = this.serverless.service.getAllFunctions(); - return BbPromise.each( - allFunctions, - functionName => this.compileFunction(functionName) - ); + return BbPromise.each(allFunctions, functionName => this.compileFunction(functionName)); } // helper functions isArnRefGetAttOrImportValue(arn) { - return typeof arn === 'object' && - _.some(_.keys(arn), (k) => _.includes(['Ref', 'Fn::GetAtt', 'Fn::ImportValue'], k)); + return ( + typeof arn === 'object' && + _.some(_.keys(arn), k => _.includes(['Ref', 'Fn::GetAtt', 'Fn::ImportValue'], k)) + ); } cfLambdaFunctionTemplate() { diff --git a/lib/plugins/aws/package/compile/functions/index.test.js b/lib/plugins/aws/package/compile/functions/index.test.js index 722285ba2..237962404 100644 --- a/lib/plugins/aws/package/compile/functions/index.test.js +++ b/lib/plugins/aws/package/compile/functions/index.test.js @@ -1,19 +1,24 @@ 'use strict'; +const AWS = require('aws-sdk'); +const fs = require('fs'); const _ = require('lodash'); const path = require('path'); const chai = require('chai'); +const sinon = require('sinon'); const AwsProvider = require('../../../provider/awsProvider'); const AwsCompileFunctions = require('./index'); -const testUtils = require('../../../../../../tests/utils'); const Serverless = require('../../../../../Serverless'); +const { getTmpDirPath, createTmpFile } = require('../../../../../../tests/utils/fs'); chai.use(require('chai-as-promised')); +chai.use(require('sinon-chai')); const expect = chai.expect; describe('AwsCompileFunctions', () => { let serverless; + let awsProvider; let awsCompileFunctions; const functionName = 'test'; const compiledFunctionName = 'TestLambdaFunction'; @@ -24,7 +29,8 @@ describe('AwsCompileFunctions', () => { region: 'us-east-1', }; serverless = new Serverless(options); - serverless.setProvider('aws', new AwsProvider(serverless, options)); + awsProvider = new AwsProvider(serverless, options); + serverless.setProvider('aws', awsProvider); serverless.cli = new serverless.classes.CLI(); awsCompileFunctions = new AwsCompileFunctions(serverless, options); awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate = { @@ -34,23 +40,28 @@ describe('AwsCompileFunctions', () => { const serviceArtifact = 'new-service.zip'; const individualArtifact = 'test.zip'; - awsCompileFunctions.packagePath = testUtils.getTmpDirPath(); + awsCompileFunctions.packagePath = getTmpDirPath(); // The contents of the test artifacts need to be predictable so the hashes stay the same - serverless.utils.writeFileSync(path.join(awsCompileFunctions.packagePath, - serviceArtifact), 'foobar'); - serverless.utils.writeFileSync(path.join(awsCompileFunctions.packagePath, - individualArtifact), 'barbaz'); + serverless.utils.writeFileSync( + path.join(awsCompileFunctions.packagePath, serviceArtifact), + 'foobar' + ); + serverless.utils.writeFileSync( + path.join(awsCompileFunctions.packagePath, individualArtifact), + 'barbaz' + ); awsCompileFunctions.serverless.service.service = 'new-service'; awsCompileFunctions.serverless.service.package.artifactDirectoryName = 'somedir'; - awsCompileFunctions.serverless.service.package - .artifact = path.join(awsCompileFunctions.packagePath, serviceArtifact); + awsCompileFunctions.serverless.service.package.artifact = path.join( + awsCompileFunctions.packagePath, + serviceArtifact + ); awsCompileFunctions.serverless.service.functions = {}; awsCompileFunctions.serverless.service.functions[functionName] = { name: 'test', package: { - artifact: path.join(awsCompileFunctions.packagePath, - individualArtifact), + artifact: path.join(awsCompileFunctions.packagePath, individualArtifact), }, handler: 'handler.hello', }; @@ -63,34 +74,92 @@ describe('AwsCompileFunctions', () => { describe('#isArnRefGetAttOrImportValue()', () => { it('should accept a Ref', () => - expect(awsCompileFunctions.isArnRefGetAttOrImportValue({ Ref: 'DLQ' })).to.equal(true)); + expect(awsCompileFunctions.isArnRefGetAttOrImportValue({ Ref: 'DLQ' })).to.equal(true)); it('should accept a Fn::GetAtt', () => - expect(awsCompileFunctions.isArnRefGetAttOrImportValue({ 'Fn::GetAtt': ['DLQ', 'Arn'] })) - .to.equal(true)); + expect( + awsCompileFunctions.isArnRefGetAttOrImportValue({ 'Fn::GetAtt': ['DLQ', 'Arn'] }) + ).to.equal(true)); it('should accept a Fn::ImportValue', () => - expect(awsCompileFunctions.isArnRefGetAttOrImportValue({ 'Fn::ImportValue': 'DLQ' })) - .to.equal(true)); + expect( + awsCompileFunctions.isArnRefGetAttOrImportValue({ 'Fn::ImportValue': 'DLQ' }) + ).to.equal(true)); it('should reject other objects', () => expect(awsCompileFunctions.isArnRefGetAttOrImportValue({ Blah: 'vtha' })).to.equal(false)); }); + describe('#downloadPackageArtifacts()', () => { + let requestStub; + let testFilePath; + const s3BucketName = 'test-bucket'; + const s3ArtifactName = 's3-hosted-artifact.zip'; + + beforeEach(() => { + testFilePath = createTmpFile('dummy-artifact'); + requestStub = sinon.stub(AWS, 'S3').returns({ + getObject: () => ({ + createReadStream() { + return fs.createReadStream(testFilePath); + }, + }), + }); + }); + + afterEach(() => { + AWS.S3.restore(); + }); + + it('should download the file and replace the artifact path for function packages', () => { + awsCompileFunctions.serverless.service.package.individually = true; + awsCompileFunctions.serverless.service.functions[ + functionName + ].package.artifact = `https://s3.amazonaws.com/${s3BucketName}/${s3ArtifactName}`; + + return expect(awsCompileFunctions.downloadPackageArtifacts()).to.be.fulfilled.then(() => { + const artifactFileName = awsCompileFunctions.serverless.service.functions[ + functionName + ].package.artifact + .split(path.sep) + .pop(); + + expect(requestStub.callCount).to.equal(1); + expect(artifactFileName).to.equal(s3ArtifactName); + }); + }); + + it('should download the file and replace the artifact path for service-wide packages', () => { + awsCompileFunctions.serverless.service.package.individually = false; + awsCompileFunctions.serverless.service.functions[functionName].package.artifact = false; + awsCompileFunctions.serverless.service.package.artifact = `https://s3.amazonaws.com/${s3BucketName}/${s3ArtifactName}`; + + return expect(awsCompileFunctions.downloadPackageArtifacts()).to.be.fulfilled.then(() => { + const artifactFileName = awsCompileFunctions.serverless.service.package.artifact + .split(path.sep) + .pop(); + + expect(requestStub.callCount).to.equal(1); + expect(artifactFileName).to.equal(s3ArtifactName); + }); + }); + }); + describe('#compileFunctions()', () => { it('should use service artifact if not individually', () => { awsCompileFunctions.serverless.service.package.individually = false; const artifactTemp = awsCompileFunctions.serverless.service.functions.test.package.artifact; awsCompileFunctions.serverless.service.functions.test.package.artifact = false; - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { - const functionResource = awsCompileFunctions.serverless.service.provider - .compiledCloudFormationTemplate.Resources[compiledFunctionName]; + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { + const functionResource = + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + compiledFunctionName + ]; const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; const s3FileName = awsCompileFunctions.serverless.service.package.artifact - .split(path.sep).pop(); + .split(path.sep) + .pop(); - expect(functionResource.Properties.Code.S3Key) - .to.deep.equal(`${s3Folder}/${s3FileName}`); + expect(functionResource.Properties.Code.S3Key).to.deep.equal(`${s3Folder}/${s3FileName}`); awsCompileFunctions.serverless.service.functions.test.package.artifact = artifactTemp; }); }); @@ -98,34 +167,40 @@ describe('AwsCompileFunctions', () => { it('should use function artifact if individually', () => { awsCompileFunctions.serverless.service.package.individually = true; - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { - const functionResource = awsCompileFunctions.serverless.service.provider - .compiledCloudFormationTemplate.Resources[compiledFunctionName]; + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { + const functionResource = + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + compiledFunctionName + ]; const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; - const s3FileName = awsCompileFunctions.serverless.service - .functions[functionName].package.artifact.split(path.sep).pop(); + const s3FileName = awsCompileFunctions.serverless.service.functions[ + functionName + ].package.artifact + .split(path.sep) + .pop(); - expect(functionResource.Properties.Code.S3Key) - .to.deep.equal(`${s3Folder}/${s3FileName}`); + expect(functionResource.Properties.Code.S3Key).to.deep.equal(`${s3Folder}/${s3FileName}`); }); }); it('should use function artifact if individually at function level', () => { awsCompileFunctions.serverless.service.functions[functionName].package.individually = true; - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { - const functionResource = awsCompileFunctions.serverless.service.provider - .compiledCloudFormationTemplate.Resources[compiledFunctionName]; + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { + const functionResource = + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + compiledFunctionName + ]; const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; - const s3FileName = awsCompileFunctions.serverless.service - .functions[functionName].package.artifact.split(path.sep).pop(); + const s3FileName = awsCompileFunctions.serverless.service.functions[ + functionName + ].package.artifact + .split(path.sep) + .pop(); - expect(functionResource.Properties.Code.S3Key) - .to.deep.equal(`${s3Folder}/${s3FileName}`); + expect(functionResource.Properties.Code.S3Key).to.deep.equal(`${s3Folder}/${s3FileName}`); awsCompileFunctions.serverless.service.functions[functionName].package = { individually: false, }; @@ -142,12 +217,14 @@ describe('AwsCompileFunctions', () => { }, }; - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { - expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FuncLambdaFunction.DependsOn).to.deep.equal(['FuncLogGroup']); - expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FuncLambdaFunction.Properties.Role + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FuncLambdaFunction.DependsOn + ).to.deep.equal(['FuncLogGroup']); + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FuncLambdaFunction.Properties.Role ).to.deep.equal(awsCompileFunctions.serverless.service.provider.role); }); }); @@ -162,18 +239,16 @@ describe('AwsCompileFunctions', () => { }, }; - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { - expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FuncLambdaFunction.DependsOn + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FuncLambdaFunction.DependsOn ).to.deep.equal(['FuncLogGroup', 'LogicalNameRole']); - expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FuncLambdaFunction.Properties.Role + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FuncLambdaFunction.Properties.Role ).to.deep.equal({ - 'Fn::GetAtt': [ - awsCompileFunctions.serverless.service.provider.role, - 'Arn', - ], + 'Fn::GetAtt': [awsCompileFunctions.serverless.service.provider.role, 'Arn'], }); }); }); @@ -181,10 +256,7 @@ describe('AwsCompileFunctions', () => { it('should add a "Fn::GetAtt" Object provider role', () => { awsCompileFunctions.serverless.service.provider.name = 'aws'; awsCompileFunctions.serverless.service.provider.role = { - 'Fn::GetAtt': [ - 'LogicalRoleName', - 'Arn', - ], + 'Fn::GetAtt': ['LogicalRoleName', 'Arn'], }; awsCompileFunctions.serverless.service.functions = { func: { @@ -193,13 +265,14 @@ describe('AwsCompileFunctions', () => { }, }; - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { - expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FuncLambdaFunction.DependsOn + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FuncLambdaFunction.DependsOn ).to.deep.equal(['FuncLogGroup', 'LogicalRoleName']); - expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FuncLambdaFunction.Properties.Role + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FuncLambdaFunction.Properties.Role ).to.deep.equal(awsCompileFunctions.serverless.service.provider.role); }); }); @@ -214,12 +287,14 @@ describe('AwsCompileFunctions', () => { }, }; - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { - expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FuncLambdaFunction.DependsOn).to.deep.equal(['FuncLogGroup']); - expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FuncLambdaFunction.Properties.Role + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FuncLambdaFunction.DependsOn + ).to.deep.equal(['FuncLogGroup']); + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FuncLambdaFunction.Properties.Role ).to.deep.equal(awsCompileFunctions.serverless.service.functions.func.role); }); }); @@ -234,18 +309,16 @@ describe('AwsCompileFunctions', () => { }, }; - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { - expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FuncLambdaFunction.DependsOn + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FuncLambdaFunction.DependsOn ).to.deep.equal(['FuncLogGroup', 'LogicalRoleName']); - expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FuncLambdaFunction.Properties.Role + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FuncLambdaFunction.Properties.Role ).to.deep.equal({ - 'Fn::GetAtt': [ - awsCompileFunctions.serverless.service.functions.func.role, - 'Arn', - ], + 'Fn::GetAtt': [awsCompileFunctions.serverless.service.functions.func.role, 'Arn'], }); }); }); @@ -257,21 +330,19 @@ describe('AwsCompileFunctions', () => { handler: 'func.function.handler', name: 'new-service-dev-func', role: { - 'Fn::GetAtt': [ - 'LogicalRoleName', - 'Arn', - ], + 'Fn::GetAtt': ['LogicalRoleName', 'Arn'], }, }, }; - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { - expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FuncLambdaFunction.DependsOn + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FuncLambdaFunction.DependsOn ).to.deep.equal(['FuncLogGroup', 'LogicalRoleName']); - expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FuncLambdaFunction.Properties.Role + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FuncLambdaFunction.Properties.Role ).to.deep.equal(awsCompileFunctions.serverless.service.functions.func.role); }); }); @@ -288,12 +359,14 @@ describe('AwsCompileFunctions', () => { }, }; - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { - expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FuncLambdaFunction.DependsOn).to.deep.equal(['FuncLogGroup']); - expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FuncLambdaFunction.Properties.Role + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FuncLambdaFunction.DependsOn + ).to.deep.equal(['FuncLogGroup']); + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FuncLambdaFunction.Properties.Role ).to.deep.equal(awsCompileFunctions.serverless.service.functions.func.role); }); }); @@ -309,12 +382,14 @@ describe('AwsCompileFunctions', () => { }, }; - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { - expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FuncLambdaFunction.DependsOn).to.deep.equal(['FuncLogGroup']); - expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FuncLambdaFunction.Properties.Role + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FuncLambdaFunction.DependsOn + ).to.deep.equal(['FuncLogGroup']); + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FuncLambdaFunction.Properties.Role ).to.equal(awsCompileFunctions.serverless.service.functions.func.role); }); }); @@ -334,18 +409,23 @@ describe('AwsCompileFunctions', () => { }, }; - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { - expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Resources.Func0LambdaFunction.DependsOn).to.deep.equal(['Func0LogGroup']); - expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Resources.Func0LambdaFunction.Properties.Role + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources + .Func0LambdaFunction.DependsOn + ).to.deep.equal(['Func0LogGroup']); + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources + .Func0LambdaFunction.Properties.Role ).to.deep.equal(awsCompileFunctions.serverless.service.functions.func0.role); - expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Resources.Func1LambdaFunction.DependsOn).to.deep.equal(['Func1LogGroup']); - expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Resources.Func1LambdaFunction.Properties.Role + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources + .Func1LambdaFunction.DependsOn + ).to.deep.equal(['Func1LogGroup']); + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources + .Func1LambdaFunction.Properties.Role ).to.deep.equal(awsCompileFunctions.serverless.service.functions.func1.role); }); }); @@ -365,18 +445,23 @@ describe('AwsCompileFunctions', () => { }, }; - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { - expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Resources.Func0LambdaFunction.DependsOn).to.deep.equal(['Func0LogGroup']); - expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Resources.Func0LambdaFunction.Properties.Role + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources + .Func0LambdaFunction.DependsOn + ).to.deep.equal(['Func0LogGroup']); + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources + .Func0LambdaFunction.Properties.Role ).to.deep.equal(awsCompileFunctions.serverless.service.provider.role); - expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Resources.Func1LambdaFunction.DependsOn).to.deep.equal(['Func1LogGroup']); - expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Resources.Func1LambdaFunction.Properties.Role + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources + .Func1LambdaFunction.DependsOn + ).to.deep.equal(['Func1LogGroup']); + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources + .Func1LambdaFunction.Properties.Role ).to.deep.equal(awsCompileFunctions.serverless.service.functions.func1.role); }); }); @@ -388,13 +473,14 @@ describe('AwsCompileFunctions', () => { }, }; - expect(awsCompileFunctions.compileFunctions()).to.be.rejectedWith(Error); + return expect(awsCompileFunctions.compileFunctions()).to.be.rejectedWith(Error); }); it('should create a simple function resource', () => { const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; const s3FileName = awsCompileFunctions.serverless.service.package.artifact - .split(path.sep).pop(); + .split(path.sep) + .pop(); awsCompileFunctions.serverless.service.functions = { func: { handler: 'func.function.handler', @@ -403,10 +489,7 @@ describe('AwsCompileFunctions', () => { }; const compiledFunction = { Type: 'AWS::Lambda::Function', - DependsOn: [ - 'FuncLogGroup', - 'IamRoleLambdaExecution', - ], + DependsOn: ['FuncLogGroup', 'IamRoleLambdaExecution'], Properties: { Code: { S3Key: `${s3Folder}/${s3FileName}`, @@ -416,16 +499,15 @@ describe('AwsCompileFunctions', () => { Handler: 'func.function.handler', MemorySize: 1024, Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, - Runtime: 'nodejs4.3', + Runtime: 'nodejs10.x', Timeout: 6, }, }; - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { expect( - awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FuncLambdaFunction + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FuncLambdaFunction ).to.deep.equal(compiledFunction); }); }); @@ -433,7 +515,8 @@ describe('AwsCompileFunctions', () => { it('should create a function resource with provider level vpc config', () => { const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; const s3FileName = awsCompileFunctions.serverless.service.package.artifact - .split(path.sep).pop(); + .split(path.sep) + .pop(); awsCompileFunctions.serverless.service.provider.vpc = { securityGroupIds: ['xxx'], subnetIds: ['xxx'], @@ -447,10 +530,7 @@ describe('AwsCompileFunctions', () => { }; const compiledFunction = { Type: 'AWS::Lambda::Function', - DependsOn: [ - 'FuncLogGroup', - 'IamRoleLambdaExecution', - ], + DependsOn: ['FuncLogGroup', 'IamRoleLambdaExecution'], Properties: { Code: { S3Key: `${s3Folder}/${s3FileName}`, @@ -460,7 +540,7 @@ describe('AwsCompileFunctions', () => { Handler: 'func.function.handler', MemorySize: 1024, Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, - Runtime: 'nodejs4.3', + Runtime: 'nodejs10.x', Timeout: 6, VpcConfig: { SecurityGroupIds: ['xxx'], @@ -469,11 +549,10 @@ describe('AwsCompileFunctions', () => { }, }; - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { expect( - awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FuncLambdaFunction + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FuncLambdaFunction ).to.deep.equal(compiledFunction); }); }); @@ -481,7 +560,8 @@ describe('AwsCompileFunctions', () => { it('should create a function resource with function level vpc config', () => { const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; const s3FileName = awsCompileFunctions.serverless.service.package.artifact - .split(path.sep).pop(); + .split(path.sep) + .pop(); awsCompileFunctions.serverless.service.functions = { func: { handler: 'func.function.handler', @@ -494,10 +574,7 @@ describe('AwsCompileFunctions', () => { }; const compiledFunction = { Type: 'AWS::Lambda::Function', - DependsOn: [ - 'FuncLogGroup', - 'IamRoleLambdaExecution', - ], + DependsOn: ['FuncLogGroup', 'IamRoleLambdaExecution'], Properties: { Code: { S3Key: `${s3Folder}/${s3FileName}`, @@ -507,7 +584,7 @@ describe('AwsCompileFunctions', () => { Handler: 'func.function.handler', MemorySize: 1024, Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, - Runtime: 'nodejs4.3', + Runtime: 'nodejs10.x', Timeout: 6, VpcConfig: { SecurityGroupIds: ['xxx'], @@ -516,11 +593,10 @@ describe('AwsCompileFunctions', () => { }, }; - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { expect( - awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FuncLambdaFunction + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FuncLambdaFunction ).to.deep.equal(compiledFunction); }); }); @@ -528,7 +604,8 @@ describe('AwsCompileFunctions', () => { it('should create a function resource with provider level tags', () => { const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; const s3FileName = awsCompileFunctions.serverless.service.package.artifact - .split(path.sep).pop(); + .split(path.sep) + .pop(); awsCompileFunctions.serverless.service.functions = { func: { handler: 'func.function.handler', @@ -543,10 +620,7 @@ describe('AwsCompileFunctions', () => { const compiledFunction = { Type: 'AWS::Lambda::Function', - DependsOn: [ - 'FuncLogGroup', - 'IamRoleLambdaExecution', - ], + DependsOn: ['FuncLogGroup', 'IamRoleLambdaExecution'], Properties: { Code: { S3Key: `${s3Folder}/${s3FileName}`, @@ -556,20 +630,16 @@ describe('AwsCompileFunctions', () => { Handler: 'func.function.handler', MemorySize: 1024, Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, - Runtime: 'nodejs4.3', + Runtime: 'nodejs10.x', Timeout: 6, - Tags: [ - { Key: 'foo', Value: 'bar' }, - { Key: 'baz', Value: 'qux' }, - ], + Tags: [{ Key: 'foo', Value: 'bar' }, { Key: 'baz', Value: 'qux' }], }, }; - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { expect( - awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FuncLambdaFunction + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FuncLambdaFunction ).to.deep.equal(compiledFunction); }); }); @@ -577,7 +647,8 @@ describe('AwsCompileFunctions', () => { it('should create a function resource with function level tags', () => { const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; const s3FileName = awsCompileFunctions.serverless.service.package.artifact - .split(path.sep).pop(); + .split(path.sep) + .pop(); awsCompileFunctions.serverless.service.functions = { func: { handler: 'func.function.handler', @@ -591,10 +662,7 @@ describe('AwsCompileFunctions', () => { const compiledFunction = { Type: 'AWS::Lambda::Function', - DependsOn: [ - 'FuncLogGroup', - 'IamRoleLambdaExecution', - ], + DependsOn: ['FuncLogGroup', 'IamRoleLambdaExecution'], Properties: { Code: { S3Key: `${s3Folder}/${s3FileName}`, @@ -604,20 +672,16 @@ describe('AwsCompileFunctions', () => { Handler: 'func.function.handler', MemorySize: 1024, Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, - Runtime: 'nodejs4.3', + Runtime: 'nodejs10.x', Timeout: 6, - Tags: [ - { Key: 'foo', Value: 'bar' }, - { Key: 'baz', Value: 'qux' }, - ], + Tags: [{ Key: 'foo', Value: 'bar' }, { Key: 'baz', Value: 'qux' }], }, }; - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { expect( - awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FuncLambdaFunction + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FuncLambdaFunction ).to.deep.equal(compiledFunction); }); }); @@ -625,7 +689,8 @@ describe('AwsCompileFunctions', () => { it('should create a function resource with provider and function level tags', () => { const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; const s3FileName = awsCompileFunctions.serverless.service.package.artifact - .split(path.sep).pop(); + .split(path.sep) + .pop(); awsCompileFunctions.serverless.service.functions = { func: { handler: 'func.function.handler', @@ -644,10 +709,7 @@ describe('AwsCompileFunctions', () => { const compiledFunction = { Type: 'AWS::Lambda::Function', - DependsOn: [ - 'FuncLogGroup', - 'IamRoleLambdaExecution', - ], + DependsOn: ['FuncLogGroup', 'IamRoleLambdaExecution'], Properties: { Code: { S3Key: `${s3Folder}/${s3FileName}`, @@ -657,7 +719,7 @@ describe('AwsCompileFunctions', () => { Handler: 'func.function.handler', MemorySize: 1024, Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, - Runtime: 'nodejs4.3', + Runtime: 'nodejs10.x', Timeout: 6, Tags: [ { Key: 'foo', Value: 'bar' }, @@ -667,11 +729,10 @@ describe('AwsCompileFunctions', () => { }, }; - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { expect( - awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FuncLambdaFunction + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FuncLambdaFunction ).to.deep.equal(compiledFunction); }); }); @@ -682,8 +743,7 @@ describe('AwsCompileFunctions', () => { beforeEach(() => { s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; - s3FileName = awsCompileFunctions.serverless.service.package.artifact - .split(path.sep).pop(); + s3FileName = awsCompileFunctions.serverless.service.package.artifact.split(path.sep).pop(); }); it('should reject if config is provided as a number', () => { @@ -727,18 +787,17 @@ describe('AwsCompileFunctions', () => { describe('when IamRoleLambdaExecution is used', () => { beforeEach(() => { // pretend that the IamRoleLambdaExecution is used - awsCompileFunctions.serverless.service.provider - .compiledCloudFormationTemplate.Resources.IamRoleLambdaExecution = { - Properties: { - Policies: [ - { - PolicyDocument: { - Statement: [], - }, + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources.IamRoleLambdaExecution = { + Properties: { + Policies: [ + { + PolicyDocument: { + Statement: [], }, - ], - }, - }; + }, + ], + }, + }; }); it('should create necessary resources if a SNS arn is provided', () => { @@ -752,10 +811,7 @@ describe('AwsCompileFunctions', () => { const compiledFunction = { Type: 'AWS::Lambda::Function', - DependsOn: [ - 'FuncLogGroup', - 'IamRoleLambdaExecution', - ], + DependsOn: ['FuncLogGroup', 'IamRoleLambdaExecution'], Properties: { Code: { S3Key: `${s3Folder}/${s3FileName}`, @@ -765,7 +821,7 @@ describe('AwsCompileFunctions', () => { Handler: 'func.function.handler', MemorySize: 1024, Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, - Runtime: 'nodejs4.3', + Runtime: 'nodejs10.x', Timeout: 6, DeadLetterConfig: { TargetArn: 'arn:aws:sns:region:accountid:foo', @@ -775,20 +831,18 @@ describe('AwsCompileFunctions', () => { const compiledDlqStatement = { Effect: 'Allow', - Action: [ - 'sns:Publish', - ], + Action: ['sns:Publish'], Resource: ['arn:aws:sns:region:accountid:foo'], }; - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { - const compiledCfTemplate = awsCompileFunctions.serverless.service.provider - .compiledCloudFormationTemplate; + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { + const compiledCfTemplate = + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate; const functionResource = compiledCfTemplate.Resources.FuncLambdaFunction; - const dlqStatement = compiledCfTemplate.Resources - .IamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement[0]; + const dlqStatement = + compiledCfTemplate.Resources.IamRoleLambdaExecution.Properties.Policies[0] + .PolicyDocument.Statement[0]; expect(functionResource).to.deep.equal(compiledFunction); expect(dlqStatement).to.deep.equal(compiledDlqStatement); @@ -804,8 +858,9 @@ describe('AwsCompileFunctions', () => { }, }; - return expect(awsCompileFunctions.compileFunctions()) - .to.be.rejectedWith('only supports SNS'); + return expect(awsCompileFunctions.compileFunctions()).to.be.rejectedWith( + 'only supports SNS' + ); }); it('should create necessary resources if a Ref is provided', () => { @@ -821,10 +876,7 @@ describe('AwsCompileFunctions', () => { const compiledFunction = { Type: 'AWS::Lambda::Function', - DependsOn: [ - 'FuncLogGroup', - 'IamRoleLambdaExecution', - ], + DependsOn: ['FuncLogGroup', 'IamRoleLambdaExecution'], Properties: { Code: { S3Key: `${s3Folder}/${s3FileName}`, @@ -834,7 +886,7 @@ describe('AwsCompileFunctions', () => { Handler: 'func.function.handler', MemorySize: 1024, Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, - Runtime: 'nodejs4.3', + Runtime: 'nodejs10.x', Timeout: 6, DeadLetterConfig: { TargetArn: { @@ -844,10 +896,9 @@ describe('AwsCompileFunctions', () => { }, }; - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { - const compiledCfTemplate = awsCompileFunctions.serverless.service.provider - .compiledCloudFormationTemplate; + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { + const compiledCfTemplate = + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate; const functionResource = compiledCfTemplate.Resources.FuncLambdaFunction; expect(functionResource).to.deep.equal(compiledFunction); @@ -867,10 +918,7 @@ describe('AwsCompileFunctions', () => { const compiledFunction = { Type: 'AWS::Lambda::Function', - DependsOn: [ - 'FuncLogGroup', - 'IamRoleLambdaExecution', - ], + DependsOn: ['FuncLogGroup', 'IamRoleLambdaExecution'], Properties: { Code: { S3Key: `${s3Folder}/${s3FileName}`, @@ -880,7 +928,7 @@ describe('AwsCompileFunctions', () => { Handler: 'func.function.handler', MemorySize: 1024, Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, - Runtime: 'nodejs4.3', + Runtime: 'nodejs10.x', Timeout: 6, DeadLetterConfig: { TargetArn: { @@ -890,10 +938,9 @@ describe('AwsCompileFunctions', () => { }, }; - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { - const compiledCfTemplate = awsCompileFunctions.serverless.service.provider - .compiledCloudFormationTemplate; + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { + const compiledCfTemplate = + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate; const functionResource = compiledCfTemplate.Resources.FuncLambdaFunction; expect(functionResource).to.deep.equal(compiledFunction); @@ -913,10 +960,7 @@ describe('AwsCompileFunctions', () => { const compiledFunction = { Type: 'AWS::Lambda::Function', - DependsOn: [ - 'FuncLogGroup', - 'IamRoleLambdaExecution', - ], + DependsOn: ['FuncLogGroup', 'IamRoleLambdaExecution'], Properties: { Code: { S3Key: `${s3Folder}/${s3FileName}`, @@ -926,7 +970,7 @@ describe('AwsCompileFunctions', () => { Handler: 'func.function.handler', MemorySize: 1024, Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, - Runtime: 'nodejs4.3', + Runtime: 'nodejs10.x', Timeout: 6, DeadLetterConfig: { TargetArn: { @@ -936,10 +980,9 @@ describe('AwsCompileFunctions', () => { }, }; - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { - const compiledCfTemplate = awsCompileFunctions.serverless.service.provider - .compiledCloudFormationTemplate; + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { + const compiledCfTemplate = + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate; const functionResource = compiledCfTemplate.Resources.FuncLambdaFunction; expect(functionResource).to.deep.equal(compiledFunction); @@ -959,10 +1002,7 @@ describe('AwsCompileFunctions', () => { const compiledFunction = { Type: 'AWS::Lambda::Function', - DependsOn: [ - 'FuncLogGroup', - 'IamRoleLambdaExecution', - ], + DependsOn: ['FuncLogGroup', 'IamRoleLambdaExecution'], Properties: { Code: { S3Key: `${s3Folder}/${s3FileName}`, @@ -972,7 +1012,7 @@ describe('AwsCompileFunctions', () => { Handler: 'func.function.handler', MemorySize: 1024, Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, - Runtime: 'nodejs4.3', + Runtime: 'nodejs10.x', Timeout: 6, DeadLetterConfig: { TargetArn: 'arn:aws:sns:region:accountid:foo', @@ -980,10 +1020,9 @@ describe('AwsCompileFunctions', () => { }, }; - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { - const compiledCfTemplate = awsCompileFunctions.serverless.service.provider - .compiledCloudFormationTemplate; + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { + const compiledCfTemplate = + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate; const functionResource = compiledCfTemplate.Resources.FuncLambdaFunction; @@ -1000,8 +1039,9 @@ describe('AwsCompileFunctions', () => { }, }; - return expect(awsCompileFunctions.compileFunctions()) - .to.be.rejectedWith('only supports SNS'); + return expect(awsCompileFunctions.compileFunctions()).to.be.rejectedWith( + 'only supports SNS' + ); }); }); }); @@ -1012,8 +1052,7 @@ describe('AwsCompileFunctions', () => { beforeEach(() => { s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; - s3FileName = awsCompileFunctions.serverless.service.package.artifact - .split(path.sep).pop(); + s3FileName = awsCompileFunctions.serverless.service.package.artifact.split(path.sep).pop(); }); it('should reject if config is provided as a number', () => { @@ -1025,8 +1064,9 @@ describe('AwsCompileFunctions', () => { }, }; - return expect(awsCompileFunctions.compileFunctions()) - .to.be.rejectedWith('provided as a string'); + return expect(awsCompileFunctions.compileFunctions()).to.be.rejectedWith( + 'provided as a string' + ); }); it('should reject if config is provided as an object', () => { @@ -1040,8 +1080,9 @@ describe('AwsCompileFunctions', () => { }, }; - return expect(awsCompileFunctions.compileFunctions()) - .to.be.rejectedWith('provided as a string'); + return expect(awsCompileFunctions.compileFunctions()).to.be.rejectedWith( + 'provided as a string' + ); }); it('should throw an error if config is not a KMS key arn', () => { @@ -1053,8 +1094,7 @@ describe('AwsCompileFunctions', () => { }, }; - return expect(awsCompileFunctions.compileFunctions()) - .to.be.rejectedWith('KMS key arn'); + return expect(awsCompileFunctions.compileFunctions()).to.be.rejectedWith('KMS key arn'); }); it('should use a the service KMS key arn if provided', () => { @@ -1072,10 +1112,7 @@ describe('AwsCompileFunctions', () => { const compiledFunction = { Type: 'AWS::Lambda::Function', - DependsOn: [ - 'FuncLogGroup', - 'IamRoleLambdaExecution', - ], + DependsOn: ['FuncLogGroup', 'IamRoleLambdaExecution'], Properties: { Code: { S3Key: `${s3Folder}/${s3FileName}`, @@ -1085,16 +1122,15 @@ describe('AwsCompileFunctions', () => { Handler: 'func.function.handler', MemorySize: 1024, Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, - Runtime: 'nodejs4.3', + Runtime: 'nodejs10.x', Timeout: 6, KmsKeyArn: 'arn:aws:kms:region:accountid:foo/bar', }, }; - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { - const compiledCfTemplate = awsCompileFunctions.serverless.service.provider - .compiledCloudFormationTemplate; + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { + const compiledCfTemplate = + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate; const functionResource = compiledCfTemplate.Resources.FuncLambdaFunction; expect(functionResource).to.deep.equal(compiledFunction); }); @@ -1120,10 +1156,7 @@ describe('AwsCompileFunctions', () => { const compiledFunction1 = { Type: 'AWS::Lambda::Function', - DependsOn: [ - 'Func1LogGroup', - 'IamRoleLambdaExecution', - ], + DependsOn: ['Func1LogGroup', 'IamRoleLambdaExecution'], Properties: { Code: { S3Key: `${s3Folder}/${s3FileName}`, @@ -1133,7 +1166,7 @@ describe('AwsCompileFunctions', () => { Handler: 'func1.function.handler', MemorySize: 1024, Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, - Runtime: 'nodejs4.3', + Runtime: 'nodejs10.x', Timeout: 6, KmsKeyArn: 'arn:aws:kms:region:accountid:foo/function', }, @@ -1141,10 +1174,7 @@ describe('AwsCompileFunctions', () => { const compiledFunction2 = { Type: 'AWS::Lambda::Function', - DependsOn: [ - 'Func2LogGroup', - 'IamRoleLambdaExecution', - ], + DependsOn: ['Func2LogGroup', 'IamRoleLambdaExecution'], Properties: { Code: { S3Key: `${s3Folder}/${s3FileName}`, @@ -1154,16 +1184,15 @@ describe('AwsCompileFunctions', () => { Handler: 'func2.function.handler', MemorySize: 1024, Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, - Runtime: 'nodejs4.3', + Runtime: 'nodejs10.x', Timeout: 6, KmsKeyArn: 'arn:aws:kms:region:accountid:foo/service', }, }; - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { - const compiledCfTemplate = awsCompileFunctions.serverless.service.provider - .compiledCloudFormationTemplate; + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { + const compiledCfTemplate = + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate; const function1Resource = compiledCfTemplate.Resources.Func1LambdaFunction; const function2Resource = compiledCfTemplate.Resources.Func2LambdaFunction; @@ -1175,18 +1204,17 @@ describe('AwsCompileFunctions', () => { describe('when IamRoleLambdaExecution is used', () => { beforeEach(() => { // pretend that the IamRoleLambdaExecution is used - awsCompileFunctions.serverless.service.provider - .compiledCloudFormationTemplate.Resources.IamRoleLambdaExecution = { - Properties: { - Policies: [ - { - PolicyDocument: { - Statement: [], - }, + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources.IamRoleLambdaExecution = { + Properties: { + Policies: [ + { + PolicyDocument: { + Statement: [], }, - ], - }, - }; + }, + ], + }, + }; }); it('should create necessary resources if a KMS key arn is provided', () => { @@ -1200,10 +1228,7 @@ describe('AwsCompileFunctions', () => { const compiledFunction = { Type: 'AWS::Lambda::Function', - DependsOn: [ - 'FuncLogGroup', - 'IamRoleLambdaExecution', - ], + DependsOn: ['FuncLogGroup', 'IamRoleLambdaExecution'], Properties: { Code: { S3Key: `${s3Folder}/${s3FileName}`, @@ -1213,7 +1238,7 @@ describe('AwsCompileFunctions', () => { Handler: 'func.function.handler', MemorySize: 1024, Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, - Runtime: 'nodejs4.3', + Runtime: 'nodejs10.x', Timeout: 6, KmsKeyArn: 'arn:aws:kms:region:accountid:foo/bar', }, @@ -1221,20 +1246,18 @@ describe('AwsCompileFunctions', () => { const compiledKmsStatement = { Effect: 'Allow', - Action: [ - 'kms:Decrypt', - ], + Action: ['kms:Decrypt'], Resource: ['arn:aws:kms:region:accountid:foo/bar'], }; - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { - const compiledCfTemplate = awsCompileFunctions.serverless.service.provider - .compiledCloudFormationTemplate; + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { + const compiledCfTemplate = + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate; const functionResource = compiledCfTemplate.Resources.FuncLambdaFunction; - const dlqStatement = compiledCfTemplate.Resources - .IamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement[0]; + const dlqStatement = + compiledCfTemplate.Resources.IamRoleLambdaExecution.Properties.Policies[0] + .PolicyDocument.Statement[0]; expect(functionResource).to.deep.equal(compiledFunction); expect(dlqStatement).to.deep.equal(compiledKmsStatement); @@ -1254,10 +1277,7 @@ describe('AwsCompileFunctions', () => { const compiledFunction = { Type: 'AWS::Lambda::Function', - DependsOn: [ - 'FuncLogGroup', - 'IamRoleLambdaExecution', - ], + DependsOn: ['FuncLogGroup', 'IamRoleLambdaExecution'], Properties: { Code: { S3Key: `${s3Folder}/${s3FileName}`, @@ -1267,16 +1287,15 @@ describe('AwsCompileFunctions', () => { Handler: 'func.function.handler', MemorySize: 1024, Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, - Runtime: 'nodejs4.3', + Runtime: 'nodejs10.x', Timeout: 6, KmsKeyArn: 'arn:aws:kms:region:accountid:foo/bar', }, }; - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { - const compiledCfTemplate = awsCompileFunctions.serverless.service.provider - .compiledCloudFormationTemplate; + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { + const compiledCfTemplate = + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate; const functionResource = compiledCfTemplate.Resources.FuncLambdaFunction; @@ -1292,8 +1311,7 @@ describe('AwsCompileFunctions', () => { beforeEach(() => { s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; - s3FileName = awsCompileFunctions.serverless.service.package.artifact - .split(path.sep).pop(); + s3FileName = awsCompileFunctions.serverless.service.package.artifact.split(path.sep).pop(); }); it('should throw an error if config paramter is not a string', () => { @@ -1305,8 +1323,7 @@ describe('AwsCompileFunctions', () => { }, }; - return expect(awsCompileFunctions.compileFunctions()) - .to.be.rejectedWith('as a string'); + return expect(awsCompileFunctions.compileFunctions()).to.be.rejectedWith('as a string'); }); it('should use a the provider wide tracing config if provided', () => { @@ -1325,10 +1342,7 @@ describe('AwsCompileFunctions', () => { const compiledFunction = { Type: 'AWS::Lambda::Function', - DependsOn: [ - 'FuncLogGroup', - 'IamRoleLambdaExecution', - ], + DependsOn: ['FuncLogGroup', 'IamRoleLambdaExecution'], Properties: { Code: { S3Key: `${s3Folder}/${s3FileName}`, @@ -1338,7 +1352,7 @@ describe('AwsCompileFunctions', () => { Handler: 'func.function.handler', MemorySize: 1024, Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, - Runtime: 'nodejs4.3', + Runtime: 'nodejs10.x', Timeout: 6, TracingConfig: { Mode: 'Active', @@ -1347,8 +1361,8 @@ describe('AwsCompileFunctions', () => { }; return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { - const compiledCfTemplate = awsCompileFunctions.serverless.service.provider - .compiledCloudFormationTemplate; + const compiledCfTemplate = + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate; const functionResource = compiledCfTemplate.Resources.FuncLambdaFunction; expect(functionResource).to.deep.equal(compiledFunction); }); @@ -1375,10 +1389,7 @@ describe('AwsCompileFunctions', () => { const compiledFunction1 = { Type: 'AWS::Lambda::Function', - DependsOn: [ - 'Func1LogGroup', - 'IamRoleLambdaExecution', - ], + DependsOn: ['Func1LogGroup', 'IamRoleLambdaExecution'], Properties: { Code: { S3Key: `${s3Folder}/${s3FileName}`, @@ -1388,7 +1399,7 @@ describe('AwsCompileFunctions', () => { Handler: 'func1.function.handler', MemorySize: 1024, Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, - Runtime: 'nodejs4.3', + Runtime: 'nodejs10.x', Timeout: 6, TracingConfig: { Mode: 'Active', @@ -1398,10 +1409,7 @@ describe('AwsCompileFunctions', () => { const compiledFunction2 = { Type: 'AWS::Lambda::Function', - DependsOn: [ - 'Func2LogGroup', - 'IamRoleLambdaExecution', - ], + DependsOn: ['Func2LogGroup', 'IamRoleLambdaExecution'], Properties: { Code: { S3Key: `${s3Folder}/${s3FileName}`, @@ -1411,7 +1419,7 @@ describe('AwsCompileFunctions', () => { Handler: 'func2.function.handler', MemorySize: 1024, Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, - Runtime: 'nodejs4.3', + Runtime: 'nodejs10.x', Timeout: 6, TracingConfig: { Mode: 'PassThrough', @@ -1420,8 +1428,8 @@ describe('AwsCompileFunctions', () => { }; return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { - const compiledCfTemplate = awsCompileFunctions.serverless.service.provider - .compiledCloudFormationTemplate; + const compiledCfTemplate = + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate; const function1Resource = compiledCfTemplate.Resources.Func1LambdaFunction; const function2Resource = compiledCfTemplate.Resources.Func2LambdaFunction; @@ -1433,18 +1441,17 @@ describe('AwsCompileFunctions', () => { describe('when IamRoleLambdaExecution is used', () => { beforeEach(() => { // pretend that the IamRoleLambdaExecution is used - awsCompileFunctions.serverless.service.provider - .compiledCloudFormationTemplate.Resources.IamRoleLambdaExecution = { - Properties: { - Policies: [ - { - PolicyDocument: { - Statement: [], - }, + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources.IamRoleLambdaExecution = { + Properties: { + Policies: [ + { + PolicyDocument: { + Statement: [], }, - ], - }, - }; + }, + ], + }, + }; }); it('should create necessary resources if a tracing config is provided', () => { @@ -1458,10 +1465,7 @@ describe('AwsCompileFunctions', () => { const compiledFunction = { Type: 'AWS::Lambda::Function', - DependsOn: [ - 'FuncLogGroup', - 'IamRoleLambdaExecution', - ], + DependsOn: ['FuncLogGroup', 'IamRoleLambdaExecution'], Properties: { Code: { S3Key: `${s3Folder}/${s3FileName}`, @@ -1471,7 +1475,7 @@ describe('AwsCompileFunctions', () => { Handler: 'func.function.handler', MemorySize: 1024, Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, - Runtime: 'nodejs4.3', + Runtime: 'nodejs10.x', Timeout: 6, TracingConfig: { Mode: 'Active', @@ -1481,20 +1485,18 @@ describe('AwsCompileFunctions', () => { const compiledXrayStatement = { Effect: 'Allow', - Action: [ - 'xray:PutTraceSegments', - 'xray:PutTelemetryRecords', - ], + Action: ['xray:PutTraceSegments', 'xray:PutTelemetryRecords'], Resource: ['*'], }; return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { - const compiledCfTemplate = awsCompileFunctions.serverless.service.provider - .compiledCloudFormationTemplate; + const compiledCfTemplate = + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate; const functionResource = compiledCfTemplate.Resources.FuncLambdaFunction; - const xrayStatement = compiledCfTemplate.Resources - .IamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement[0]; + const xrayStatement = + compiledCfTemplate.Resources.IamRoleLambdaExecution.Properties.Policies[0] + .PolicyDocument.Statement[0]; expect(functionResource).to.deep.equal(compiledFunction); expect(xrayStatement).to.deep.equal(compiledXrayStatement); @@ -1514,10 +1516,7 @@ describe('AwsCompileFunctions', () => { const compiledFunction = { Type: 'AWS::Lambda::Function', - DependsOn: [ - 'FuncLogGroup', - 'IamRoleLambdaExecution', - ], + DependsOn: ['FuncLogGroup', 'IamRoleLambdaExecution'], Properties: { Code: { S3Key: `${s3Folder}/${s3FileName}`, @@ -1527,7 +1526,7 @@ describe('AwsCompileFunctions', () => { Handler: 'func.function.handler', MemorySize: 1024, Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, - Runtime: 'nodejs4.3', + Runtime: 'nodejs10.x', Timeout: 6, TracingConfig: { Mode: 'PassThrough', @@ -1536,8 +1535,8 @@ describe('AwsCompileFunctions', () => { }; return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { - const compiledCfTemplate = awsCompileFunctions.serverless.service.provider - .compiledCloudFormationTemplate; + const compiledCfTemplate = + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate; const functionResource = compiledCfTemplate.Resources.FuncLambdaFunction; @@ -1550,7 +1549,8 @@ describe('AwsCompileFunctions', () => { it('should create a function resource with environment config', () => { const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; const s3FileName = awsCompileFunctions.serverless.service.package.artifact - .split(path.sep).pop(); + .split(path.sep) + .pop(); awsCompileFunctions.serverless.service.functions = { func: { handler: 'func.function.handler', @@ -1568,10 +1568,7 @@ describe('AwsCompileFunctions', () => { const compiledFunction = { Type: 'AWS::Lambda::Function', - DependsOn: [ - 'FuncLogGroup', - 'IamRoleLambdaExecution', - ], + DependsOn: ['FuncLogGroup', 'IamRoleLambdaExecution'], Properties: { Code: { S3Key: `${s3Folder}/${s3FileName}`, @@ -1581,7 +1578,7 @@ describe('AwsCompileFunctions', () => { Handler: 'func.function.handler', MemorySize: 1024, Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, - Runtime: 'nodejs4.3', + Runtime: 'nodejs10.x', Timeout: 6, Environment: { Variables: { @@ -1593,11 +1590,10 @@ describe('AwsCompileFunctions', () => { }, }; - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { expect( - awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FuncLambdaFunction + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FuncLambdaFunction ).to.deep.equal(compiledFunction); }); }); @@ -1605,7 +1601,8 @@ describe('AwsCompileFunctions', () => { it('should create a function resource with function level environment config', () => { const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; const s3FileName = awsCompileFunctions.serverless.service.package.artifact - .split(path.sep).pop(); + .split(path.sep) + .pop(); awsCompileFunctions.serverless.service.functions = { func: { handler: 'func.function.handler', @@ -1618,10 +1615,7 @@ describe('AwsCompileFunctions', () => { const compiledFunction = { Type: 'AWS::Lambda::Function', - DependsOn: [ - 'FuncLogGroup', - 'IamRoleLambdaExecution', - ], + DependsOn: ['FuncLogGroup', 'IamRoleLambdaExecution'], Properties: { Code: { S3Key: `${s3Folder}/${s3FileName}`, @@ -1631,7 +1625,7 @@ describe('AwsCompileFunctions', () => { Handler: 'func.function.handler', MemorySize: 1024, Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, - Runtime: 'nodejs4.3', + Runtime: 'nodejs10.x', Timeout: 6, Environment: { Variables: { @@ -1641,11 +1635,10 @@ describe('AwsCompileFunctions', () => { }, }; - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { expect( - awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FuncLambdaFunction + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FuncLambdaFunction ).to.deep.equal(compiledFunction); }); }); @@ -1653,7 +1646,8 @@ describe('AwsCompileFunctions', () => { it('should create a function resource with provider level environment config', () => { const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; const s3FileName = awsCompileFunctions.serverless.service.package.artifact - .split(path.sep).pop(); + .split(path.sep) + .pop(); awsCompileFunctions.serverless.service.functions = { func: { handler: 'func.function.handler', @@ -1667,10 +1661,7 @@ describe('AwsCompileFunctions', () => { const compiledFunction = { Type: 'AWS::Lambda::Function', - DependsOn: [ - 'FuncLogGroup', - 'IamRoleLambdaExecution', - ], + DependsOn: ['FuncLogGroup', 'IamRoleLambdaExecution'], Properties: { Code: { S3Key: `${s3Folder}/${s3FileName}`, @@ -1680,7 +1671,7 @@ describe('AwsCompileFunctions', () => { Handler: 'func.function.handler', MemorySize: 1024, Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, - Runtime: 'nodejs4.3', + Runtime: 'nodejs10.x', Timeout: 6, Environment: { Variables: { @@ -1690,11 +1681,10 @@ describe('AwsCompileFunctions', () => { }, }; - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { expect( - awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FuncLambdaFunction + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FuncLambdaFunction ).to.deep.equal(compiledFunction); }); }); @@ -1702,7 +1692,8 @@ describe('AwsCompileFunctions', () => { it('should overwrite a provider level environment config when function config is given', () => { const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; const s3FileName = awsCompileFunctions.serverless.service.package.artifact - .split(path.sep).pop(); + .split(path.sep) + .pop(); awsCompileFunctions.serverless.service.provider.environment = { variable: 'overwrite-me', }; @@ -1719,10 +1710,7 @@ describe('AwsCompileFunctions', () => { const compiledFunction = { Type: 'AWS::Lambda::Function', - DependsOn: [ - 'FuncLogGroup', - 'IamRoleLambdaExecution', - ], + DependsOn: ['FuncLogGroup', 'IamRoleLambdaExecution'], Properties: { Code: { S3Key: `${s3Folder}/${s3FileName}`, @@ -1732,7 +1720,7 @@ describe('AwsCompileFunctions', () => { Handler: 'func.function.handler', MemorySize: 1024, Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, - Runtime: 'nodejs4.3', + Runtime: 'nodejs10.x', Timeout: 6, Environment: { Variables: { @@ -1742,11 +1730,10 @@ describe('AwsCompileFunctions', () => { }, }; - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { expect( - awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FuncLambdaFunction + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FuncLambdaFunction ).to.deep.equal(compiledFunction); }); }); @@ -1758,7 +1745,7 @@ describe('AwsCompileFunctions', () => { name: 'new-service-dev-func', environment: { '1test1': 'test1', - test2: 'test2', + 'test2': 'test2', }, }, }; @@ -1777,11 +1764,10 @@ describe('AwsCompileFunctions', () => { }, }; - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { - expect(awsCompileFunctions.serverless.service.provider - .compiledCloudFormationTemplate - .Resources.FuncLambdaFunction.Properties.Environment.Variables.counter + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FuncLambdaFunction.Properties.Environment.Variables.counter ).to.equal(18); }); }); @@ -1802,27 +1788,27 @@ describe('AwsCompileFunctions', () => { }, }; - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { - awsCompileFunctions.serverless.service.functions = { - func: { - handler: 'func.function.handler', - name: 'new-service-dev-func', - environment: { - counter: { - NotRef: 'TestVariable', - }, + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + environment: { + counter: { + NotRef: 'TestVariable', }, }, - }; - return expect(awsCompileFunctions.compileFunctions()).to.be.rejectedWith(Error); - }); + }, + }; + return expect(awsCompileFunctions.compileFunctions()).to.be.rejectedWith(Error); + }); }); it('should consider function based config when creating a function resource', () => { const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; const s3FileName = awsCompileFunctions.serverless.service.package.artifact - .split(path.sep).pop(); + .split(path.sep) + .pop(); awsCompileFunctions.serverless.service.functions = { func: { name: 'customized-func-function', @@ -1833,10 +1819,7 @@ describe('AwsCompileFunctions', () => { }; const compiledFunction = { Type: 'AWS::Lambda::Function', - DependsOn: [ - 'FuncLogGroup', - 'IamRoleLambdaExecution', - ], + DependsOn: ['FuncLogGroup', 'IamRoleLambdaExecution'], Properties: { Code: { S3Key: `${s3Folder}/${s3FileName}`, @@ -1846,66 +1829,66 @@ describe('AwsCompileFunctions', () => { Handler: 'func.function.handler', MemorySize: 128, Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, - Runtime: 'nodejs4.3', + Runtime: 'nodejs10.x', Timeout: 10, }, }; - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { expect( - awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FuncLambdaFunction + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FuncLambdaFunction ).to.deep.equal(compiledFunction); }); }); - it('should allow functions to use a different runtime' + - ' than the service default runtime if specified', () => { - const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; - const s3FileName = awsCompileFunctions.serverless.service.package.artifact - .split(path.sep).pop(); - awsCompileFunctions.serverless.service.functions = { - func: { - handler: 'func.function.handler', - name: 'new-service-dev-func', - runtime: 'python2.7', - }, - }; - - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { - const compiledFunction = { - Type: 'AWS::Lambda::Function', - DependsOn: [ - 'FuncLogGroup', - 'IamRoleLambdaExecution', - ], - Properties: { - Code: { - S3Key: `${s3Folder}/${s3FileName}`, - S3Bucket: { Ref: 'ServerlessDeploymentBucket' }, - }, - FunctionName: 'new-service-dev-func', - Handler: 'func.function.handler', - MemorySize: 1024, - Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, - Runtime: 'python2.7', - Timeout: 6, + it( + 'should allow functions to use a different runtime' + + ' than the service default runtime if specified', + () => { + const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; + const s3FileName = awsCompileFunctions.serverless.service.package.artifact + .split(path.sep) + .pop(); + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + runtime: 'python2.7', }, }; - expect( - awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FuncLambdaFunction - ).to.deep.equal(compiledFunction); - }); - }); + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { + const compiledFunction = { + Type: 'AWS::Lambda::Function', + DependsOn: ['FuncLogGroup', 'IamRoleLambdaExecution'], + Properties: { + Code: { + S3Key: `${s3Folder}/${s3FileName}`, + S3Bucket: { Ref: 'ServerlessDeploymentBucket' }, + }, + FunctionName: 'new-service-dev-func', + Handler: 'func.function.handler', + MemorySize: 1024, + Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, + Runtime: 'python2.7', + Timeout: 6, + }, + }; - it('should default to the nodejs4.3 runtime when no provider runtime is given', () => { + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FuncLambdaFunction + ).to.deep.equal(compiledFunction); + }); + } + ); + + it('should default to the nodejs10.x runtime when no provider runtime is given', () => { const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; const s3FileName = awsCompileFunctions.serverless.service.package.artifact - .split(path.sep).pop(); + .split(path.sep) + .pop(); awsCompileFunctions.serverless.service.provider.runtime = null; awsCompileFunctions.serverless.service.functions = { func: { @@ -1915,10 +1898,7 @@ describe('AwsCompileFunctions', () => { }; const compiledFunction = { Type: 'AWS::Lambda::Function', - DependsOn: [ - 'FuncLogGroup', - 'IamRoleLambdaExecution', - ], + DependsOn: ['FuncLogGroup', 'IamRoleLambdaExecution'], Properties: { Code: { S3Key: `${s3Folder}/${s3FileName}`, @@ -1928,25 +1908,24 @@ describe('AwsCompileFunctions', () => { Handler: 'func.function.handler', MemorySize: 1024, Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, - Runtime: 'nodejs4.3', + Runtime: 'nodejs10.x', Timeout: 6, }, }; - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { expect( - awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FuncLambdaFunction + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FuncLambdaFunction ).to.deep.equal(compiledFunction); }); }); - it('should consider the providers runtime and memorySize ' + - 'when creating a function resource', () => { + it('should consider the providers runtime and memorySize when creating a function resource', () => { const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; const s3FileName = awsCompileFunctions.serverless.service.package.artifact - .split(path.sep).pop(); + .split(path.sep) + .pop(); awsCompileFunctions.serverless.service.provider.runtime = 'python2.7'; awsCompileFunctions.serverless.service.provider.memorySize = 128; awsCompileFunctions.serverless.service.functions = { @@ -1957,10 +1936,7 @@ describe('AwsCompileFunctions', () => { }; const compiledFunction = { Type: 'AWS::Lambda::Function', - DependsOn: [ - 'FuncLogGroup', - 'IamRoleLambdaExecution', - ], + DependsOn: ['FuncLogGroup', 'IamRoleLambdaExecution'], Properties: { Code: { S3Key: `${s3Folder}/${s3FileName}`, @@ -1975,11 +1951,10 @@ describe('AwsCompileFunctions', () => { }, }; - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { expect( - awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FuncLambdaFunction + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FuncLambdaFunction ).to.deep.equal(compiledFunction); }); }); @@ -1987,7 +1962,8 @@ describe('AwsCompileFunctions', () => { it('should use a custom bucket if specified', () => { const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; const s3FileName = awsCompileFunctions.serverless.service.package.artifact - .split(path.sep).pop(); + .split(path.sep) + .pop(); const bucketName = 'com.serverless.deploys'; awsCompileFunctions.serverless.service.package.deploymentBucket = bucketName; @@ -2001,10 +1977,7 @@ describe('AwsCompileFunctions', () => { }; const compiledFunction = { Type: 'AWS::Lambda::Function', - DependsOn: [ - 'FuncLogGroup', - 'IamRoleLambdaExecution', - ], + DependsOn: ['FuncLogGroup', 'IamRoleLambdaExecution'], Properties: { Code: { S3Key: `${s3Folder}/${s3FileName}`, @@ -2019,22 +1992,14 @@ describe('AwsCompileFunctions', () => { }, }; const coreCloudFormationTemplate = awsCompileFunctions.serverless.utils.readFileSync( - path.join( - __dirname, - '..', - '..', - 'lib', - 'core-cloudformation-template.json' - ) + path.join(__dirname, '..', '..', 'lib', 'core-cloudformation-template.json') ); - awsCompileFunctions.serverless.service.provider - .compiledCloudFormationTemplate = coreCloudFormationTemplate; + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate = coreCloudFormationTemplate; - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { expect( - awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FuncLambdaFunction + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FuncLambdaFunction ).to.deep.equal(compiledFunction); }); }); @@ -2047,11 +2012,10 @@ describe('AwsCompileFunctions', () => { }, }; - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { expect( - awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FuncLambdaFunction.Properties.Description + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FuncLambdaFunction.Properties.Description ).to.equal('Lambda function description'); }); }); @@ -2069,24 +2033,20 @@ describe('AwsCompileFunctions', () => { const expectedOutputs = { FuncLambdaFunctionQualifiedArn: { Description: 'Current Lambda function version', - Value: { Ref: 'FuncLambdaVersionl6Rjpaz0gycgsEDI51sLed039fH2uR4W8Q2IW8cNo' }, + Value: { Ref: 'FuncLambdaVersionpcyXz9PqN5xesfBZOOhY7t6jhi8kOCyGDknpfuhJ4' }, }, AnotherFuncLambdaFunctionQualifiedArn: { Description: 'Current Lambda function version', Value: { - Ref: 'AnotherFuncLambdaVersion6JZQneYqP4bC0Z3ywMc3XJPyECHK4RMGhpv8iis4E', + Ref: 'AnotherFuncLambdaVersionIo6IWr3BPLeAaVjlRwEGEz4vvDzC43h07eOBY0fXyI', }, }, }; - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { expect( - awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Outputs - ).to.deep.equal( - expectedOutputs - ); + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Outputs + ).to.deep.equal(expectedOutputs); }); }); @@ -2103,57 +2063,47 @@ describe('AwsCompileFunctions', () => { const expectedOutputs = { FuncLambdaFunctionQualifiedArn: { Description: 'Current Lambda function version', - Value: { Ref: 'FuncLambdaVersionl6Rjpaz0gycgsEDI51sLed039fH2uR4W8Q2IW8cNo' }, + Value: { Ref: 'FuncLambdaVersionpcyXz9PqN5xesfBZOOhY7t6jhi8kOCyGDknpfuhJ4' }, }, AnotherFuncLambdaFunctionQualifiedArn: { Description: 'Current Lambda function version', Value: { - Ref: 'AnotherFuncLambdaVersion6JZQneYqP4bC0Z3ywMc3XJPyECHK4RMGhpv8iis4E', + Ref: 'AnotherFuncLambdaVersionIo6IWr3BPLeAaVjlRwEGEz4vvDzC43h07eOBY0fXyI', }, }, }; - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { - expect( - awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Outputs - ).to.deep.equal( - expectedOutputs - ); + return expect(awsCompileFunctions.compileFunctions()) + .to.be.fulfilled.then(() => { + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Outputs + ).to.deep.equal(expectedOutputs); - // Change configuration - awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate = { - Resources: {}, - Outputs: {}, - }; + // Change configuration + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate = { + Resources: {}, + Outputs: {}, + }; - _.set( - awsCompileFunctions, - 'serverless.service.functions.func.environment.MY_ENV_VAR', - 'myvalue' - ); + _.set( + awsCompileFunctions, + 'serverless.service.functions.func.environment.MY_ENV_VAR', + 'myvalue' + ); - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled; - }) - .then(() => { - // Expect different version hash - _.set( - expectedOutputs, - 'FuncLambdaFunctionQualifiedArn', - { + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled; + }) + .then(() => { + // Expect different version hash + _.set(expectedOutputs, 'FuncLambdaFunctionQualifiedArn', { Description: 'Current Lambda function version', - Value: { Ref: 'FuncLambdaVersiona6VymfU25aF6eS2qysm7sHqPyy8RqYUzoTvDeBrrBA' }, - } - ); + Value: { Ref: 'FuncLambdaVersionI1xWetHMVQO8bvzGqgmokPl25rtJA0A8g6lZNYdkdg' }, + }); - expect( - awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Outputs - ).to.deep.equal( - expectedOutputs - ); - }); + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Outputs + ).to.deep.equal(expectedOutputs); + }); }); it('should include description under version too if function is specified', () => { @@ -2164,12 +2114,10 @@ describe('AwsCompileFunctions', () => { }, }; - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { expect( - awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FuncLambdaVersionOKy3yjVllZnozzdvQqHlRN8lBwkZyA6l76TCAEyork - .Properties.Description + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FuncLambdaVersionBzAYHivcbYLoEZcl7hN9cBrakBNygN0PiUC9UjQVMA.Properties.Description ).to.equal('Lambda function description'); }); }); @@ -2187,21 +2135,18 @@ describe('AwsCompileFunctions', () => { const expectedOutputs = {}; - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { expect( - awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Outputs - ).to.deep.equal( - expectedOutputs - ); + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Outputs + ).to.deep.equal(expectedOutputs); }); }); it('should set function declared reserved concurrency limit', () => { const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; const s3FileName = awsCompileFunctions.serverless.service.package.artifact - .split(path.sep).pop(); + .split(path.sep) + .pop(); awsCompileFunctions.serverless.service.functions = { func: { handler: 'func.function.handler', @@ -2211,10 +2156,7 @@ describe('AwsCompileFunctions', () => { }; const compiledFunction = { Type: 'AWS::Lambda::Function', - DependsOn: [ - 'FuncLogGroup', - 'IamRoleLambdaExecution', - ], + DependsOn: ['FuncLogGroup', 'IamRoleLambdaExecution'], Properties: { Code: { S3Key: `${s3Folder}/${s3FileName}`, @@ -2225,24 +2167,24 @@ describe('AwsCompileFunctions', () => { MemorySize: 1024, ReservedConcurrentExecutions: 5, Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, - Runtime: 'nodejs4.3', + Runtime: 'nodejs10.x', Timeout: 6, }, }; - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { - expect( - awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FuncLambdaFunction - ).to.deep.equal(compiledFunction); - }); + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FuncLambdaFunction + ).to.deep.equal(compiledFunction); + }); }); it('should set function declared reserved concurrency limit even if it is zero', () => { const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; const s3FileName = awsCompileFunctions.serverless.service.package.artifact - .split(path.sep).pop(); + .split(path.sep) + .pop(); awsCompileFunctions.serverless.service.functions = { func: { handler: 'func.function.handler', @@ -2252,10 +2194,7 @@ describe('AwsCompileFunctions', () => { }; const compiledFunction = { Type: 'AWS::Lambda::Function', - DependsOn: [ - 'FuncLogGroup', - 'IamRoleLambdaExecution', - ], + DependsOn: ['FuncLogGroup', 'IamRoleLambdaExecution'], Properties: { Code: { S3Key: `${s3Folder}/${s3FileName}`, @@ -2266,37 +2205,39 @@ describe('AwsCompileFunctions', () => { MemorySize: 1024, ReservedConcurrentExecutions: 0, Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, - Runtime: 'nodejs4.3', + Runtime: 'nodejs10.x', Timeout: 6, }, }; - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { - expect( - awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FuncLambdaFunction - ).to.deep.equal(compiledFunction); - }); + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FuncLambdaFunction + ).to.deep.equal(compiledFunction); + }); }); - it('should throw an informative error message if non-integer reserved concurrency limit set ' + - 'on function', () => { - awsCompileFunctions.serverless.service.functions = { - func: { - handler: 'func.function.handler', - name: 'new-service-dev-func', - reservedConcurrency: 'a', - }, - }; + it( + 'should throw an informative error message if non-integer reserved concurrency limit set ' + + 'on function', + () => { + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + reservedConcurrency: 'a', + }, + }; - const errorMessage = [ - 'You should use integer as reservedConcurrency value on function: ', - 'new-service-dev-func', - ].join(''); + const errorMessage = [ + 'You should use integer as reservedConcurrency value on function: ', + 'new-service-dev-func', + ].join(''); - return expect(awsCompileFunctions.compileFunctions()).to.be.rejectedWith(errorMessage); - }); + return expect(awsCompileFunctions.compileFunctions()).to.be.rejectedWith(errorMessage); + } + ); }); describe('#compileRole()', () => { @@ -2306,15 +2247,10 @@ describe('AwsCompileFunctions', () => { awsCompileFunctions.compileRole(resource, role); expect(resource).to.deep.equal({ - DependsOn: [ - role, - ], + DependsOn: [role], Properties: { Role: { - 'Fn::GetAtt': [ - role, - 'Arn', - ], + 'Fn::GetAtt': [role, 'Arn'], }, }, }); @@ -2326,15 +2262,10 @@ describe('AwsCompileFunctions', () => { awsCompileFunctions.compileRole(resource, role); expect(resource).to.deep.equal({ - DependsOn: [ - role, - ], + DependsOn: [role], Properties: { Role: { - 'Fn::GetAtt': [ - role, - 'Arn', - ], + 'Fn::GetAtt': [role, 'Arn'], }, }, }); @@ -2346,9 +2277,7 @@ describe('AwsCompileFunctions', () => { awsCompileFunctions.compileRole(resource, role); expect(resource).to.deep.equal({ - DependsOn: [ - 'Foo', - ], + DependsOn: ['Foo'], Properties: { Role: role, }, @@ -2384,34 +2313,29 @@ describe('AwsCompileFunctions', () => { const role = { Ref: 'Foo' }; const resource = { Properties: {} }; - expect(() => - awsCompileFunctions.compileRole(resource, role) - ).to.throw(Error); + expect(() => awsCompileFunctions.compileRole(resource, role)).to.throw(Error); }); it('should throw for object type Buffer', () => { const role = new Buffer('Foo'); const resource = { Properties: {} }; - expect(() => - awsCompileFunctions.compileRole(resource, role) - ).to.throw(Error); + expect(() => awsCompileFunctions.compileRole(resource, role)).to.throw(Error); }); it('should throw for object type Array', () => { const role = [1, 2, 3]; const resource = { Properties: {} }; - expect(() => - awsCompileFunctions.compileRole(resource, role) - ).to.throw(Error); + expect(() => awsCompileFunctions.compileRole(resource, role)).to.throw(Error); }); }); it('should not set unset properties when not specified in yml (layers, vpc, etc)', () => { const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; const s3FileName = awsCompileFunctions.serverless.service.package.artifact - .split(path.sep).pop(); + .split(path.sep) + .pop(); awsCompileFunctions.serverless.service.functions = { func: { @@ -2421,10 +2345,7 @@ describe('AwsCompileFunctions', () => { }; const compiledFunction = { Type: 'AWS::Lambda::Function', - DependsOn: [ - 'FuncLogGroup', - 'IamRoleLambdaExecution', - ], + DependsOn: ['FuncLogGroup', 'IamRoleLambdaExecution'], Properties: { Code: { S3Key: `${s3Folder}/${s3FileName}`, @@ -2434,16 +2355,15 @@ describe('AwsCompileFunctions', () => { Handler: 'func.function.handler', MemorySize: 1024, Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, - Runtime: 'nodejs4.3', + Runtime: 'nodejs10.x', Timeout: 6, }, }; - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { expect( - awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FuncLambdaFunction + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FuncLambdaFunction ).to.deep.equal(compiledFunction); }); }); @@ -2451,7 +2371,8 @@ describe('AwsCompileFunctions', () => { it('should set Layers when specified', () => { const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; const s3FileName = awsCompileFunctions.serverless.service.package.artifact - .split(path.sep).pop(); + .split(path.sep) + .pop(); awsCompileFunctions.serverless.service.functions = { func: { @@ -2462,10 +2383,7 @@ describe('AwsCompileFunctions', () => { }; const compiledFunction = { Type: 'AWS::Lambda::Function', - DependsOn: [ - 'FuncLogGroup', - 'IamRoleLambdaExecution', - ], + DependsOn: ['FuncLogGroup', 'IamRoleLambdaExecution'], Properties: { Code: { S3Key: `${s3Folder}/${s3FileName}`, @@ -2475,17 +2393,16 @@ describe('AwsCompileFunctions', () => { Handler: 'func.function.handler', MemorySize: 1024, Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, - Runtime: 'nodejs4.3', + Runtime: 'nodejs10.x', Timeout: 6, Layers: ['arn:aws:xxx:*:*'], }, }; - return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled - .then(() => { + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { expect( - awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FuncLambdaFunction + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FuncLambdaFunction ).to.deep.equal(compiledFunction); }); }); diff --git a/lib/plugins/aws/package/compile/layers/index.js b/lib/plugins/aws/package/compile/layers/index.js index d79dd29cc..b75e5ec88 100644 --- a/lib/plugins/aws/package/compile/layers/index.js +++ b/lib/plugins/aws/package/compile/layers/index.js @@ -10,14 +10,13 @@ class AwsCompileLayers { this.serverless = serverless; this.options = options; const servicePath = this.serverless.config.servicePath || ''; - this.packagePath = this.serverless.service.package.path || - path.join(servicePath || '.', '.serverless'); + this.packagePath = + this.serverless.service.package.path || path.join(servicePath || '.', '.serverless'); this.provider = this.serverless.getProvider('aws'); this.hooks = { - 'package:compileLayers': () => BbPromise.bind(this) - .then(this.compileLayers), + 'package:compileLayers': () => BbPromise.bind(this).then(this.compileLayers), }; } @@ -27,9 +26,10 @@ class AwsCompileLayers { layerObject.package = layerObject.package || {}; const artifactFileName = this.provider.naming.getLayerArtifactName(layerName); - const artifactFilePath = layerObject.package && layerObject.package.artifact - ? layerObject.package.artifact - : path.join(this.serverless.config.servicePath, '.serverless', artifactFileName); + const artifactFilePath = + layerObject.package && layerObject.package.artifact + ? layerObject.package.artifact + : path.join(this.serverless.config.servicePath, '.serverless', artifactFileName); if (this.serverless.service.package.deploymentBucket) { newLayer.Properties.Content.S3Bucket = this.serverless.service.package.deploymentBucket; @@ -52,7 +52,10 @@ class AwsCompileLayers { let layerLogicalId = this.provider.naming.getLambdaLayerLogicalId(layerName); if (layerObject.retain) { - const sha = crypto.createHash('sha1').update(JSON.stringify(newLayer)).digest('hex'); + const sha = crypto + .createHash('sha1') + .update(JSON.stringify(newLayer)) + .digest('hex'); layerLogicalId = `${layerLogicalId}${sha}`; newLayer.DeletionPolicy = 'Retain'; } @@ -71,18 +74,21 @@ class AwsCompileLayers { newPermission.Properties.LayerVersionArn = { Ref: layerLogicalId }; newPermission.Properties.Principal = parsedAccount; const layerPermLogicalId = this.provider.naming.getLambdaLayerPermissionLogicalId( - layerName, parsedAccount); + layerName, + parsedAccount + ); newLayerObject[layerPermLogicalId] = newPermission; return newPermission; }); } - _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, - newLayerObject); + _.merge( + this.serverless.service.provider.compiledCloudFormationTemplate.Resources, + newLayerObject + ); // Add layer to Outputs section - const layerOutputLogicalId = this.provider.naming - .getLambdaLayerOutputLogicalId(layerName); + const layerOutputLogicalId = this.provider.naming.getLambdaLayerOutputLogicalId(layerName); const newLayerOutput = this.cfOutputLayerTemplate(); newLayerOutput.Value = { Ref: layerLogicalId }; @@ -94,10 +100,7 @@ class AwsCompileLayers { compileLayers() { const allLayers = this.serverless.service.getAllLayers(); - return BbPromise.each( - allLayers, - layerName => this.compileLayer(layerName) - ); + return BbPromise.each(allLayers, layerName => this.compileLayer(layerName)); } cfLambdaLayerTemplate() { diff --git a/lib/plugins/aws/package/compile/layers/index.test.js b/lib/plugins/aws/package/compile/layers/index.test.js index c9ec66124..dfadb7011 100644 --- a/lib/plugins/aws/package/compile/layers/index.test.js +++ b/lib/plugins/aws/package/compile/layers/index.test.js @@ -5,8 +5,8 @@ const path = require('path'); const chai = require('chai'); const AwsProvider = require('../../../provider/awsProvider'); const AwsCompileLayers = require('./index'); -const testUtils = require('../../../../../../tests/utils'); const Serverless = require('../../../../../Serverless'); +const { getTmpDirPath } = require('../../../../../../tests/utils/fs'); chai.use(require('chai-as-promised')); @@ -35,23 +35,28 @@ describe('AwsCompileLayers', () => { const serviceArtifact = 'new-service.zip'; const individualArtifact = 'test.zip'; - awsCompileLayers.packagePath = testUtils.getTmpDirPath(); + awsCompileLayers.packagePath = getTmpDirPath(); // The contents of the test artifacts need to be predictable so the hashes stay the same - serverless.utils.writeFileSync(path.join(awsCompileLayers.packagePath, - serviceArtifact), 'foobar'); - serverless.utils.writeFileSync(path.join(awsCompileLayers.packagePath, - individualArtifact), 'barbaz'); + serverless.utils.writeFileSync( + path.join(awsCompileLayers.packagePath, serviceArtifact), + 'foobar' + ); + serverless.utils.writeFileSync( + path.join(awsCompileLayers.packagePath, individualArtifact), + 'barbaz' + ); awsCompileLayers.serverless.service.service = 'new-service'; awsCompileLayers.serverless.service.package.artifactDirectoryName = 'somedir'; - awsCompileLayers.serverless.service.package - .artifact = path.join(awsCompileLayers.packagePath, serviceArtifact); + awsCompileLayers.serverless.service.package.artifact = path.join( + awsCompileLayers.packagePath, + serviceArtifact + ); awsCompileLayers.serverless.service.layers = {}; awsCompileLayers.serverless.service.layers[layerName] = { name: 'test', package: { - artifact: path.join(awsCompileLayers.packagePath, - individualArtifact), + artifact: path.join(awsCompileLayers.packagePath, individualArtifact), }, handler: 'handler.hello', }; @@ -66,24 +71,26 @@ describe('AwsCompileLayers', () => { it('should use layer artifact if individually', () => { awsCompileLayers.serverless.service.package.individually = true; - return expect(awsCompileLayers.compileLayers()).to.be.fulfilled - .then(() => { - const layerResource = awsCompileLayers.serverless.service.provider - .compiledCloudFormationTemplate.Resources[compiledLayerName]; + return expect(awsCompileLayers.compileLayers()).to.be.fulfilled.then(() => { + const layerResource = + awsCompileLayers.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + compiledLayerName + ]; const s3Folder = awsCompileLayers.serverless.service.package.artifactDirectoryName; - const s3FileName = awsCompileLayers.serverless.service - .layers[layerName].package.artifact.split(path.sep).pop(); + const s3FileName = awsCompileLayers.serverless.service.layers[layerName].package.artifact + .split(path.sep) + .pop(); - expect(layerResource.Properties.Content.S3Key) - .to.deep.equal(`${s3Folder}/${s3FileName}`); + expect(layerResource.Properties.Content.S3Key).to.deep.equal(`${s3Folder}/${s3FileName}`); }); }); it('should create a simple layer resource', () => { const s3Folder = awsCompileLayers.serverless.service.package.artifactDirectoryName; const s3FileName = awsCompileLayers.serverless.service.layers.test.package.artifact - .split(path.sep).pop(); + .split(path.sep) + .pop(); awsCompileLayers.serverless.service.layers = { test: { path: 'layer', @@ -106,15 +113,14 @@ describe('AwsCompileLayers', () => { }, }; - return expect(awsCompileLayers.compileLayers()).to.be.fulfilled - .then(() => { + return expect(awsCompileLayers.compileLayers()).to.be.fulfilled.then(() => { expect( - awsCompileLayers.serverless.service.provider.compiledCloudFormationTemplate - .Resources.TestLambdaLayer + awsCompileLayers.serverless.service.provider.compiledCloudFormationTemplate.Resources + .TestLambdaLayer ).to.deep.equal(compiledLayer); expect( - awsCompileLayers.serverless.service.provider.compiledCloudFormationTemplate - .Outputs.TestLambdaLayerQualifiedArn + awsCompileLayers.serverless.service.provider.compiledCloudFormationTemplate.Outputs + .TestLambdaLayerQualifiedArn ).to.deep.equal(compiledLayerOutput); }); }); @@ -122,7 +128,8 @@ describe('AwsCompileLayers', () => { it('should create a layer resource with a retention policy', () => { const s3Folder = awsCompileLayers.serverless.service.package.artifactDirectoryName; const s3FileName = awsCompileLayers.serverless.service.layers.test.package.artifact - .split(path.sep).pop(); + .split(path.sep) + .pop(); awsCompileLayers.serverless.service.layers = { test: { path: 'layer', @@ -139,7 +146,10 @@ describe('AwsCompileLayers', () => { LayerName: 'test', }, }; - const sha = crypto.createHash('sha1').update(JSON.stringify(compiledLayer)).digest('hex'); + const sha = crypto + .createHash('sha1') + .update(JSON.stringify(compiledLayer)) + .digest('hex'); compiledLayer.DeletionPolicy = 'Retain'; const compiledLayerOutput = { Description: 'Current Lambda layer version', @@ -148,15 +158,15 @@ describe('AwsCompileLayers', () => { }, }; - return expect(awsCompileLayers.compileLayers()).to.be.fulfilled - .then(() => { + return expect(awsCompileLayers.compileLayers()).to.be.fulfilled.then(() => { expect( - awsCompileLayers.serverless.service.provider.compiledCloudFormationTemplate - .Resources[`TestLambdaLayer${sha}`] + awsCompileLayers.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + `TestLambdaLayer${sha}` + ] ).to.deep.equal(compiledLayer); expect( - awsCompileLayers.serverless.service.provider.compiledCloudFormationTemplate - .Outputs.TestLambdaLayerQualifiedArn + awsCompileLayers.serverless.service.provider.compiledCloudFormationTemplate.Outputs + .TestLambdaLayerQualifiedArn ).to.deep.equal(compiledLayerOutput); }); }); @@ -164,7 +174,8 @@ describe('AwsCompileLayers', () => { it('should create a layer resource with permissions', () => { const s3Folder = awsCompileLayers.serverless.service.package.artifactDirectoryName; const s3FileName = awsCompileLayers.serverless.service.layers.test.package.artifact - .split(path.sep).pop(); + .split(path.sep) + .pop(); awsCompileLayers.serverless.service.layers = { test: { path: 'layer', @@ -198,19 +209,18 @@ describe('AwsCompileLayers', () => { }, }; - return expect(awsCompileLayers.compileLayers()).to.be.fulfilled - .then(() => { + return expect(awsCompileLayers.compileLayers()).to.be.fulfilled.then(() => { expect( - awsCompileLayers.serverless.service.provider.compiledCloudFormationTemplate - .Resources.TestLambdaLayer + awsCompileLayers.serverless.service.provider.compiledCloudFormationTemplate.Resources + .TestLambdaLayer ).to.deep.equal(compiledLayer); expect( - awsCompileLayers.serverless.service.provider.compiledCloudFormationTemplate - .Outputs.TestLambdaLayerQualifiedArn + awsCompileLayers.serverless.service.provider.compiledCloudFormationTemplate.Outputs + .TestLambdaLayerQualifiedArn ).to.deep.equal(compiledLayerOutput); expect( - awsCompileLayers.serverless.service.provider.compiledCloudFormationTemplate - .Resources.TestWildLambdaLayerPermission + awsCompileLayers.serverless.service.provider.compiledCloudFormationTemplate.Resources + .TestWildLambdaLayerPermission ).to.deep.equal(compiledLayerVersion); }); }); @@ -218,7 +228,8 @@ describe('AwsCompileLayers', () => { it('should create a layer resource with permissions per account', () => { const s3Folder = awsCompileLayers.serverless.service.package.artifactDirectoryName; const s3FileName = awsCompileLayers.serverless.service.layers.test.package.artifact - .split(path.sep).pop(); + .split(path.sep) + .pop(); awsCompileLayers.serverless.service.layers = { test: { path: 'layer', @@ -263,23 +274,22 @@ describe('AwsCompileLayers', () => { }, }; - return expect(awsCompileLayers.compileLayers()).to.be.fulfilled - .then(() => { + return expect(awsCompileLayers.compileLayers()).to.be.fulfilled.then(() => { expect( - awsCompileLayers.serverless.service.provider.compiledCloudFormationTemplate - .Resources.TestLambdaLayer + awsCompileLayers.serverless.service.provider.compiledCloudFormationTemplate.Resources + .TestLambdaLayer ).to.deep.equal(compiledLayer); expect( - awsCompileLayers.serverless.service.provider.compiledCloudFormationTemplate - .Outputs.TestLambdaLayerQualifiedArn + awsCompileLayers.serverless.service.provider.compiledCloudFormationTemplate.Outputs + .TestLambdaLayerQualifiedArn ).to.deep.equal(compiledLayerOutput); expect( - awsCompileLayers.serverless.service.provider.compiledCloudFormationTemplate - .Resources.Test1111111LambdaLayerPermission + awsCompileLayers.serverless.service.provider.compiledCloudFormationTemplate.Resources + .Test1111111LambdaLayerPermission ).to.deep.equal(compiledLayerVersionNumber); expect( - awsCompileLayers.serverless.service.provider.compiledCloudFormationTemplate - .Resources.Test2222222LambdaLayerPermission + awsCompileLayers.serverless.service.provider.compiledCloudFormationTemplate.Resources + .Test2222222LambdaLayerPermission ).to.deep.equal(compiledLayerVersionString); }); }); @@ -287,12 +297,13 @@ describe('AwsCompileLayers', () => { it('should create a layer resource with metadata options set', () => { const s3Folder = awsCompileLayers.serverless.service.package.artifactDirectoryName; const s3FileName = awsCompileLayers.serverless.service.layers.test.package.artifact - .split(path.sep).pop(); + .split(path.sep) + .pop(); awsCompileLayers.serverless.service.layers = { test: { path: 'layer', description: 'desc', - compatibleRuntimes: ['nodejs8.10'], + compatibleRuntimes: ['nodejs10.x'], licenseInfo: 'GPL', }, }; @@ -305,7 +316,7 @@ describe('AwsCompileLayers', () => { }, LayerName: 'test', Description: 'desc', - CompatibleRuntimes: ['nodejs8.10'], + CompatibleRuntimes: ['nodejs10.x'], LicenseInfo: 'GPL', }, }; @@ -316,15 +327,14 @@ describe('AwsCompileLayers', () => { }, }; - return expect(awsCompileLayers.compileLayers()).to.be.fulfilled - .then(() => { + return expect(awsCompileLayers.compileLayers()).to.be.fulfilled.then(() => { expect( - awsCompileLayers.serverless.service.provider.compiledCloudFormationTemplate - .Resources.TestLambdaLayer + awsCompileLayers.serverless.service.provider.compiledCloudFormationTemplate.Resources + .TestLambdaLayer ).to.deep.equal(compiledLayer); expect( - awsCompileLayers.serverless.service.provider.compiledCloudFormationTemplate - .Outputs.TestLambdaLayerQualifiedArn + awsCompileLayers.serverless.service.provider.compiledCloudFormationTemplate.Outputs + .TestLambdaLayerQualifiedArn ).to.deep.equal(compiledLayerOutput); }); }); diff --git a/lib/plugins/aws/package/index.js b/lib/plugins/aws/package/index.js index b461fdb2c..5ed9b0e5d 100644 --- a/lib/plugins/aws/package/index.js +++ b/lib/plugins/aws/package/index.js @@ -14,7 +14,8 @@ class AwsPackage { this.serverless = serverless; this.options = options; this.servicePath = this.serverless.config.servicePath || ''; - this.packagePath = this.options.package || + this.packagePath = + this.options.package || this.serverless.service.package.path || path.join(this.servicePath || '.', '.serverless'); this.provider = this.serverless.getProvider('aws'); @@ -37,10 +38,7 @@ class AwsPackage { package: { commands: { finalize: { - lifecycleEvents: [ - 'mergeCustomProviderResources', - 'saveServiceState', - ], + lifecycleEvents: ['mergeCustomProviderResources', 'saveServiceState'], }, }, }, @@ -52,37 +50,39 @@ class AwsPackage { /** * Outer lifecycle hooks */ - 'package:cleanup': () => BbPromise.bind(this) - .then(() => this.serverless.pluginManager.spawn('aws:common:validate')) - .then(() => this.serverless.pluginManager.spawn('aws:common:cleanupTempDir')), + 'package:cleanup': () => + BbPromise.bind(this) + .then(() => this.serverless.pluginManager.spawn('aws:common:validate')) + .then(() => this.serverless.pluginManager.spawn('aws:common:cleanupTempDir')), - 'package:initialize': () => BbPromise.bind(this) - .then(this.generateCoreTemplate), + 'package:initialize': () => BbPromise.bind(this).then(this.generateCoreTemplate), - 'package:setupProviderConfiguration': () => BbPromise.bind(this) - .then(this.mergeIamTemplates), + 'package:setupProviderConfiguration': () => BbPromise.bind(this).then(this.mergeIamTemplates), - 'before:package:compileFunctions': () => BbPromise.bind(this) - .then(this.generateArtifactDirectoryName), + 'before:package:compileFunctions': () => + BbPromise.bind(this).then(this.generateArtifactDirectoryName), - 'before:package:compileLayers': () => BbPromise.bind(this) - .then(this.generateArtifactDirectoryName), + 'before:package:compileLayers': () => + BbPromise.bind(this).then(this.generateArtifactDirectoryName), - 'package:finalize': () => BbPromise.bind(this) - .then(() => this.serverless.pluginManager.spawn('aws:package:finalize')), + 'package:finalize': () => + BbPromise.bind(this).then(() => + this.serverless.pluginManager.spawn('aws:package:finalize') + ), /** * Inner lifecycle hooks */ // Package finalize inner lifecycle - 'aws:package:finalize:mergeCustomProviderResources': () => BbPromise.bind(this) - .then(this.mergeCustomProviderResources), + 'aws:package:finalize:mergeCustomProviderResources': () => + BbPromise.bind(this).then(this.mergeCustomProviderResources), - 'aws:package:finalize:saveServiceState': () => BbPromise.bind(this) - .then(this.saveCompiledTemplate) - .then(this.saveServiceState) - .then(() => this.serverless.pluginManager.spawn('aws:common:moveArtifactsToPackage')), + 'aws:package:finalize:saveServiceState': () => + BbPromise.bind(this) + .then(this.saveCompiledTemplate) + .then(this.saveServiceState) + .then(() => this.serverless.pluginManager.spawn('aws:common:moveArtifactsToPackage')), }; } } diff --git a/lib/plugins/aws/package/index.test.js b/lib/plugins/aws/package/index.test.js index 9c912b57c..fdfa003e3 100644 --- a/lib/plugins/aws/package/index.test.js +++ b/lib/plugins/aws/package/index.test.js @@ -85,20 +85,17 @@ describe('AwsPackage', () => { let saveServiceStateStub; beforeEach(() => { - spawnStub = sinon - .stub(serverless.pluginManager, 'spawn'); - generateCoreTemplateStub = sinon - .stub(awsPackage, 'generateCoreTemplate').resolves(); - mergeIamTemplatesStub = sinon - .stub(awsPackage, 'mergeIamTemplates').resolves(); + spawnStub = sinon.stub(serverless.pluginManager, 'spawn'); + generateCoreTemplateStub = sinon.stub(awsPackage, 'generateCoreTemplate').resolves(); + mergeIamTemplatesStub = sinon.stub(awsPackage, 'mergeIamTemplates').resolves(); generateArtifactDirectoryNameStub = sinon - .stub(awsPackage, 'generateArtifactDirectoryName').resolves(); + .stub(awsPackage, 'generateArtifactDirectoryName') + .resolves(); mergeCustomProviderResourcesStub = sinon - .stub(awsPackage, 'mergeCustomProviderResources').resolves(); - saveCompiledTemplateStub = sinon - .stub(awsPackage, 'saveCompiledTemplate').resolves(); - saveServiceStateStub = sinon - .stub(awsPackage, 'saveServiceState').resolves(); + .stub(awsPackage, 'mergeCustomProviderResources') + .resolves(); + saveCompiledTemplateStub = sinon.stub(awsPackage, 'saveCompiledTemplate').resolves(); + saveServiceStateStub = sinon.stub(awsPackage, 'saveServiceState').resolves(); }); afterEach(() => { @@ -113,33 +110,32 @@ describe('AwsPackage', () => { it('should run "package:cleanup" hook', () => { const spawnAwsCommonValidateStub = spawnStub.withArgs('aws:common:validate').resolves(); - const spawnAwsCommonCleanupTempDirStub = spawnStub.withArgs('aws:common:cleanupTempDir') + const spawnAwsCommonCleanupTempDirStub = spawnStub + .withArgs('aws:common:cleanupTempDir') .resolves(); return awsPackage.hooks['package:cleanup']().then(() => { expect(spawnAwsCommonValidateStub.calledOnce).to.equal(true); - expect(spawnAwsCommonCleanupTempDirStub.calledAfter(spawnAwsCommonValidateStub)) - .to.equal(true); + expect(spawnAwsCommonCleanupTempDirStub.calledAfter(spawnAwsCommonValidateStub)).to.equal( + true + ); }); }); - it('should run "package:initialize" hook', () => awsPackage - .hooks['package:initialize']().then(() => { + it('should run "package:initialize" hook', () => + awsPackage.hooks['package:initialize']().then(() => { expect(generateCoreTemplateStub.calledOnce).to.equal(true); - }) - ); + })); - it('should run "package:setupProviderConfiguration" hook', () => awsPackage - .hooks['package:setupProviderConfiguration']().then(() => { + it('should run "package:setupProviderConfiguration" hook', () => + awsPackage.hooks['package:setupProviderConfiguration']().then(() => { expect(mergeIamTemplatesStub.calledOnce).to.equal(true); - }) - ); + })); - it('should run "before:package:compileFunctions" hook', () => awsPackage - .hooks['before:package:compileFunctions']().then(() => { + it('should run "before:package:compileFunctions" hook', () => + awsPackage.hooks['before:package:compileFunctions']().then(() => { expect(generateArtifactDirectoryNameStub.calledOnce).to.equal(true); - }) - ); + })); it('should run "package:finalize" hook', () => { const spawnAwsPackageFinalzeStub = spawnStub.withArgs('aws:package:finalize').resolves(); @@ -149,21 +145,22 @@ describe('AwsPackage', () => { }); }); - it('should run "aws:package:finalize:mergeCustomProviderResources" hook', () => awsPackage - .hooks['aws:package:finalize:mergeCustomProviderResources']().then(() => { + it('should run "aws:package:finalize:mergeCustomProviderResources" hook', () => + awsPackage.hooks['aws:package:finalize:mergeCustomProviderResources']().then(() => { expect(mergeCustomProviderResourcesStub.calledOnce).to.equal(true); - }) - ); + })); it('should run "aws:package:finalize:saveServiceState" hook', () => { const spawnAwsCommonMoveArtifactsToPackageStub = spawnStub - .withArgs('aws:common:moveArtifactsToPackage').resolves(); + .withArgs('aws:common:moveArtifactsToPackage') + .resolves(); return awsPackage.hooks['aws:package:finalize:saveServiceState']().then(() => { expect(saveCompiledTemplateStub.calledOnce).to.equal(true); expect(saveServiceStateStub.calledAfter(saveCompiledTemplateStub)).to.equal(true); - expect(spawnAwsCommonMoveArtifactsToPackageStub.calledAfter(saveServiceStateStub)) - .to.equal(true); + expect(spawnAwsCommonMoveArtifactsToPackageStub.calledAfter(saveServiceStateStub)).to.equal( + true + ); }); }); }); diff --git a/lib/plugins/aws/package/lib/core-cloudformation-template.json b/lib/plugins/aws/package/lib/core-cloudformation-template.json index 501b507f8..195b41fd7 100644 --- a/lib/plugins/aws/package/lib/core-cloudformation-template.json +++ b/lib/plugins/aws/package/lib/core-cloudformation-template.json @@ -3,8 +3,8 @@ "Description": "The AWS CloudFormation template for this Serverless application", "Resources": { "ServerlessDeploymentBucket": { - "Type" : "AWS::S3::Bucket", - "Properties" : { + "Type": "AWS::S3::Bucket", + "Properties": { "BucketEncryption": { "ServerSideEncryptionConfiguration": [ { diff --git a/lib/plugins/aws/package/lib/generateArtifactDirectoryName.js b/lib/plugins/aws/package/lib/generateArtifactDirectoryName.js index c0d1f6b33..1c5de71f9 100644 --- a/lib/plugins/aws/package/lib/generateArtifactDirectoryName.js +++ b/lib/plugins/aws/package/lib/generateArtifactDirectoryName.js @@ -10,8 +10,7 @@ module.exports = { const serviceStage = `${this.serverless.service.service}/${this.provider.getStage()}`; const dateString = `${date.getTime().toString()}-${date.toISOString()}`; const prefix = this.provider.getDeploymentPrefix(); - this.serverless.service.package - .artifactDirectoryName = `${prefix}/${serviceStage}/${dateString}`; + this.serverless.service.package.artifactDirectoryName = `${prefix}/${serviceStage}/${dateString}`; } return BbPromise.resolve(); diff --git a/lib/plugins/aws/package/lib/generateArtifactDirectoryName.test.js b/lib/plugins/aws/package/lib/generateArtifactDirectoryName.test.js index 4a39df554..f4022872b 100644 --- a/lib/plugins/aws/package/lib/generateArtifactDirectoryName.test.js +++ b/lib/plugins/aws/package/lib/generateArtifactDirectoryName.test.js @@ -20,9 +20,8 @@ describe('#generateArtifactDirectoryName()', () => { awsPackage.serverless.cli = new serverless.classes.CLI(); }); - it('should generate a name for the artifact directory based on the current time', () => awsPackage - .generateArtifactDirectoryName().then(() => { + it('should generate a name for the artifact directory based on the current time', () => + awsPackage.generateArtifactDirectoryName().then(() => { expect(awsPackage.serverless.service.package.artifactDirectoryName).to.match(/[0-9]+-.+/); - }) - ); + })); }); diff --git a/lib/plugins/aws/package/lib/generateCoreTemplate.js b/lib/plugins/aws/package/lib/generateCoreTemplate.js index 3f99b2060..8e62d6347 100644 --- a/lib/plugins/aws/package/lib/generateCoreTemplate.js +++ b/lib/plugins/aws/package/lib/generateCoreTemplate.js @@ -8,20 +8,18 @@ const validateS3BucketName = require('../../lib/validateS3BucketName'); module.exports = { generateCoreTemplate() { - _.assign( - this, - validateS3BucketName - ); + _.assign(this, validateS3BucketName); - this.serverless.service.provider - .compiledCloudFormationTemplate = this.serverless.utils.readFileSync( - path.join(this.serverless.config.serverlessPath, - 'plugins', - 'aws', - 'package', - 'lib', - 'core-cloudformation-template.json') - ); + this.serverless.service.provider.compiledCloudFormationTemplate = this.serverless.utils.readFileSync( + path.join( + this.serverless.config.serverlessPath, + 'plugins', + 'aws', + 'package', + 'lib', + 'core-cloudformation-template.json' + ) + ); const bucketName = this.serverless.service.provider.deploymentBucket; @@ -31,15 +29,19 @@ module.exports = { const tags = deploymentBucketObject.tags; const deploymentBucketLogicalId = this.provider.naming.getDeploymentBucketLogicalId(); - const bucketTags = _.map(_.keys(tags), (key) => ({ + const bucketTags = _.map(_.keys(tags), key => ({ Key: key, Value: tags[key], })); - Object.assign(this.serverless.service.provider.compiledCloudFormationTemplate - .Resources[deploymentBucketLogicalId].Properties, { + Object.assign( + this.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + deploymentBucketLogicalId + ].Properties, + { Tags: bucketTags, - }); + } + ); } const isS3TransferAccelerationSupported = this.provider.isS3TransferAccelerationSupported(); @@ -64,48 +66,58 @@ module.exports = { } this.bucketName = bucketName; this.serverless.service.package.deploymentBucket = bucketName; - this.serverless.service.provider.compiledCloudFormationTemplate - .Outputs.ServerlessDeploymentBucketName.Value = bucketName; + this.serverless.service.provider.compiledCloudFormationTemplate.Outputs.ServerlessDeploymentBucketName.Value = bucketName; - delete this.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ServerlessDeploymentBucket; + delete this.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ServerlessDeploymentBucket; }); } if (isS3TransferAccelerationEnabled && isS3TransferAccelerationSupported) { // enable acceleration via CloudFormation - Object.assign(this.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ServerlessDeploymentBucket.Properties, { + Object.assign( + this.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ServerlessDeploymentBucket.Properties, + { AccelerateConfiguration: { AccelerationStatus: 'Enabled', }, - }); + } + ); // keep track of acceleration status via CloudFormation Output - this.serverless.service.provider.compiledCloudFormationTemplate - .Outputs.ServerlessDeploymentBucketAccelerated = { Value: true }; + this.serverless.service.provider.compiledCloudFormationTemplate.Outputs.ServerlessDeploymentBucketAccelerated = { + Value: true, + }; } else if (isS3TransferAccelerationDisabled && isS3TransferAccelerationSupported) { // explicitly disable acceleration via CloudFormation - Object.assign(this.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ServerlessDeploymentBucket.Properties, { + Object.assign( + this.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ServerlessDeploymentBucket.Properties, + { AccelerateConfiguration: { AccelerationStatus: 'Suspended', }, - }); + } + ); } const coreTemplateFileName = this.provider.naming.getCoreTemplateFileName(); - const coreTemplateFilePath = path.join(this.serverless.config.servicePath, + const coreTemplateFilePath = path.join( + this.serverless.config.servicePath, '.serverless', - coreTemplateFileName); + coreTemplateFileName + ); - this.serverless.utils.writeFileSync(coreTemplateFilePath, - this.serverless.service.provider.compiledCloudFormationTemplate); + this.serverless.utils.writeFileSync( + coreTemplateFilePath, + this.serverless.service.provider.compiledCloudFormationTemplate + ); - this.serverless.service.provider.coreCloudFormationTemplate = - _.cloneDeep(this.serverless.service.provider.compiledCloudFormationTemplate); + this.serverless.service.provider.coreCloudFormationTemplate = _.cloneDeep( + this.serverless.service.provider.compiledCloudFormationTemplate + ); return BbPromise.resolve(); }, - }; diff --git a/lib/plugins/aws/package/lib/generateCoreTemplate.test.js b/lib/plugins/aws/package/lib/generateCoreTemplate.test.js index 177693e10..acb0b2ea6 100644 --- a/lib/plugins/aws/package/lib/generateCoreTemplate.test.js +++ b/lib/plugins/aws/package/lib/generateCoreTemplate.test.js @@ -7,8 +7,8 @@ const AwsProvider = require('../../provider/awsProvider'); const Serverless = require('../../../../Serverless'); const validate = require('../../lib/validate'); const generateCoreTemplate = require('./generateCoreTemplate'); -const testUtils = require('../../../../../tests/utils'); const expect = require('chai').expect; +const { getTmpDirPath } = require('../../../../../tests/utils/fs'); chai.use(require('chai-as-promised')); @@ -29,7 +29,7 @@ describe('#generateCoreTemplate()', () => { Object.assign(awsPlugin, generateCoreTemplate, validate); awsPlugin.serverless.cli = new serverless.classes.CLI(); - awsPlugin.serverless.config.servicePath = testUtils.getTmpDirPath(); + awsPlugin.serverless.config.servicePath = getTmpDirPath(); awsPlugin.serverless.service.provider.compiledCloudFormationTemplate = { Resources: {}, @@ -51,25 +51,16 @@ describe('#generateCoreTemplate()', () => { awsPlugin.serverless.service.provider.deploymentBucket = bucketName; const coreCloudFormationTemplate = awsPlugin.serverless.utils.readFileSync( - path.join( - __dirname, - 'core-cloudformation-template.json' - ) + path.join(__dirname, 'core-cloudformation-template.json') ); - awsPlugin.serverless.service.provider - .compiledCloudFormationTemplate = coreCloudFormationTemplate; + awsPlugin.serverless.service.provider.compiledCloudFormationTemplate = coreCloudFormationTemplate; - return expect(awsPlugin.generateCoreTemplate()).to.be.fulfilled - .then(() => { - const template = awsPlugin.serverless.service.provider.compiledCloudFormationTemplate; - expect( - template.Outputs.ServerlessDeploymentBucketName.Value - ).to.equal(bucketName); - // eslint-disable-next-line no-unused-expressions - expect( - template.Resources.ServerlessDeploymentBucket - ).to.not.exist; - }); + return expect(awsPlugin.generateCoreTemplate()).to.be.fulfilled.then(() => { + const template = awsPlugin.serverless.service.provider.compiledCloudFormationTemplate; + expect(template.Outputs.ServerlessDeploymentBucketName.Value).to.equal(bucketName); + // eslint-disable-next-line no-unused-expressions + expect(template.Resources.ServerlessDeploymentBucket).to.not.exist; + }); }); it('should add resource tags to the bucket if present', () => { @@ -84,8 +75,8 @@ describe('#generateCoreTemplate()', () => { return expect(awsPlugin.generateCoreTemplate()).to.be.fulfilled.then(() => { expect( - awsPlugin.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ServerlessDeploymentBucket + awsPlugin.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ServerlessDeploymentBucket ).to.be.deep.equal({ Type: 'AWS::S3::Bucket', Properties: { @@ -98,10 +89,7 @@ describe('#generateCoreTemplate()', () => { }, ], }, - Tags: [ - { Key: 'FOO', Value: 'bar' }, - { Key: 'BAZ', Value: 'qux' }, - ], + Tags: [{ Key: 'FOO', Value: 'bar' }, { Key: 'BAZ', Value: 'qux' }], }, }); }); @@ -114,36 +102,25 @@ describe('#generateCoreTemplate()', () => { awsPlugin.provider.options['aws-s3-accelerate'] = true; const coreCloudFormationTemplate = awsPlugin.serverless.utils.readFileSync( - path.join( - __dirname, - 'core-cloudformation-template.json' - ) + path.join(__dirname, 'core-cloudformation-template.json') ); - awsPlugin.serverless.service.provider - .compiledCloudFormationTemplate = coreCloudFormationTemplate; + awsPlugin.serverless.service.provider.compiledCloudFormationTemplate = coreCloudFormationTemplate; - return expect(awsPlugin.generateCoreTemplate()).to.be.fulfilled - .then(() => { - const template = awsPlugin.serverless.service.provider.compiledCloudFormationTemplate; - expect( - template.Outputs.ServerlessDeploymentBucketName.Value - ).to.equal(bucketName); - // eslint-disable-next-line no-unused-expressions - expect( - template.Resources.ServerlessDeploymentBucket - ).to.not.exist; - // eslint-disable-next-line no-unused-expressions - expect( - template.Outputs.ServerlessDeploymentBucketAccelerated - ).to.not.exist; - }); + return expect(awsPlugin.generateCoreTemplate()).to.be.fulfilled.then(() => { + const template = awsPlugin.serverless.service.provider.compiledCloudFormationTemplate; + expect(template.Outputs.ServerlessDeploymentBucketName.Value).to.equal(bucketName); + // eslint-disable-next-line no-unused-expressions + expect(template.Resources.ServerlessDeploymentBucket).to.not.exist; + // eslint-disable-next-line no-unused-expressions + expect(template.Outputs.ServerlessDeploymentBucketAccelerated).to.not.exist; + }); }); it('should use a auto generated bucket if you does not specify deploymentBucket', () => expect(awsPlugin.generateCoreTemplate()).to.be.fulfilled.then(() => { expect( - awsPlugin.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ServerlessDeploymentBucket + awsPlugin.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ServerlessDeploymentBucket ).to.be.deep.equal({ Type: 'AWS::S3::Bucket', Properties: { @@ -158,19 +135,17 @@ describe('#generateCoreTemplate()', () => { }, }, }); - }) - ); + })); it('should add a deployment bucket to the CF template, if not provided', () => { sinon.stub(awsPlugin.provider, 'request').resolves(); sinon.stub(serverless.utils, 'writeFileSync').resolves(); serverless.config.servicePath = './'; - return awsPlugin.generateCoreTemplate() - .then(() => { - const template = serverless.service.provider.coreCloudFormationTemplate; - expect(template).to.not.equal(null); - }); + return awsPlugin.generateCoreTemplate().then(() => { + const template = serverless.service.provider.coreCloudFormationTemplate; + expect(template).to.not.equal(null); + }); }); it('should add a custom output if S3 Transfer Acceleration is enabled', () => { @@ -179,12 +154,11 @@ describe('#generateCoreTemplate()', () => { serverless.config.servicePath = './'; awsPlugin.provider.options['aws-s3-accelerate'] = true; - return awsPlugin.generateCoreTemplate() - .then(() => { - const template = serverless.service.provider.coreCloudFormationTemplate; - expect(template.Outputs.ServerlessDeploymentBucketAccelerated).to.not.equal(null); - expect(template.Outputs.ServerlessDeploymentBucketAccelerated.Value).to.equal(true); - }); + return awsPlugin.generateCoreTemplate().then(() => { + const template = serverless.service.provider.coreCloudFormationTemplate; + expect(template.Outputs.ServerlessDeploymentBucketAccelerated).to.not.equal(null); + expect(template.Outputs.ServerlessDeploymentBucketAccelerated.Value).to.equal(true); + }); }); it('should explicitly disable S3 Transfer Acceleration, if requested', () => { @@ -193,27 +167,26 @@ describe('#generateCoreTemplate()', () => { serverless.config.servicePath = './'; awsPlugin.provider.options['no-aws-s3-accelerate'] = true; - return awsPlugin.generateCoreTemplate() - .then(() => { - const template = serverless.service.provider.coreCloudFormationTemplate; - expect(template.Resources.ServerlessDeploymentBucket).to.be.deep.equal({ - Type: 'AWS::S3::Bucket', - Properties: { - AccelerateConfiguration: { - AccelerationStatus: 'Suspended', - }, - BucketEncryption: { - ServerSideEncryptionConfiguration: [ - { - ServerSideEncryptionByDefault: { - SSEAlgorithm: 'AES256', - }, - }, - ], - }, + return awsPlugin.generateCoreTemplate().then(() => { + const template = serverless.service.provider.coreCloudFormationTemplate; + expect(template.Resources.ServerlessDeploymentBucket).to.be.deep.equal({ + Type: 'AWS::S3::Bucket', + Properties: { + AccelerateConfiguration: { + AccelerationStatus: 'Suspended', }, - }); + BucketEncryption: { + ServerSideEncryptionConfiguration: [ + { + ServerSideEncryptionByDefault: { + SSEAlgorithm: 'AES256', + }, + }, + ], + }, + }, }); + }); }); it('should exclude AccelerateConfiguration for govcloud region', () => { @@ -222,24 +195,23 @@ describe('#generateCoreTemplate()', () => { serverless.config.servicePath = './'; awsPlugin.provider.options.region = 'us-gov-west-1'; - return awsPlugin.generateCoreTemplate() - .then(() => { - const template = serverless.service.provider.coreCloudFormationTemplate; - expect(template.Resources.ServerlessDeploymentBucket).to.be.deep.equal({ - Type: 'AWS::S3::Bucket', - Properties: { - BucketEncryption: { - ServerSideEncryptionConfiguration: [ - { - ServerSideEncryptionByDefault: { - SSEAlgorithm: 'AES256', - }, + return awsPlugin.generateCoreTemplate().then(() => { + const template = serverless.service.provider.coreCloudFormationTemplate; + expect(template.Resources.ServerlessDeploymentBucket).to.be.deep.equal({ + Type: 'AWS::S3::Bucket', + Properties: { + BucketEncryption: { + ServerSideEncryptionConfiguration: [ + { + ServerSideEncryptionByDefault: { + SSEAlgorithm: 'AES256', }, - ], - }, + }, + ], }, - }); + }, }); + }); }); it('should explode if transfer acceleration is both enabled and disabled', () => { @@ -249,8 +221,9 @@ describe('#generateCoreTemplate()', () => { awsPlugin.provider.options['aws-s3-accelerate'] = true; awsPlugin.provider.options['no-aws-s3-accelerate'] = true; - return expect( - awsPlugin.generateCoreTemplate() - ).to.be.rejectedWith(serverless.classes.Error, /at the same time/); + return expect(awsPlugin.generateCoreTemplate()).to.be.rejectedWith( + serverless.classes.Error, + /at the same time/ + ); }); }); diff --git a/lib/plugins/aws/package/lib/iam-role-lambda-execution-template.json b/lib/plugins/aws/package/lib/iam-role-lambda-execution-template.json index d75bf0450..c2ca81b2c 100644 --- a/lib/plugins/aws/package/lib/iam-role-lambda-execution-template.json +++ b/lib/plugins/aws/package/lib/iam-role-lambda-execution-template.json @@ -7,13 +7,9 @@ { "Effect": "Allow", "Principal": { - "Service": [ - "lambda.amazonaws.com" - ] + "Service": ["lambda.amazonaws.com"] }, - "Action": [ - "sts:AssumeRole" - ] + "Action": ["sts:AssumeRole"] } ] }, @@ -25,16 +21,12 @@ "Statement": [ { "Effect": "Allow", - "Action": [ - "logs:CreateLogStream" - ], + "Action": ["logs:CreateLogStream"], "Resource": [] }, { "Effect": "Allow", - "Action": [ - "logs:PutLogEvents" - ], + "Action": ["logs:PutLogEvents"], "Resource": [] } ] diff --git a/lib/plugins/aws/package/lib/mergeCustomProviderResources.test.js b/lib/plugins/aws/package/lib/mergeCustomProviderResources.test.js index 885cf5b34..8ecfc7b97 100644 --- a/lib/plugins/aws/package/lib/mergeCustomProviderResources.test.js +++ b/lib/plugins/aws/package/lib/mergeCustomProviderResources.test.js @@ -14,40 +14,33 @@ describe('mergeCustomProviderResources', () => { serverless = new Serverless(); awsPackage = new AwsPackage(serverless, {}); - coreCloudFormationTemplate = awsPackage - .serverless.utils.readFileSync( - path.join( - __dirname, - '..', - 'lib', - 'core-cloudformation-template.json' - ) - ); + coreCloudFormationTemplate = awsPackage.serverless.utils.readFileSync( + path.join(__dirname, '..', 'lib', 'core-cloudformation-template.json') + ); - awsPackage.serverless.service.provider - .compiledCloudFormationTemplate = coreCloudFormationTemplate; + awsPackage.serverless.service.provider.compiledCloudFormationTemplate = coreCloudFormationTemplate; }); describe('#mergeCustomProviderResources()', () => { it('should set an empty resources.Resources object if it is not present', () => { - awsPackage.serverless.service.provider - .compiledCloudFormationTemplate.Resources = {}; // reset the core CloudFormation template + awsPackage.serverless.service.provider.compiledCloudFormationTemplate.Resources = {}; // reset the core CloudFormation template awsPackage.serverless.service.resources.Resources = null; return awsPackage.mergeCustomProviderResources().then(() => { - expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate.Resources) - .to.deep.equal({}); + expect( + awsPackage.serverless.service.provider.compiledCloudFormationTemplate.Resources + ).to.deep.equal({}); }); }); it('should set an empty resources.Outputs object if it is not present', () => { - awsPackage.serverless.service.provider - .compiledCloudFormationTemplate.Outputs = {}; // reset the core CloudFormation template + awsPackage.serverless.service.provider.compiledCloudFormationTemplate.Outputs = {}; // reset the core CloudFormation template awsPackage.serverless.service.resources.Outputs = null; return awsPackage.mergeCustomProviderResources().then(() => { - expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate.Outputs) - .to.deep.equal({}); + expect( + awsPackage.serverless.service.provider.compiledCloudFormationTemplate.Outputs + ).to.deep.equal({}); }); }); @@ -59,8 +52,9 @@ describe('mergeCustomProviderResources', () => { awsPackage.serverless.service.resources = customResourcesMock; return awsPackage.mergeCustomProviderResources().then(() => { - expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate.Description) - .to.equal(customResourcesMock.Description); + expect( + awsPackage.serverless.service.provider.compiledCloudFormationTemplate.Description + ).to.equal(customResourcesMock.Description); }); }); @@ -91,8 +85,9 @@ describe('mergeCustomProviderResources', () => { awsPackage.serverless.service.resources = customResourcesMock; return awsPackage.mergeCustomProviderResources().then(() => { - expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ServerlessDeploymentBucket + expect( + awsPackage.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ServerlessDeploymentBucket ).to.deep.equal(customResourcesMock.Resources.ServerlessDeploymentBucket); }); }); @@ -124,16 +119,23 @@ describe('mergeCustomProviderResources', () => { awsPackage.serverless.service.resources = customResourcesMock; return awsPackage.mergeCustomProviderResources().then(() => { - expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FakeResource1).to.deep.equal(customResourcesMock.Resources.FakeResource1); - expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate - .Resources.FakeResource2).to.deep.equal(customResourcesMock.Resources.FakeResource2); - expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate - .Outputs.FakeOutput1).to.deep.equal(customResourcesMock.Outputs.FakeOutput1); - expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate - .Outputs.FakeOutput2).to.deep.equal(customResourcesMock.Outputs.FakeOutput2); - expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate - .CustomDefinition).to.deep.equal(customResourcesMock.CustomDefinition); + expect( + awsPackage.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FakeResource1 + ).to.deep.equal(customResourcesMock.Resources.FakeResource1); + expect( + awsPackage.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FakeResource2 + ).to.deep.equal(customResourcesMock.Resources.FakeResource2); + expect( + awsPackage.serverless.service.provider.compiledCloudFormationTemplate.Outputs.FakeOutput1 + ).to.deep.equal(customResourcesMock.Outputs.FakeOutput1); + expect( + awsPackage.serverless.service.provider.compiledCloudFormationTemplate.Outputs.FakeOutput2 + ).to.deep.equal(customResourcesMock.Outputs.FakeOutput2); + expect( + awsPackage.serverless.service.provider.compiledCloudFormationTemplate.CustomDefinition + ).to.deep.equal(customResourcesMock.CustomDefinition); }); }); @@ -151,28 +153,21 @@ describe('mergeCustomProviderResources', () => { expect( awsPackage.serverless.service.provider.compiledCloudFormationTemplate .AWSTemplateFormatVersion - ).to.equal( - coreCloudFormationTemplate.AWSTemplateFormatVersion - ); + ).to.equal(coreCloudFormationTemplate.AWSTemplateFormatVersion); expect( awsPackage.serverless.service.provider.compiledCloudFormationTemplate.Description - ).to.equal( - coreCloudFormationTemplate.Description - ); - - expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ServerlessDeploymentBucket - ).to.deep.equal( - coreCloudFormationTemplate.Resources.ServerlessDeploymentBucket - ); + ).to.equal(coreCloudFormationTemplate.Description); expect( - awsPackage.serverless.service.provider.compiledCloudFormationTemplate - .Outputs.ServerlessDeploymentBucketName - ).to.deep.equal( - coreCloudFormationTemplate.Outputs.ServerlessDeploymentBucketName - ); + awsPackage.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ServerlessDeploymentBucket + ).to.deep.equal(coreCloudFormationTemplate.Resources.ServerlessDeploymentBucket); + + expect( + awsPackage.serverless.service.provider.compiledCloudFormationTemplate.Outputs + .ServerlessDeploymentBucketName + ).to.deep.equal(coreCloudFormationTemplate.Outputs.ServerlessDeploymentBucketName); }); }); }); diff --git a/lib/plugins/aws/package/lib/mergeIamTemplates.js b/lib/plugins/aws/package/lib/mergeIamTemplates.js index cb74e820d..945312b69 100644 --- a/lib/plugins/aws/package/lib/mergeIamTemplates.js +++ b/lib/plugins/aws/package/lib/mergeIamTemplates.js @@ -18,10 +18,9 @@ module.exports = { } // create log group resources - this.serverless.service.getAllFunctions().forEach((functionName) => { + this.serverless.service.getAllFunctions().forEach(functionName => { const functionObject = this.serverless.service.getFunction(functionName); - const logGroupLogicalId = this.provider.naming - .getLogGroupLogicalId(functionName); + const logGroupLogicalId = this.provider.naming.getLogGroupLogicalId(functionName); const newLogGroup = { [logGroupLogicalId]: { Type: 'AWS::Logs::LogGroup', @@ -37,14 +36,15 @@ module.exports = { if (_.isInteger(retentionInDays) && retentionInDays > 0) { newLogGroup[logGroupLogicalId].Properties.RetentionInDays = retentionInDays; } else { - const errorMessage = - `logRetentionInDays should be an integer over 0 but ${rawRetentionInDays}`; + const errorMessage = `logRetentionInDays should be an integer over 0 but ${rawRetentionInDays}`; throw new this.serverless.classes.Error(errorMessage); } } - _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, - newLogGroup); + _.merge( + this.serverless.service.provider.compiledCloudFormationTemplate.Resources, + newLogGroup + ); }); // resolve early if provider level role is provided @@ -54,7 +54,7 @@ module.exports = { // resolve early if all functions contain a custom role const customRolesProvided = []; - this.serverless.service.getAllFunctions().forEach((functionName) => { + this.serverless.service.getAllFunctions().forEach(functionName => { const functionObject = this.serverless.service.getFunction(functionName); customRolesProvided.push('role' in functionObject); }); @@ -64,66 +64,76 @@ module.exports = { // merge in the iamRoleLambdaTemplate const iamRoleLambdaExecutionTemplate = this.serverless.utils.readFileSync( - path.join(this.serverless.config.serverlessPath, + path.join( + this.serverless.config.serverlessPath, 'plugins', 'aws', 'package', 'lib', - 'iam-role-lambda-execution-template.json') + 'iam-role-lambda-execution-template.json' + ) ); iamRoleLambdaExecutionTemplate.Properties.Path = this.provider.naming.getRolePath(); iamRoleLambdaExecutionTemplate.Properties.RoleName = this.provider.naming.getRoleName(); - iamRoleLambdaExecutionTemplate.Properties.Policies[0] - .PolicyName = this.provider.naming.getPolicyName(); + iamRoleLambdaExecutionTemplate.Properties.Policies[0].PolicyName = this.provider.naming.getPolicyName(); - _.merge( - this.serverless.service.provider.compiledCloudFormationTemplate.Resources, - { - [this.provider.naming.getRoleLogicalId()]: iamRoleLambdaExecutionTemplate, + _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, { + [this.provider.naming.getRoleLogicalId()]: iamRoleLambdaExecutionTemplate, + }); + + const canonicalFunctionNamePrefix = `${ + this.provider.serverless.service.service + }-${this.provider.getStage()}`; + const logGroupsPrefix = this.provider.naming.getLogGroupName(canonicalFunctionNamePrefix); + + const policyDocumentStatements = this.serverless.service.provider.compiledCloudFormationTemplate + .Resources[this.provider.naming.getRoleLogicalId()].Properties.Policies[0].PolicyDocument + .Statement; + + // Ensure general polices for functions with default name resolution + policyDocumentStatements[0].Resource.push({ + 'Fn::Sub': + 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}' + + `:log-group:${logGroupsPrefix}*:*`, + }); + + policyDocumentStatements[1].Resource.push({ + 'Fn::Sub': + 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}' + + `:log-group:${logGroupsPrefix}*:*:*`, + }); + + // Ensure policies for functions with custom name resolution + this.serverless.service.getAllFunctions().forEach(functionName => { + const { name: resolvedFunctionName } = this.serverless.service.getFunction(functionName); + if (!resolvedFunctionName || resolvedFunctionName.startsWith(canonicalFunctionNamePrefix)) { + return; } - ); - this.serverless.service.getAllFunctions().forEach((functionName) => { - const functionObject = this.serverless.service.getFunction(functionName); + const customFunctionNamelogGroupsPrefix = this.provider.naming.getLogGroupName( + resolvedFunctionName + ); - this.serverless.service.provider.compiledCloudFormationTemplate - .Resources[this.provider.naming.getRoleLogicalId()] - .Properties - .Policies[0] - .PolicyDocument - .Statement[0] - .Resource - .push({ - 'Fn::Sub': 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}' + - `:log-group:${this.provider.naming.getLogGroupName(functionObject.name)}:*`, - }); + policyDocumentStatements[0].Resource.push({ + 'Fn::Sub': + 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}' + + `:log-group:${customFunctionNamelogGroupsPrefix}:*`, + }); - this.serverless.service.provider.compiledCloudFormationTemplate - .Resources[this.provider.naming.getRoleLogicalId()] - .Properties - .Policies[0] - .PolicyDocument - .Statement[1] - .Resource - .push({ - 'Fn::Sub': 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}' + - `:log-group:${this.provider.naming.getLogGroupName(functionObject.name)}:*:*`, - }); + policyDocumentStatements[1].Resource.push({ + 'Fn::Sub': + 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}' + + `:log-group:${customFunctionNamelogGroupsPrefix}:*:*`, + }); }); if (this.serverless.service.provider.iamRoleStatements) { // add custom iam role statements - this.serverless.service.provider.compiledCloudFormationTemplate - .Resources[this.provider.naming.getRoleLogicalId()] - .Properties - .Policies[0] - .PolicyDocument - .Statement = this.serverless.service.provider.compiledCloudFormationTemplate - .Resources[this.provider.naming.getRoleLogicalId()] - .Properties - .Policies[0] - .PolicyDocument - .Statement.concat(this.serverless.service.provider.iamRoleStatements); + this.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + this.provider.naming.getRoleLogicalId() + ].Properties.Policies[0].PolicyDocument.Statement = policyDocumentStatements.concat( + this.serverless.service.provider.iamRoleStatements + ); } if (this.serverless.service.provider.iamManagedPolicies) { @@ -136,7 +146,7 @@ module.exports = { // check if one of the functions contains vpc configuration const vpcConfigProvided = []; - this.serverless.service.getAllFunctions().forEach((functionName) => { + this.serverless.service.getAllFunctions().forEach(functionName => { const functionObject = this.serverless.service.getFunction(functionName); if ('vpc' in functionObject) { vpcConfigProvided.push(true); @@ -145,24 +155,27 @@ module.exports = { if (_.includes(vpcConfigProvided, true) || this.serverless.service.provider.vpc) { // add managed iam policy to allow ENI management - this.mergeManagedPolicies([{ - 'Fn::Join': ['', - [ - 'arn:', - { Ref: 'AWS::Partition' }, - ':iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole', + this.mergeManagedPolicies([ + { + 'Fn::Join': [ + '', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole', + ], ], - ], - }]); + }, + ]); } return BbPromise.resolve(); }, mergeManagedPolicies(managedPolicies) { - const resource = this.serverless.service.provider.compiledCloudFormationTemplate - .Resources[this.provider.naming.getRoleLogicalId()] - .Properties; + const resource = this.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + this.provider.naming.getRoleLogicalId() + ].Properties; if (!_.has(resource, 'ManagedPolicyArns') || _.isEmpty(resource.ManagedPolicyArns)) { resource.ManagedPolicyArns = []; } @@ -181,9 +194,11 @@ module.exports = { } else { const descriptions = statements.map((statement, i) => { const missing = ['Effect', 'Action', 'Resource'].filter( - prop => statement[prop] === undefined); - return missing.length === 0 ? null : - `statement ${i} is missing the following properties: ${missing.join(', ')}`; + prop => statement[prop] === undefined + ); + return missing.length === 0 + ? null + : `statement ${i} is missing the following properties: ${missing.join(', ')}`; }); const flawed = descriptions.filter(curr => curr); if (flawed.length) { diff --git a/lib/plugins/aws/package/lib/mergeIamTemplates.test.js b/lib/plugins/aws/package/lib/mergeIamTemplates.test.js index b282f82cf..09447f641 100644 --- a/lib/plugins/aws/package/lib/mergeIamTemplates.test.js +++ b/lib/plugins/aws/package/lib/mergeIamTemplates.test.js @@ -9,12 +9,15 @@ const AwsPackage = require('../index'); describe('#mergeIamTemplates()', () => { let awsPackage; let serverless; + const serviceName = 'new-service'; const functionName = 'test'; + const stage = 'dev'; + const resolvedFunctionName = `${serviceName}-${stage}-${functionName}`; beforeEach(() => { serverless = new Serverless(); const options = { - stage: 'dev', + stage, region: 'us-east-1', }; serverless.setProvider('aws', new AwsProvider(serverless, options)); @@ -23,138 +26,224 @@ describe('#mergeIamTemplates()', () => { awsPackage.serverless.service.provider.compiledCloudFormationTemplate = { Resources: {}, }; - awsPackage.serverless.service.service = 'new-service'; + awsPackage.serverless.service.service = serviceName; awsPackage.serverless.service.functions = { [functionName]: { - name: 'test', artifact: 'test.zip', handler: 'handler.hello', }, }; + serverless.service.setFunctionNames(); // Ensure to resolve function names }); it('should not merge if there are no functions', () => { awsPackage.serverless.service.functions = {}; - return awsPackage.mergeIamTemplates() - .then(() => { - const resources = awsPackage.serverless.service.provider - .compiledCloudFormationTemplate.Resources; + return awsPackage.mergeIamTemplates().then(() => { + const resources = + awsPackage.serverless.service.provider.compiledCloudFormationTemplate.Resources; - return expect( - resources[awsPackage.provider.naming.getRoleLogicalId()] - ).to.not.exist; - }); + return expect(resources[awsPackage.provider.naming.getRoleLogicalId()]).to.not.exist; + }); }); - it('should merge the IamRoleLambdaExecution template into the CloudFormation template', - () => awsPackage.mergeIamTemplates() - .then(() => { - const qualifiedFunction = awsPackage.serverless.service.getFunction(functionName).name; - expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate - .Resources[awsPackage.provider.naming.getRoleLogicalId()] - ).to.deep.equal({ - Type: 'AWS::IAM::Role', - Properties: { - AssumeRolePolicyDocument: { - Version: '2012-10-17', - Statement: [ - { - Effect: 'Allow', - Principal: { - Service: [ - 'lambda.amazonaws.com', - ], - }, - Action: [ - 'sts:AssumeRole', - ], - }, - ], - }, - Path: '/', - Policies: [ + it('should merge the IamRoleLambdaExecution template into the CloudFormation template', () => + awsPackage.mergeIamTemplates().then(() => { + const canonicalFunctionsPrefix = `${ + awsPackage.serverless.service.service + }-${awsPackage.provider.getStage()}`; + + expect( + awsPackage.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + awsPackage.provider.naming.getRoleLogicalId() + ] + ).to.deep.equal({ + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Version: '2012-10-17', + Statement: [ { - PolicyName: { - 'Fn::Join': [ - '-', - [ - awsPackage.provider.getStage(), - awsPackage.serverless.service.service, - 'lambda', - ], - ], - }, - PolicyDocument: { - Version: '2012-10-17', - Statement: [ - { - Effect: 'Allow', - Action: [ - 'logs:CreateLogStream', - ], - Resource: [ - { - 'Fn::Sub': 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:' - + `log-group:/aws/lambda/${qualifiedFunction}:*`, - }, - ], - }, - { - Effect: 'Allow', - Action: [ - 'logs:PutLogEvents', - ], - Resource: [ - { - 'Fn::Sub': 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:' - + `log-group:/aws/lambda/${qualifiedFunction}:*:*`, - }, - ], - }, - ], + Effect: 'Allow', + Principal: { + Service: ['lambda.amazonaws.com'], }, + Action: ['sts:AssumeRole'], }, ], - RoleName: { - 'Fn::Join': [ - '-', - [ - awsPackage.serverless.service.service, - awsPackage.provider.getStage(), - { - Ref: 'AWS::Region', - }, - 'lambdaRole', - ], - ], - }, }, - }); - }) - ); + Path: '/', + Policies: [ + { + PolicyName: { + 'Fn::Join': [ + '-', + [awsPackage.provider.getStage(), awsPackage.serverless.service.service, 'lambda'], + ], + }, + PolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Action: ['logs:CreateLogStream'], + Resource: [ + { + 'Fn::Sub': + 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:' + + `log-group:/aws/lambda/${canonicalFunctionsPrefix}*:*`, + }, + ], + }, + { + Effect: 'Allow', + Action: ['logs:PutLogEvents'], + Resource: [ + { + 'Fn::Sub': + 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:' + + `log-group:/aws/lambda/${canonicalFunctionsPrefix}*:*:*`, + }, + ], + }, + ], + }, + }, + ], + RoleName: { + 'Fn::Join': [ + '-', + [ + awsPackage.serverless.service.service, + awsPackage.provider.getStage(), + { + Ref: 'AWS::Region', + }, + 'lambdaRole', + ], + ], + }, + }, + }); + })); + + it('should ensure IAM policies for custom named functions', () => { + const customFunctionName = 'foo-bar'; + awsPackage.serverless.service.functions = { + [functionName]: { + name: customFunctionName, + artifact: 'test.zip', + handler: 'handler.hello', + }, + }; + serverless.service.setFunctionNames(); // Ensure to resolve function names + + return awsPackage.mergeIamTemplates().then(() => { + const canonicalFunctionsPrefix = `${ + awsPackage.serverless.service.service + }-${awsPackage.provider.getStage()}`; + + expect( + awsPackage.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + awsPackage.provider.naming.getRoleLogicalId() + ] + ).to.deep.equal({ + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Principal: { + Service: ['lambda.amazonaws.com'], + }, + Action: ['sts:AssumeRole'], + }, + ], + }, + Path: '/', + Policies: [ + { + PolicyName: { + 'Fn::Join': [ + '-', + [awsPackage.provider.getStage(), awsPackage.serverless.service.service, 'lambda'], + ], + }, + PolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Action: ['logs:CreateLogStream'], + Resource: [ + { + 'Fn::Sub': + 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:' + + `log-group:/aws/lambda/${canonicalFunctionsPrefix}*:*`, + }, + { + 'Fn::Sub': + 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:' + + `log-group:/aws/lambda/${customFunctionName}:*`, + }, + ], + }, + { + Effect: 'Allow', + Action: ['logs:PutLogEvents'], + Resource: [ + { + 'Fn::Sub': + 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:' + + `log-group:/aws/lambda/${canonicalFunctionsPrefix}*:*:*`, + }, + { + 'Fn::Sub': + 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:' + + `log-group:/aws/lambda/${customFunctionName}:*:*`, + }, + ], + }, + ], + }, + }, + ], + RoleName: { + 'Fn::Join': [ + '-', + [ + awsPackage.serverless.service.service, + awsPackage.provider.getStage(), + { + Ref: 'AWS::Region', + }, + 'lambdaRole', + ], + ], + }, + }, + }); + }); + }); it('should add custom IAM policy statements', () => { awsPackage.serverless.service.provider.iamRoleStatements = [ { Effect: 'Allow', - Action: [ - 'something:SomethingElse', - ], + Action: ['something:SomethingElse'], Resource: 'some:aws:arn:xxx:*:*', }, ]; - return awsPackage.mergeIamTemplates() - .then(() => { - expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate - .Resources[awsPackage.provider.naming.getRoleLogicalId()] - .Properties - .Policies[0] - .PolicyDocument - .Statement[2] - ).to.deep.equal(awsPackage.serverless.service.provider.iamRoleStatements[0]); - }); + return awsPackage.mergeIamTemplates().then(() => { + expect( + awsPackage.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + awsPackage.provider.naming.getRoleLogicalId() + ].Properties.Policies[0].PolicyDocument.Statement[2] + ).to.deep.equal(awsPackage.serverless.service.provider.iamRoleStatements[0]); + }); }); it('should add managed policy arns', () => { @@ -163,14 +252,13 @@ describe('#mergeIamTemplates()', () => { 'someOther:aws:arn:xxx:*:*', { 'Fn::Join': [':', ['arn:aws:iam:', { Ref: 'AWSAccountId' }, 'some/path']] }, ]; - return awsPackage.mergeIamTemplates() - .then(() => { - expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate - .Resources[awsPackage.provider.naming.getRoleLogicalId()] - .Properties - .ManagedPolicyArns - ).to.deep.equal(awsPackage.serverless.service.provider.iamManagedPolicies); - }); + return awsPackage.mergeIamTemplates().then(() => { + expect( + awsPackage.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + awsPackage.provider.naming.getRoleLogicalId() + ].Properties.ManagedPolicyArns + ).to.deep.equal(awsPackage.serverless.service.provider.iamManagedPolicies); + }); }); it('should merge managed policy arns when vpc config supplied', () => { @@ -188,23 +276,24 @@ describe('#mergeIamTemplates()', () => { 'some:aws:arn:xxx:*:*', 'someOther:aws:arn:xxx:*:*', { 'Fn::Join': [':', ['arn:aws:iam:', { Ref: 'AWSAccountId' }, 'some/path']] }, - { 'Fn::Join': ['', - [ - 'arn:', - { Ref: 'AWS::Partition' }, - ':iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole', + { + 'Fn::Join': [ + '', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole', + ], ], - ], }, ]; - return awsPackage.mergeIamTemplates() - .then(() => { - expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate - .Resources[awsPackage.provider.naming.getRoleLogicalId()] - .Properties - .ManagedPolicyArns - ).to.deep.equal(expectedManagedPolicyArns); - }); + return awsPackage.mergeIamTemplates().then(() => { + expect( + awsPackage.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + awsPackage.provider.naming.getRoleLogicalId() + ].Properties.ManagedPolicyArns + ).to.deep.equal(expectedManagedPolicyArns); + }); }); it('should throw error if custom IAM policy statements is not an array', () => { @@ -213,9 +302,7 @@ describe('#mergeIamTemplates()', () => { statments: [ { Effect: 'Allow', - Action: [ - 'something:SomethingElse', - ], + Action: ['something:SomethingElse'], Resource: 'some:aws:arn:xxx:*:*', }, ], @@ -225,33 +312,42 @@ describe('#mergeIamTemplates()', () => { }); it('should throw error if a custom IAM policy statement does not have an Effect field', () => { - awsPackage.serverless.service.provider.iamRoleStatements = [{ - Action: ['something:SomethingElse'], - Resource: '*', - }]; + awsPackage.serverless.service.provider.iamRoleStatements = [ + { + Action: ['something:SomethingElse'], + Resource: '*', + }, + ]; expect(() => awsPackage.mergeIamTemplates()).to.throw( - 'missing the following properties: Effect'); + 'missing the following properties: Effect' + ); }); it('should throw error if a custom IAM policy statement does not have an Action field', () => { - awsPackage.serverless.service.provider.iamRoleStatements = [{ - Effect: 'Allow', - Resource: '*', - }]; + awsPackage.serverless.service.provider.iamRoleStatements = [ + { + Effect: 'Allow', + Resource: '*', + }, + ]; expect(() => awsPackage.mergeIamTemplates()).to.throw( - 'missing the following properties: Action'); + 'missing the following properties: Action' + ); }); it('should throw error if a custom IAM policy statement does not have a Resource field', () => { - awsPackage.serverless.service.provider.iamRoleStatements = [{ - Action: ['something:SomethingElse'], - Effect: 'Allow', - }]; + awsPackage.serverless.service.provider.iamRoleStatements = [ + { + Action: ['something:SomethingElse'], + Effect: 'Allow', + }, + ]; expect(() => awsPackage.mergeIamTemplates()).to.throw( - 'missing the following properties: Resource'); + 'missing the following properties: Resource' + ); }); it('should throw an error describing all problematics custom IAM policy statements', () => { @@ -270,68 +366,69 @@ describe('#mergeIamTemplates()', () => { }, ]; - expect(() => awsPackage.mergeIamTemplates()) - .to.throw(/statement 0 is missing.*Resource; statement 2 is missing.*Effect, Action/); + expect(() => awsPackage.mergeIamTemplates()).to.throw( + /statement 0 is missing.*Resource; statement 2 is missing.*Effect, Action/ + ); }); it('should throw error if managed policies is not an array', () => { awsPackage.serverless.service.provider.iamManagedPolicies = 'a string'; - expect(() => awsPackage.mergeIamTemplates()) - .to.throw('iamManagedPolicies should be an array of arns'); + expect(() => awsPackage.mergeIamTemplates()).to.throw( + 'iamManagedPolicies should be an array of arns' + ); }); it('should add a CloudWatch LogGroup resource', () => { const normalizedName = awsPackage.provider.naming.getLogGroupLogicalId(functionName); return awsPackage.mergeIamTemplates().then(() => { - expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate - .Resources[normalizedName] - ).to.deep.equal( - { - Type: 'AWS::Logs::LogGroup', - Properties: { - LogGroupName: awsPackage.provider.naming.getLogGroupName(functionName), - }, - } - ); + expect( + awsPackage.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + normalizedName + ] + ).to.deep.equal({ + Type: 'AWS::Logs::LogGroup', + Properties: { + LogGroupName: awsPackage.provider.naming.getLogGroupName(resolvedFunctionName), + }, + }); }); }); - it('should add RetentionInDays to a CloudWatch LogGroup resource if logRetentionInDays is given' - , () => { - [5, '5'].forEach((logRetentionInDays) => { + it('should add RetentionInDays to a CloudWatch LogGroup resource if logRetentionInDays is given', () => + Promise.all( + [5, '5'].map(logRetentionInDays => { awsPackage.serverless.service.provider.logRetentionInDays = logRetentionInDays; const normalizedName = awsPackage.provider.naming.getLogGroupLogicalId(functionName); return awsPackage.mergeIamTemplates().then(() => { - expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate - .Resources[normalizedName] - ).to.deep.equal( - { - Type: 'AWS::Logs::LogGroup', - Properties: { - LogGroupName: awsPackage.provider.naming.getLogGroupName(functionName), - RetentionInDays: 5, - }, - } - ); + expect( + awsPackage.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + normalizedName + ] + ).to.deep.equal({ + Type: 'AWS::Logs::LogGroup', + Properties: { + LogGroupName: awsPackage.provider.naming.getLogGroupName(resolvedFunctionName), + RetentionInDays: 5, + }, + }); }); - }); - }); + }) + )); - it('should throw error if RetentionInDays is 0 or not an integer' - , () => { - awsPackage.serverless.service.provider.logRetentionInDays = 0; - expect(() => awsPackage.mergeIamTemplates()).to.throw('should be an integer'); - awsPackage.serverless.service.provider.logRetentionInDays = 'string'; - expect(() => awsPackage.mergeIamTemplates()).to.throw('should be an integer'); - awsPackage.serverless.service.provider.logRetentionInDays = []; - expect(() => awsPackage.mergeIamTemplates()).to.throw('should be an integer'); - awsPackage.serverless.service.provider.logRetentionInDays = {}; - expect(() => awsPackage.mergeIamTemplates()).to.throw('should be an integer'); - awsPackage.serverless.service.provider.logRetentionInDays = undefined; - expect(() => awsPackage.mergeIamTemplates()).to.throw('should be an integer'); - awsPackage.serverless.service.provider.logRetentionInDays = null; - expect(() => awsPackage.mergeIamTemplates()).to.throw('should be an integer'); - }); + it('should throw error if RetentionInDays is 0 or not an integer', () => { + awsPackage.serverless.service.provider.logRetentionInDays = 0; + expect(() => awsPackage.mergeIamTemplates()).to.throw('should be an integer'); + awsPackage.serverless.service.provider.logRetentionInDays = 'string'; + expect(() => awsPackage.mergeIamTemplates()).to.throw('should be an integer'); + awsPackage.serverless.service.provider.logRetentionInDays = []; + expect(() => awsPackage.mergeIamTemplates()).to.throw('should be an integer'); + awsPackage.serverless.service.provider.logRetentionInDays = {}; + expect(() => awsPackage.mergeIamTemplates()).to.throw('should be an integer'); + awsPackage.serverless.service.provider.logRetentionInDays = undefined; + expect(() => awsPackage.mergeIamTemplates()).to.throw('should be an integer'); + awsPackage.serverless.service.provider.logRetentionInDays = null; + expect(() => awsPackage.mergeIamTemplates()).to.throw('should be an integer'); + }); it('should add a CloudWatch LogGroup resource if all functions use custom roles', () => { awsPackage.serverless.service.functions[functionName].role = 'something'; @@ -351,111 +448,26 @@ describe('#mergeIamTemplates()', () => { awsPackage.provider.naming.getLogGroupLogicalId(f.func1.name), ]; return awsPackage.mergeIamTemplates().then(() => { - expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate - .Resources[normalizedNames[0]] - ).to.deep.equal( - { - Type: 'AWS::Logs::LogGroup', - Properties: { - LogGroupName: awsPackage.provider.naming.getLogGroupName(f.func0.name), - }, - } - ); - expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate - .Resources[normalizedNames[1]] - ).to.deep.equal( - { - Type: 'AWS::Logs::LogGroup', - Properties: { - LogGroupName: awsPackage.provider.naming.getLogGroupName(f.func1.name), - }, - } - ); - }); - }); - - it('should update IamRoleLambdaExecution with a logging resource for the function', () => { - const qualifiedFunction = awsPackage.serverless.service.getFunction(functionName).name; - return awsPackage.mergeIamTemplates().then(() => { - expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate - .Resources[awsPackage.provider.naming.getRoleLogicalId()] - .Properties - .Policies[0] - .PolicyDocument - .Statement[0] - .Resource - ).to.deep.equal([ - { - 'Fn::Sub': 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:' - + `log-group:/aws/lambda/${qualifiedFunction}:*`, - }, - ]); - expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate - .Resources[awsPackage.provider.naming.getRoleLogicalId()] - .Properties - .Policies[0] - .PolicyDocument - .Statement[1] - .Resource - ).to.deep.equal([ - { - 'Fn::Sub': 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:' - + `log-group:/aws/lambda/${qualifiedFunction}:*:*`, - }, - ]); - }); - }); - - it('should update IamRoleLambdaExecution with each function\'s logging resources', () => { - awsPackage.serverless.service.functions = { - func0: { - handler: 'func.function.handler', - name: 'func0', - }, - func1: { - handler: 'func.function.handler', - name: 'func1', - }, - }; - return awsPackage.mergeIamTemplates().then(() => { - expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate - .Resources[awsPackage.provider.naming.getRoleLogicalId()] - .Properties - .Policies[0] - .PolicyDocument - .Statement[0] - .Resource - ).to.deep.equal( - [ - { - 'Fn::Sub': 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:' - + 'log-group:/aws/lambda/func0:*', - }, - { - 'Fn::Sub': 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:' - + 'log-group:/aws/lambda/func1:*', - }, + expect( + awsPackage.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + normalizedNames[0] ] - ); - expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate - .Resources[awsPackage.provider.naming.getRoleLogicalId()] - .Properties - .Policies[0] - .PolicyDocument - .Statement[1] - .Resource - ).to.deep.equal( - [ - { - 'Fn::Sub': 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:' - + 'log-group:/aws/lambda/func0:*:*', - }, - { - 'Fn::Sub': 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:' - + 'log-group:/aws/lambda/func1:*:*', - }, + ).to.deep.equal({ + Type: 'AWS::Logs::LogGroup', + Properties: { + LogGroupName: awsPackage.provider.naming.getLogGroupName(f.func0.name), + }, + }); + expect( + awsPackage.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + normalizedNames[1] ] - ); + ).to.deep.equal({ + Type: 'AWS::Logs::LogGroup', + Properties: { + LogGroupName: awsPackage.provider.naming.getLogGroupName(f.func1.name), + }, + }); }); }); @@ -473,10 +485,15 @@ describe('#mergeIamTemplates()', () => { }, }; - return awsPackage.mergeIamTemplates() - .then(() => expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate - .Resources[awsPackage.provider.naming.getRoleLogicalId()] - ).to.exist + return awsPackage + .mergeIamTemplates() + .then( + () => + expect( + awsPackage.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + awsPackage.provider.naming.getRoleLogicalId() + ] + ).to.exist ); }); @@ -493,10 +510,16 @@ describe('#mergeIamTemplates()', () => { }, }; - return awsPackage.mergeIamTemplates() - .then(() => expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate - .Resources[awsPackage.provider.naming.getRoleLogicalId()] - ).to.not.exist); + return awsPackage + .mergeIamTemplates() + .then( + () => + expect( + awsPackage.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + awsPackage.provider.naming.getRoleLogicalId() + ] + ).to.not.exist + ); }); it('should not add the default role if all functions have an ARN role', () => { @@ -513,10 +536,15 @@ describe('#mergeIamTemplates()', () => { }, }; - return awsPackage.mergeIamTemplates() - .then(() => expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate - .Resources[awsPackage.provider.naming.getRoleLogicalId()] - ).to.not.exist + return awsPackage + .mergeIamTemplates() + .then( + () => + expect( + awsPackage.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + awsPackage.provider.naming.getRoleLogicalId() + ] + ).to.not.exist ); }); @@ -529,10 +557,15 @@ describe('#mergeIamTemplates()', () => { }, }; - return awsPackage.mergeIamTemplates() - .then(() => expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate - .Resources[awsPackage.provider.naming.getRoleLogicalId()].Properties.ManagedPolicyArns - ).to.not.exist + return awsPackage + .mergeIamTemplates() + .then( + () => + expect( + awsPackage.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + awsPackage.provider.naming.getRoleLogicalId() + ].Properties.ManagedPolicyArns + ).to.not.exist ); }); @@ -542,20 +575,24 @@ describe('#mergeIamTemplates()', () => { subnetIds: ['xxx'], }; - return awsPackage.mergeIamTemplates() - .then(() => { - expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate - .Resources[awsPackage.provider.naming.getRoleLogicalId()].Properties.ManagedPolicyArns - ).to.deep.equal([{ - 'Fn::Join': ['', + return awsPackage.mergeIamTemplates().then(() => { + expect( + awsPackage.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + awsPackage.provider.naming.getRoleLogicalId() + ].Properties.ManagedPolicyArns + ).to.deep.equal([ + { + 'Fn::Join': [ + '', [ 'arn:', { Ref: 'AWS::Partition' }, ':iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole', ], ], - }]); - }); + }, + ]); + }); }); it('should be added if vpc config is defined on function level', () => { @@ -574,20 +611,24 @@ describe('#mergeIamTemplates()', () => { }, }; - return awsPackage.mergeIamTemplates() - .then(() => { - expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate - .Resources[awsPackage.provider.naming.getRoleLogicalId()].Properties.ManagedPolicyArns - ).to.deep.equal([{ - 'Fn::Join': ['', + return awsPackage.mergeIamTemplates().then(() => { + expect( + awsPackage.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + awsPackage.provider.naming.getRoleLogicalId() + ].Properties.ManagedPolicyArns + ).to.deep.equal([ + { + 'Fn::Join': [ + '', [ 'arn:', { Ref: 'AWS::Partition' }, ':iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole', ], ], - }]); - }); + }, + ]); + }); }); it('should not be added if vpc config is defined with role on function level', () => { @@ -603,10 +644,15 @@ describe('#mergeIamTemplates()', () => { }, }; - return awsPackage.mergeIamTemplates() - .then(() => expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate - .Resources[awsPackage.provider.naming.getRoleLogicalId()] - ).to.not.exist + return awsPackage + .mergeIamTemplates() + .then( + () => + expect( + awsPackage.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + awsPackage.provider.naming.getRoleLogicalId() + ] + ).to.not.exist ); }); }); diff --git a/lib/plugins/aws/package/lib/saveCompiledTemplate.js b/lib/plugins/aws/package/lib/saveCompiledTemplate.js index 17e60630e..23ddc689d 100644 --- a/lib/plugins/aws/package/lib/saveCompiledTemplate.js +++ b/lib/plugins/aws/package/lib/saveCompiledTemplate.js @@ -13,8 +13,10 @@ module.exports = { compiledTemplateFileName ); - this.serverless.utils.writeFileSync(compiledTemplateFilePath, - this.serverless.service.provider.compiledCloudFormationTemplate); + this.serverless.utils.writeFileSync( + compiledTemplateFilePath, + this.serverless.service.provider.compiledCloudFormationTemplate + ); return BbPromise.resolve(); }, diff --git a/lib/plugins/aws/package/lib/saveCompiledTemplate.test.js b/lib/plugins/aws/package/lib/saveCompiledTemplate.test.js index ad9f57371..9d3b44b42 100644 --- a/lib/plugins/aws/package/lib/saveCompiledTemplate.test.js +++ b/lib/plugins/aws/package/lib/saveCompiledTemplate.test.js @@ -27,8 +27,7 @@ describe('#saveCompiledTemplate()', () => { getCompiledTemplateFileNameStub = sinon .stub(awsPackage.provider.naming, 'getCompiledTemplateFileName') .returns('compiled.json'); - writeFileSyncStub = sinon - .stub(awsPackage.serverless.utils, 'writeFileSync').returns(); + writeFileSyncStub = sinon.stub(awsPackage.serverless.utils, 'writeFileSync').returns(); }); afterEach(() => { diff --git a/lib/plugins/aws/package/lib/saveServiceState.js b/lib/plugins/aws/package/lib/saveServiceState.js index 7d6afe2fb..ab3ab9a84 100644 --- a/lib/plugins/aws/package/lib/saveServiceState.js +++ b/lib/plugins/aws/package/lib/saveServiceState.js @@ -16,13 +16,12 @@ module.exports = { ); const artifact = _.last( - _.split( - _.get(this.serverless.service, 'package.artifact', ''), path.sep - ) + _.split(_.get(this.serverless.service, 'package.artifact', ''), path.sep) ); const strippedService = _.assign( - {}, _.omit(this.serverless.service, ['serverless', 'package']) + {}, + _.omit(this.serverless.service, ['serverless', 'package']) ); const selfReferences = findReferences(strippedService, this.serverless.service); _.forEach(selfReferences, refPath => _.set(strippedService, refPath, '${self:}')); diff --git a/lib/plugins/aws/package/lib/saveServiceState.test.js b/lib/plugins/aws/package/lib/saveServiceState.test.js index 064fb861c..41800ab2f 100644 --- a/lib/plugins/aws/package/lib/saveServiceState.test.js +++ b/lib/plugins/aws/package/lib/saveServiceState.test.js @@ -32,8 +32,7 @@ describe('#saveServiceState()', () => { getServiceStateFileNameStub = sinon .stub(awsPackage.provider.naming, 'getServiceStateFileName') .returns('service-state.json'); - writeFileSyncStub = sinon - .stub(awsPackage.serverless.utils, 'writeFileSync').returns(); + writeFileSyncStub = sinon.stub(awsPackage.serverless.utils, 'writeFileSync').returns(); }); afterEach(() => { @@ -63,8 +62,9 @@ describe('#saveServiceState()', () => { }; expect(getServiceStateFileNameStub.calledOnce).to.equal(true); - expect(writeFileSyncStub.calledWithExactly(filePath, expectedStateFileContent, true)) - .to.equal(true); + expect( + writeFileSyncStub.calledWithExactly(filePath, expectedStateFileContent, true) + ).to.equal(true); }); }); @@ -97,8 +97,9 @@ describe('#saveServiceState()', () => { }; expect(getServiceStateFileNameStub.calledOnce).to.equal(true); - expect(writeFileSyncStub.calledWithExactly(filePath, expectedStateFileContent, true)) - .to.equal(true); + expect( + writeFileSyncStub.calledWithExactly(filePath, expectedStateFileContent, true) + ).to.equal(true); }); }); }); diff --git a/lib/plugins/aws/provider/awsProvider.js b/lib/plugins/aws/provider/awsProvider.js index bcc309d7a..fecc37960 100644 --- a/lib/plugins/aws/provider/awsProvider.js +++ b/lib/plugins/aws/provider/awsProvider.js @@ -29,18 +29,18 @@ const impl = { * @param credentials The credentials to test for validity * @return {boolean} Whether the given credentials were valid */ - validCredentials: (credentials) => { + validCredentials: credentials => { let result = false; if (credentials) { if ( - ( // valid credentials loaded - credentials.accessKeyId && credentials.accessKeyId !== 'undefined' && - credentials.secretAccessKey && credentials.secretAccessKey !== 'undefined' - ) || ( - // a role to assume has been successfully loaded, the associated STS request has been - // sent, and the temporary credentials will be asynchronously delivered. - credentials.roleArn - ) + // valid credentials loaded + (credentials.accessKeyId && + credentials.accessKeyId !== 'undefined' && + credentials.secretAccessKey && + credentials.secretAccessKey !== 'undefined') || + // a role to assume has been successfully loaded, the associated STS request has been + // sent, and the temporary credentials will be asynchronously delivered. + credentials.roleArn ) { result = true; } @@ -83,16 +83,20 @@ const impl = { // Setup a MFA callback for asking the code from the user. params.tokenCodeFn = (mfaSerial, callback) => { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); - rl.question(`Enter MFA code for ${mfaSerial}: `, (answer) => { + rl.question(`Enter MFA code for ${mfaSerial}: `, answer => { rl.close(); callback(null, answer); }); }; const profileCredentials = new AWS.SharedIniFileCredentials(params); - if (!(profileCredentials.accessKeyId - || profileCredentials.sessionToken - || profileCredentials.roleArn)) { + if ( + !( + profileCredentials.accessKeyId || + profileCredentials.sessionToken || + profileCredentials.roleArn + ) + ) { throw new Error(`Profile ${profile} does not exist`); } @@ -124,6 +128,9 @@ class AwsProvider { this.serverless = serverless; this.sdk = AWS; this.serverless.setProvider(constants.providerName, this); + if (this.serverless.service.provider.name === 'aws') { + this.serverless.service.provider.region = this.getRegion(); + } this.requestCache = {}; this.requestQueue = new PromiseQueue(2, Infinity); // Store credentials in this variable to avoid creating them several times (messes up MFA). @@ -137,20 +144,19 @@ class AwsProvider { } // Use HTTPS Proxy (Optional) - const proxy = process.env.proxy - || process.env.HTTP_PROXY - || process.env.http_proxy - || process.env.HTTPS_PROXY - || process.env.https_proxy; + const proxy = + process.env.proxy || + process.env.HTTP_PROXY || + process.env.http_proxy || + process.env.HTTPS_PROXY || + process.env.https_proxy; const proxyOptions = {}; if (proxy) { Object.assign(proxyOptions, url.parse(proxy)); } - const ca = process.env.ca - || process.env.HTTPS_CA - || process.env.https_ca; + const ca = process.env.ca || process.env.HTTPS_CA || process.env.https_ca; let caCerts = []; @@ -161,9 +167,7 @@ class AwsProvider { caCerts = caCerts.concat(caArr.map(cert => cert.replace(/\\n/g, '\n'))); } - const cafile = process.env.cafile - || process.env.HTTPS_CAFILE - || process.env.https_cafile; + const cafile = process.env.cafile || process.env.HTTPS_CAFILE || process.env.https_cafile; if (cafile) { // Can be a single certificate file path or multiple paths, comma separated. @@ -228,27 +232,33 @@ class AwsProvider { }); const paramsHash = objectHash.sha1(paramsWithRegion); const MAX_TRIES = 4; - const persistentRequest = (f) => new BbPromise((resolve, reject) => { - const doCall = (numTry) => { - f() - // We're resembling if/else logic, therefore single `then` instead of `then`/`catch` pair - .then(resolve, e => { - if (numTry < MAX_TRIES && - ((e.providerError && e.providerError.retryable) || e.statusCode === 429)) { - that.serverless.cli.log( - _.join([ - `Recoverable error occurred (${e.message}), sleeping for 5 seconds.`, - `Try ${numTry + 1} of ${MAX_TRIES}`, - ], ' ') - ); - setTimeout(doCall, 5000, numTry + 1); - } else { - reject(e); - } - }); - }; - return doCall(0); - }); + const persistentRequest = f => + new BbPromise((resolve, reject) => { + const doCall = numTry => { + f() + // We're resembling if/else logic, therefore single `then` instead of `then`/`catch` pair + .then(resolve, e => { + if ( + numTry < MAX_TRIES && + ((e.providerError && e.providerError.retryable) || e.statusCode === 429) + ) { + that.serverless.cli.log( + _.join( + [ + `Recoverable error occurred (${e.message}), sleeping for 5 seconds.`, + `Try ${numTry + 1} of ${MAX_TRIES}`, + ], + ' ' + ) + ); + setTimeout(doCall, 5000, numTry + 1); + } else { + reject(e); + } + }); + }; + return doCall(0); + }); // Emit a warning for misuses of the old signature including stage and region // TODO: Determine calling module and log that @@ -268,49 +278,54 @@ class AwsProvider { } } - const request = this.requestQueue.add(() => persistentRequest(() => { - if (options && !_.isUndefined(options.region)) { - credentials.region = options.region; - } - const awsService = new that.sdk[service](credentials); - const req = awsService[method](params); - - // TODO: Add listeners, put Debug statments here... - // req.on('send', function (r) {console.log(r)}); - - const promise = req.promise ? req.promise() : BbPromise.fromCallback(cb => { - req.send(cb); - }); - return promise.catch(err => { - let message = err.message !== null ? err.message : err.code; - if (err.message === 'Missing credentials in config') { - const errorMessage = [ - 'AWS provider credentials not found.', - ' Learn how to set up AWS provider credentials', - ` in our docs here: <${chalk.green('http://bit.ly/aws-creds-setup')}>.`, - ].join(''); - message = errorMessage; - userStats.track('user_awsCredentialsNotFound'); - // We do not want to trigger the retry mechanism for credential errors - return BbPromise.reject(Object.assign( - new this.serverless.classes.Error(message, err.statusCode), - { providerError: _.assign({}, err, { retryable: false }) } - )); + const request = this.requestQueue.add(() => + persistentRequest(() => { + if (options && !_.isUndefined(options.region)) { + credentials.region = options.region; } + const awsService = new that.sdk[service](credentials); + const req = awsService[method](params); - return BbPromise.reject(Object.assign( - new this.serverless.classes.Error(message, err.statusCode), - { providerError: err } - )); - }); - }) - .then(data => { + // TODO: Add listeners, put Debug statments here... + // req.on('send', function (r) {console.log(r)}); + + const promise = req.promise + ? req.promise() + : BbPromise.fromCallback(cb => { + req.send(cb); + }); + return promise.catch(err => { + let message = err.message !== null ? err.message : err.code; + if (err.message === 'Missing credentials in config') { + const errorMessage = [ + 'AWS provider credentials not found.', + ' Learn how to set up AWS provider credentials', + ` in our docs here: <${chalk.green('http://bit.ly/aws-creds-setup')}>.`, + ].join(''); + message = errorMessage; + userStats.track('user_awsCredentialsNotFound'); + // We do not want to trigger the retry mechanism for credential errors + return BbPromise.reject( + Object.assign(new this.serverless.classes.Error(message, err.statusCode), { + providerError: _.assign({}, err, { retryable: false }), + }) + ); + } + + return BbPromise.reject( + Object.assign(new this.serverless.classes.Error(message, err.statusCode), { + providerError: err, + }) + ); + }); + }).then(data => { const result = BbPromise.resolve(data); if (shouldCache) { _.set(this.requestCache, `${service}.${method}.${paramsHash}`, result); } return result; - })); + }) + ); if (shouldCache) { _.set(this.requestCache, `${service}.${method}.${paramsHash}`, request); @@ -348,8 +363,11 @@ class AwsProvider { result.region = this.getRegion(); const deploymentBucketObject = this.serverless.service.provider.deploymentBucketObject; - if (deploymentBucketObject && deploymentBucketObject.serverSideEncryption - && deploymentBucketObject.serverSideEncryption === 'aws:kms') { + if ( + deploymentBucketObject && + deploymentBucketObject.serverSideEncryption && + deploymentBucketObject.serverSideEncryption === 'aws:kms' + ) { result.signatureVersion = 'v4'; } @@ -360,9 +378,11 @@ class AwsProvider { canUseS3TransferAcceleration(service, method) { // TODO enable more S3 APIs? - return service === 'S3' - && ['upload', 'putObject'].indexOf(method) !== -1 - && this.isS3TransferAccelerationEnabled(); + return ( + service === 'S3' && + ['upload', 'putObject'].indexOf(method) !== -1 && + this.isS3TransferAccelerationEnabled() + ); } // This function will be used to block the addition of transfer acceleration options @@ -388,7 +408,7 @@ class AwsProvider { enableS3TransferAcceleration(credentials) { this.serverless.cli.log('Using S3 Transfer Acceleration Endpoint...'); - credentials.useAccelerateEndpoint = true; // eslint-disable-line no-param-reassign + credentials.useAccelerateEndpoint = true; // eslint-disable-line no-param-reassign } getValues(source, paths) { @@ -433,13 +453,10 @@ class AwsProvider { if (this.serverless.service.provider.deploymentBucket) { return BbPromise.resolve(this.serverless.service.provider.deploymentBucket); } - return this.request('CloudFormation', - 'describeStackResource', - { - StackName: this.naming.getStackName(), - LogicalResourceId: this.naming.getDeploymentBucketLogicalId(), - } - ).then((result) => result.StackResourceDetail.PhysicalResourceId); + return this.request('CloudFormation', 'describeStackResource', { + StackName: this.naming.getStackName(), + LogicalResourceId: this.naming.getDeploymentBucketLogicalId(), + }).then(result => result.StackResourceDetail.PhysicalResourceId); } getDeploymentPrefix() { @@ -461,31 +478,31 @@ class AwsProvider { } getAccountId() { - return this.getAccountInfo() - .then((result) => result.accountId); + return this.getAccountInfo().then(result => result.accountId); } getAccountInfo() { - return this.request('STS', 'getCallerIdentity', {}) - .then((result) => { - const arn = result.Arn; - const accountId = result.Account; - const partition = _.nth(_.split(arn, ':'), 1); // ex: arn:aws:iam:acctId:user/xyz - return { - accountId, - partition, - arn: result.Arn, - userId: result.UserId, - }; - }); + return this.request('STS', 'getCallerIdentity', {}).then(result => { + const arn = result.Arn; + const accountId = result.Account; + const partition = _.nth(_.split(arn, ':'), 1); // ex: arn:aws:iam:acctId:user/xyz + return { + accountId, + partition, + arn: result.Arn, + userId: result.UserId, + }; + }); } /** * Get API Gateway Rest API ID from serverless config */ getApiGatewayRestApiId() { - if (this.serverless.service.provider.apiGateway - && this.serverless.service.provider.apiGateway.restApiId) { + if ( + this.serverless.service.provider.apiGateway && + this.serverless.service.provider.apiGateway.restApiId + ) { return this.serverless.service.provider.apiGateway.restApiId; } @@ -493,8 +510,10 @@ class AwsProvider { } getApiGatewayDescription() { - if (this.serverless.service.provider.apiGateway - && this.serverless.service.provider.apiGateway.description) { + if ( + this.serverless.service.provider.apiGateway && + this.serverless.service.provider.apiGateway.description + ) { return this.serverless.service.provider.apiGateway.description; } return undefined; @@ -513,8 +532,10 @@ class AwsProvider { * Get Rest API Root Resource ID from serverless config */ getApiGatewayRestApiRootResourceId() { - if (this.serverless.service.provider.apiGateway - && this.serverless.service.provider.apiGateway.restApiRootResourceId) { + if ( + this.serverless.service.provider.apiGateway && + this.serverless.service.provider.apiGateway.restApiRootResourceId + ) { return this.serverless.service.provider.apiGateway.restApiRootResourceId; } return { 'Fn::GetAtt': [this.naming.getRestApiLogicalId(), 'RootResourceId'] }; @@ -524,8 +545,10 @@ class AwsProvider { * Get Rest API Predefined Resources from serverless config */ getApiGatewayPredefinedResources() { - if (!this.serverless.service.provider.apiGateway - || !this.serverless.service.provider.apiGateway.restApiResources) { + if ( + !this.serverless.service.provider.apiGateway || + !this.serverless.service.provider.apiGateway.restApiResources + ) { return []; } @@ -537,11 +560,24 @@ class AwsProvider { throw new Error('REST API resource must be an array of object'); } - return Object.keys(this.serverless.service.provider.apiGateway.restApiResources) - .map((key) => ({ - path: key, - resourceId: this.serverless.service.provider.apiGateway.restApiResources[key], - })); + return Object.keys(this.serverless.service.provider.apiGateway.restApiResources).map(key => ({ + path: key, + resourceId: this.serverless.service.provider.apiGateway.restApiResources[key], + })); + } + + /** + * Get API Gateway websocket API ID from serverless config + */ + getApiGatewayWebsocketApiId() { + if ( + this.serverless.service.provider.apiGateway && + this.serverless.service.provider.apiGateway.websocketApiId + ) { + return this.serverless.service.provider.apiGateway.websocketApiId; + } + + return { Ref: this.naming.getWebsocketsApiLogicalId() }; } getStackResources(next, resourcesParam) { @@ -552,10 +588,7 @@ class AwsProvider { if (!resources) resources = []; if (next) params.NextToken = next; - return this.request('CloudFormation', - 'listStackResources', - params - ).then((res) => { + return this.request('CloudFormation', 'listStackResources', params).then(res => { const allResources = resources.concat(res.StackResourceSummaries); if (!res.NextToken) { return allResources; diff --git a/lib/plugins/aws/provider/awsProvider.test.js b/lib/plugins/aws/provider/awsProvider.test.js index 1cf09dbf2..593482273 100644 --- a/lib/plugins/aws/provider/awsProvider.test.js +++ b/lib/plugins/aws/provider/awsProvider.test.js @@ -10,10 +10,12 @@ const sinon = require('sinon'); const fs = require('fs'); const os = require('os'); const path = require('path'); +const overrideEnv = require('process-utils/override-env'); const AwsProvider = require('./awsProvider'); const Serverless = require('../../../Serverless'); -const testUtils = require('../../../../tests/utils'); +const { replaceEnv } = require('../../../../tests/utils/misc'); +const { getTmpFilePath } = require('../../../../tests/utils/fs'); chai.use(require('chai-as-promised')); chai.use(require('sinon-chai')); @@ -23,16 +25,19 @@ const expect = chai.expect; describe('AwsProvider', () => { let awsProvider; let serverless; + let restoreEnv; const options = { stage: 'dev', region: 'us-east-1', }; beforeEach(() => { + ({ restoreEnv } = overrideEnv()); serverless = new Serverless(options); + serverless.cli = new serverless.classes.CLI(); awsProvider = new AwsProvider(serverless, options); - awsProvider.serverless.cli = new serverless.classes.CLI(); }); + afterEach(() => restoreEnv()); describe('#getProviderName()', () => { it('should return the provider name', () => { @@ -41,11 +46,6 @@ describe('AwsProvider', () => { }); describe('#constructor()', () => { - afterEach('Environment Variable Cleanup', () => { - // clear env - delete process.env.proxy; - }); - it('should set Serverless instance', () => { expect(typeof awsProvider.serverless).to.not.equal('undefined'); }); @@ -59,18 +59,14 @@ describe('AwsProvider', () => { }); it('should have no AWS logger', () => { - expect(awsProvider.sdk.config.logger).to.be.undefined; + expect(awsProvider.sdk.config.logger).to.be.null; }); it('should set AWS logger', () => { - const BK_SLS_DEBUG = process.env.SLS_DEBUG; process.env.SLS_DEBUG = 'true'; const newAwsProvider = new AwsProvider(serverless, options); expect(typeof newAwsProvider.sdk.config.logger).to.not.equal('undefined'); - - // reset env - process.env.SLS_DEBUG = BK_SLS_DEBUG; }); it('should set AWS proxy', () => { @@ -85,18 +81,10 @@ describe('AwsProvider', () => { const newAwsProvider = new AwsProvider(serverless, options); expect(typeof newAwsProvider.sdk.config.httpOptions.timeout).to.not.equal('undefined'); - - // clear env - delete process.env.AWS_CLIENT_TIMEOUT; }); describe('stage name validation', () => { - const stages = [ - 'myStage', - 'my-stage', - 'my_stage', - '${opt:stage, \'prod\'}', - ]; + const stages = ['myStage', 'my-stage', 'my_stage', "${opt:stage, 'prod'}"]; stages.forEach(stage => { it(`should not throw an error before variable population even if http event is present and stage is ${stage}`, () => { @@ -131,10 +119,6 @@ describe('AwsProvider', () => { }); describe('certificate authority - environment variable', () => { - afterEach('Environment Variable Cleanup', () => { - // clear env - delete process.env.ca; - }); it('should set AWS ca single', () => { process.env.ca = '-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----'; const newAwsProvider = new AwsProvider(serverless, options); @@ -165,6 +149,7 @@ describe('AwsProvider', () => { const tmpdir = os.tmpdir(); let file1 = null; let file2 = null; + beforeEach('Create CA Files and env vars', () => { file1 = path.join(tmpdir, 'ca1.txt'); file2 = path.join(tmpdir, 'ca2.txt'); @@ -176,9 +161,6 @@ describe('AwsProvider', () => { // delete files fs.unlinkSync(file1); fs.unlinkSync(file2); - // clear env - delete process.env.ca; - delete process.env.cafile; }); it('should set AWS cafile single', () => { @@ -218,8 +200,9 @@ describe('AwsProvider', () => { const newAwsProvider = new AwsProvider(serverless, options); - expect(newAwsProvider.serverless.service.provider.deploymentBucket) - .to.equal('my.deployment.bucket'); + expect(newAwsProvider.serverless.service.provider.deploymentBucket).to.equal( + 'my.deployment.bucket' + ); }); it('should save a given object and use name from it', () => { @@ -231,10 +214,12 @@ describe('AwsProvider', () => { const newAwsProvider = new AwsProvider(serverless, options); - expect(newAwsProvider.serverless.service.provider.deploymentBucket) - .to.equal('my.deployment.bucket'); - expect(newAwsProvider.serverless.service.provider.deploymentBucketObject) - .to.deep.equal(deploymentBucketObject); + expect(newAwsProvider.serverless.service.provider.deploymentBucket).to.equal( + 'my.deployment.bucket' + ); + expect(newAwsProvider.serverless.service.provider.deploymentBucketObject).to.deep.equal( + deploymentBucketObject + ); }); it('should save a given object and nullify the name if one is not provided', () => { @@ -245,17 +230,20 @@ describe('AwsProvider', () => { const newAwsProvider = new AwsProvider(serverless, options); - expect(newAwsProvider.serverless.service.provider.deploymentBucket) - .to.equal(null); - expect(newAwsProvider.serverless.service.provider.deploymentBucketObject) - .to.deep.equal(deploymentBucketObject); + expect(newAwsProvider.serverless.service.provider.deploymentBucket).to.equal(null); + expect(newAwsProvider.serverless.service.provider.deploymentBucketObject).to.deep.equal( + deploymentBucketObject + ); }); }); }); describe('#request()', () => { beforeEach(() => { - sinon.stub(global, 'setTimeout', (cb) => { cb(); }); + const originalSetTimeout = setTimeout; + sinon + .stub(global, 'setTimeout') + .callsFake((cb, timeout) => originalSetTimeout(cb, Math.min(timeout || 0, 10))); }); afterEach(() => { @@ -271,7 +259,7 @@ describe('AwsProvider', () => { putObject() { return { - send: (cb) => cb(null, { called: true }), + send: cb => cb(null, { called: true }), }; } } @@ -328,7 +316,6 @@ describe('AwsProvider', () => { }); }); - it('should request to the specified region if region in options set', () => { // mocking S3 for testing class FakeCloudForamtion { @@ -338,9 +325,10 @@ describe('AwsProvider', () => { describeStacks() { return { - send: (cb) => cb(null, { - region: this.config.region, - }), + send: cb => + cb(null, { + region: this.config.region, + }), }; } } @@ -361,10 +349,12 @@ describe('AwsProvider', () => { expect(awsProvider.getCredentials().region).to.eql(options.region); return awsProvider - .request('CloudFormation', + .request( + 'CloudFormation', 'describeStacks', { StackName: 'foo' }, - { region: 'ap-northeast-1' }) + { region: 'ap-northeast-1' } + ) .then(data => { expect(data).to.eql({ region: 'ap-northeast-1' }); // Requesting different region should not affect region in credentials @@ -372,7 +362,7 @@ describe('AwsProvider', () => { }); }); - it('should retry if error code is 429', (done) => { + it('should retry if error code is 429', done => { const error = { statusCode: 429, retryable: true, @@ -395,7 +385,8 @@ describe('AwsProvider', () => { awsProvider.sdk = { S3: FakeS3, }; - awsProvider.request('S3', 'error', {}) + awsProvider + .request('S3', 'error', {}) .then(data => { expect(data).to.exist; expect(sendFake.send).to.have.been.calledTwice; @@ -404,7 +395,7 @@ describe('AwsProvider', () => { .catch(done); }); - it('should retry if error code is 429 and retryable is set to false', (done) => { + it('should retry if error code is 429 and retryable is set to false', done => { const error = { statusCode: 429, retryable: false, @@ -427,7 +418,8 @@ describe('AwsProvider', () => { awsProvider.sdk = { S3: FakeS3, }; - awsProvider.request('S3', 'error', {}) + awsProvider + .request('S3', 'error', {}) .then(data => { expect(data).to.exist; expect(sendFake.send).to.have.been.calledTwice; @@ -436,7 +428,7 @@ describe('AwsProvider', () => { .catch(done); }); - it('should reject errors', (done) => { + it('should reject errors', done => { const error = { statusCode: 500, message: 'Some error message', @@ -457,12 +449,13 @@ describe('AwsProvider', () => { awsProvider.sdk = { S3: FakeS3, }; - awsProvider.request('S3', 'error', {}) + awsProvider + .request('S3', 'error', {}) .then(() => done('Should not succeed')) .catch(() => done()); }); - it('should use error message if it exists', (done) => { + it('should use error message if it exists', done => { const awsErrorResponse = { message: 'Something went wrong...', code: 'Forbidden', @@ -491,16 +484,17 @@ describe('AwsProvider', () => { awsProvider.sdk = { S3: FakeS3, }; - awsProvider.request('S3', 'error', {}) + awsProvider + .request('S3', 'error', {}) .then(() => done('Should not succeed')) - .catch((err) => { + .catch(err => { expect(err.message).to.eql(awsErrorResponse.message); done(); }) .catch(done); }); - it('should default to error code if error message is non-existent', (done) => { + it('should default to error code if error message is non-existent', done => { const awsErrorResponse = { message: null, code: 'Forbidden', @@ -529,16 +523,17 @@ describe('AwsProvider', () => { awsProvider.sdk = { S3: FakeS3, }; - awsProvider.request('S3', 'error', {}) + awsProvider + .request('S3', 'error', {}) .then(() => done('Should not succeed')) - .catch((err) => { + .catch(err => { expect(err.message).to.eql(awsErrorResponse.code); done(); }) .catch(done); }); - it('should return ref to docs for missing credentials', (done) => { + it('should return ref to docs for missing credentials', done => { const error = { statusCode: 403, message: 'Missing credentials in config', @@ -559,16 +554,17 @@ describe('AwsProvider', () => { awsProvider.sdk = { S3: FakeS3, }; - awsProvider.request('S3', 'error', {}) + awsProvider + .request('S3', 'error', {}) .then(() => done('Should not succeed')) - .catch((err) => { + .catch(err => { expect(err.message).to.contain('in our docs here:'); done(); }) .catch(done); }); - it('should not retry for missing credentials', (done) => { + it('should not retry for missing credentials', done => { const error = { statusCode: 403, message: 'Missing credentials in config', @@ -588,9 +584,10 @@ describe('AwsProvider', () => { awsProvider.sdk = { S3: FakeS3, }; - awsProvider.request('S3', 'error', {}) + awsProvider + .request('S3', 'error', {}) .then(() => done('Should not succeed')) - .catch((err) => { + .catch(err => { expect(sendFake.send).to.have.been.calledOnce; expect(err.message).to.contain('in our docs here:'); done(); @@ -607,7 +604,7 @@ describe('AwsProvider', () => { putObject() { return { - send: (cb) => cb(null, { called: true }), + send: cb => cb(null, { called: true }), }; } } @@ -627,7 +624,8 @@ describe('AwsProvider', () => { }; const enableS3TransferAccelerationStub = sinon - .stub(awsProvider, 'enableS3TransferAcceleration').resolves(); + .stub(awsProvider, 'enableS3TransferAcceleration') + .resolves(); awsProvider.options['aws-s3-accelerate'] = true; return awsProvider.request('S3', 'putObject', {}).then(() => { @@ -645,7 +643,7 @@ describe('AwsProvider', () => { describeStacks() { return { - send: (cb) => cb(null, { called: true }), + send: cb => cb(null, { called: true }), }; } } @@ -664,12 +662,8 @@ describe('AwsProvider', () => { }, }; - return awsProvider.request( - 'CloudFormation', - 'describeStacks', - {}, - { useCache: true } - ) + return awsProvider + .request('CloudFormation', 'describeStacks', {}, { useCache: true }) .then(data => { expect(data.called).to.equal(true); }); @@ -693,15 +687,16 @@ describe('AwsProvider', () => { awsProvider.sdk = { CloudFormation: FakeCF, }; - const executeRequestWithRegion = (region) => awsProvider.request( - 'CloudFormation', - 'describeStacks', - { StackName: 'same-stack' }, - { - useCache: true, - region, - } - ); + const executeRequestWithRegion = region => + awsProvider.request( + 'CloudFormation', + 'describeStacks', + { StackName: 'same-stack' }, + { + useCache: true, + region, + } + ); const requests = []; requests.push(BbPromise.try(() => executeRequestWithRegion('us-east-1'))); requests.push(BbPromise.try(() => executeRequestWithRegion('ap-northeast-1'))); @@ -751,12 +746,8 @@ describe('AwsProvider', () => { }; const numTests = 1000; - const executeRequest = () => awsProvider.request( - 'CloudFormation', - 'describeStacks', - {}, - { useCache: true } - ); + const executeRequest = () => + awsProvider.request('CloudFormation', 'describeStacks', {}, { useCache: true }); const requests = []; for (let n = 0; n < numTests; n++) { requests.push(BbPromise.try(() => executeRequest())); @@ -783,27 +774,27 @@ describe('AwsProvider', () => { let originalProviderProfile; let originalEnvironmentVariables; const relevantEnvironment = { - AWS_SHARED_CREDENTIALS_FILE: testUtils.getTmpFilePath('credentials'), + AWS_SHARED_CREDENTIALS_FILE: getTmpFilePath('credentials'), }; beforeEach(() => { originalProviderProfile = serverless.service.provider.profile; - originalEnvironmentVariables = testUtils.replaceEnv(relevantEnvironment); + originalEnvironmentVariables = replaceEnv(relevantEnvironment); serverless.utils.writeFileSync( relevantEnvironment.AWS_SHARED_CREDENTIALS_FILE, '[default]\n' + - 'aws_access_key_id = 1111\n' + - 'aws_secret_access_key = 22222\n' + - '\n' + - '[async]\n' + - 'role_arn = arn:123\n' + - 'source_profile = default' + 'aws_access_key_id = 1111\n' + + 'aws_secret_access_key = 22222\n' + + '\n' + + '[async]\n' + + 'role_arn = arn:123\n' + + 'source_profile = default' ); newAwsProvider = new AwsProvider(serverless, options); }); afterEach(() => { - testUtils.replaceEnv(originalEnvironmentVariables); + replaceEnv(originalEnvironmentVariables); serverless.service.provider.profile = originalProviderProfile; }); @@ -823,7 +814,7 @@ describe('AwsProvider', () => { describeStacks() { return { - send: (cb) => cb(null, {}), + send: cb => cb(null, {}), }; } } @@ -833,10 +824,12 @@ describe('AwsProvider', () => { }; return newAwsProvider - .request('CloudFormation', + .request( + 'CloudFormation', 'describeStacks', { StackName: 'foo' }, - { region: 'ap-northeast-1' }) + { region: 'ap-northeast-1' } + ) .then(() => { // STS token is resolved after SDK call const actualToken = newAwsProvider.getCredentials().credentials.sessionToken; @@ -853,7 +846,6 @@ describe('AwsProvider', () => { 'aws-sdk': awsStub, }); - // add environment variables here if you want them cleared prior to your test and restored // after it has completed. Any environment variable that might alter credentials loading // should be added here @@ -864,7 +856,7 @@ describe('AwsProvider', () => { AWS_TESTSTAGE_ACCESS_KEY_ID: 'undefined', AWS_TESTSTAGE_SECRET_ACCESS_KEY: 'undefined', AWS_TESTSTAGE_SESSION_TOKEN: 'undefined', - AWS_SHARED_CREDENTIALS_FILE: testUtils.getTmpFilePath('credentials'), + AWS_SHARED_CREDENTIALS_FILE: getTmpFilePath('credentials'), AWS_PROFILE: 'undefined', AWS_TESTSTAGE_PROFILE: 'undefined', }; @@ -889,28 +881,28 @@ describe('AwsProvider', () => { beforeEach(() => { originalProviderCredentials = serverless.service.provider.credentials; originalProviderProfile = serverless.service.provider.profile; - originalEnvironmentVariables = testUtils.replaceEnv(relevantEnvironment); + originalEnvironmentVariables = replaceEnv(relevantEnvironment); // make temporary credentials file serverless.utils.writeFileSync( relevantEnvironment.AWS_SHARED_CREDENTIALS_FILE, '[notDefault]\n' + - `aws_access_key_id = ${fakeCredentials.accessKeyId}\n` + - `aws_secret_access_key = ${fakeCredentials.secretAccessKey}\n` + - '\n' + - '[notDefaultTemporary]\n' + - `aws_access_key_id = ${fakeCredentials.accessKeyId}\n` + - `aws_secret_access_key = ${fakeCredentials.secretAccessKey}\n` + - `aws_session_token = ${fakeCredentials.sessionToken}\n` + - '\n' + - '[notDefaultAsync]\n' + - `role_arn = ${fakeCredentials.roleArn}\n` + - `source_profile = ${fakeCredentials.sourceProfile}\n` + `aws_access_key_id = ${fakeCredentials.accessKeyId}\n` + + `aws_secret_access_key = ${fakeCredentials.secretAccessKey}\n` + + '\n' + + '[notDefaultTemporary]\n' + + `aws_access_key_id = ${fakeCredentials.accessKeyId}\n` + + `aws_secret_access_key = ${fakeCredentials.secretAccessKey}\n` + + `aws_session_token = ${fakeCredentials.sessionToken}\n` + + '\n' + + '[notDefaultAsync]\n' + + `role_arn = ${fakeCredentials.roleArn}\n` + + `source_profile = ${fakeCredentials.sourceProfile}\n` ); newAwsProvider = new AwsProviderProxyquired(serverless, newOptions); }); afterEach(() => { - testUtils.replaceEnv(originalEnvironmentVariables); + replaceEnv(originalEnvironmentVariables); serverless.service.provider.profile = originalProviderProfile; serverless.service.provider.credentials = originalProviderCredentials; }); @@ -1014,6 +1006,7 @@ describe('AwsProvider', () => { process.env.AWS_ACCESS_KEY_ID = testVal.accessKeyId; process.env.AWS_SECRET_ACCESS_KEY = testVal.secretAccessKey; process.env.AWS_SESSION_TOKEN = testVal.sessionToken; + const credentials = newAwsProvider.getCredentials(); expect(credentials.credentials.accessKeyId).to.equal(testVal.accessKeyId); expect(credentials.credentials.secretAccessKey).to.equal(testVal.secretAccessKey); @@ -1029,6 +1022,7 @@ describe('AwsProvider', () => { process.env.AWS_TESTSTAGE_ACCESS_KEY_ID = testVal.accessKeyId; process.env.AWS_TESTSTAGE_SECRET_ACCESS_KEY = testVal.secretAccessKey; process.env.AWS_TESTSTAGE_SESSION_TOKEN = testVal.sessionToken; + const credentials = newAwsProvider.getCredentials(); expect(credentials.credentials.accessKeyId).to.equal(testVal.accessKeyId); expect(credentials.credentials.secretAccessKey).to.equal(testVal.secretAccessKey); @@ -1055,7 +1049,8 @@ describe('AwsProvider', () => { expect(credentials.credentials.profile).to.equal('notDefault'); }); - it('should get credentials when profile is provied via --aws-profile option even if profile is defined in serverless.yml', () => { // eslint-disable-line max-len + it('should get credentials when profile is provied via --aws-profile option even if profile is defined in serverless.yml', () => { + // eslint-disable-line max-len process.env.AWS_PROFILE = 'notDefaultTemporary'; newAwsProvider.options['aws-profile'] = 'notDefault'; @@ -1065,7 +1060,8 @@ describe('AwsProvider', () => { expect(credentials.credentials.profile).to.equal('notDefault'); }); - it('should get credentials when profile is provied via process.env.AWS_PROFILE even if profile is defined in serverless.yml', () => { // eslint-disable-line max-len + it('should get credentials when profile is provied via process.env.AWS_PROFILE even if profile is defined in serverless.yml', () => { + // eslint-disable-line max-len process.env.AWS_PROFILE = 'notDefault'; serverless.service.provider.profile = 'notDefaultTemporary'; @@ -1094,11 +1090,7 @@ describe('AwsProvider', () => { }, }, }; - const paths = [ - ['a'], - ['c', 'd'], - ['c', 'f', 'g'], - ]; + const paths = [['a'], ['c', 'd'], ['c', 'f', 'g']]; const getExpected = [ { path: paths[0], value: obj.a }, { path: paths[1], value: obj.c.d }, @@ -1110,13 +1102,13 @@ describe('AwsProvider', () => { }); }); describe('#firstValue', () => { - it('should ignore entries without a \'value\' attribute', () => { + it("should ignore entries without a 'value' attribute", () => { const input = _.cloneDeep(getExpected); delete input[0].value; delete input[2].value; expect(awsProvider.firstValue(input)).to.eql(getExpected[1]); }); - it('should ignore entries with an undefined \'value\' attribute', () => { + it("should ignore entries with an undefined 'value' attribute", () => { const input = _.cloneDeep(getExpected); input[0].value = undefined; input[2].value = undefined; @@ -1238,47 +1230,39 @@ describe('AwsProvider', () => { describe('#getServerlessDeploymentBucketName()', () => { it('should return the name of the serverless deployment bucket', () => { - const describeStackResourcesStub = sinon - .stub(awsProvider, 'request') - .resolves({ - StackResourceDetail: { - PhysicalResourceId: 'serverlessDeploymentBucketName', - }, - }); + const describeStackResourcesStub = sinon.stub(awsProvider, 'request').resolves({ + StackResourceDetail: { + PhysicalResourceId: 'serverlessDeploymentBucketName', + }, + }); - return awsProvider.getServerlessDeploymentBucketName() - .then((bucketName) => { - expect(bucketName).to.equal('serverlessDeploymentBucketName'); - expect(describeStackResourcesStub.calledOnce).to.be.equal(true); - expect(describeStackResourcesStub.calledWithExactly( - 'CloudFormation', - 'describeStackResource', - { - StackName: awsProvider.naming.getStackName(), - LogicalResourceId: awsProvider.naming.getDeploymentBucketLogicalId(), - } - )).to.be.equal(true); - awsProvider.request.restore(); - }); + return awsProvider.getServerlessDeploymentBucketName().then(bucketName => { + expect(bucketName).to.equal('serverlessDeploymentBucketName'); + expect(describeStackResourcesStub.calledOnce).to.be.equal(true); + expect( + describeStackResourcesStub.calledWithExactly('CloudFormation', 'describeStackResource', { + StackName: awsProvider.naming.getStackName(), + LogicalResourceId: awsProvider.naming.getDeploymentBucketLogicalId(), + }) + ).to.be.equal(true); + awsProvider.request.restore(); + }); }); it('should return the name of the custom deployment bucket', () => { awsProvider.serverless.service.provider.deploymentBucket = 'custom-bucket'; - const describeStackResourcesStub = sinon - .stub(awsProvider, 'request') - .resolves({ - StackResourceDetail: { - PhysicalResourceId: 'serverlessDeploymentBucketName', - }, - }); + const describeStackResourcesStub = sinon.stub(awsProvider, 'request').resolves({ + StackResourceDetail: { + PhysicalResourceId: 'serverlessDeploymentBucketName', + }, + }); - return awsProvider.getServerlessDeploymentBucketName() - .then((bucketName) => { - expect(describeStackResourcesStub.called).to.be.equal(false); - expect(bucketName).to.equal('custom-bucket'); - awsProvider.request.restore(); - }); + return awsProvider.getServerlessDeploymentBucketName().then(bucketName => { + expect(describeStackResourcesStub.called).to.be.equal(false); + expect(bucketName).to.equal('custom-bucket'); + awsProvider.request.restore(); + }); }); }); @@ -1286,8 +1270,9 @@ describe('AwsProvider', () => { it('should return custom deployment prefix if defined', () => { serverless.service.provider.deploymentPrefix = 'providerPrefix'; - expect(awsProvider.getDeploymentPrefix()) - .to.equal(serverless.service.provider.deploymentPrefix); + expect(awsProvider.getDeploymentPrefix()).to.equal( + serverless.service.provider.deploymentPrefix + ); }); it('should use the default serverless if not defined', () => { @@ -1349,22 +1334,19 @@ describe('AwsProvider', () => { const accountId = '12345678'; const partition = 'aws'; - const stsGetCallerIdentityStub = sinon - .stub(awsProvider, 'request') - .resolves({ - ResponseMetadata: { RequestId: '12345678-1234-1234-1234-123456789012' }, - UserId: 'ABCDEFGHIJKLMNOPQRSTU:VWXYZ', - Account: accountId, - Arn: 'arn:aws:sts::123456789012:assumed-role/ROLE-NAME/VWXYZ', - }); + const stsGetCallerIdentityStub = sinon.stub(awsProvider, 'request').resolves({ + ResponseMetadata: { RequestId: '12345678-1234-1234-1234-123456789012' }, + UserId: 'ABCDEFGHIJKLMNOPQRSTU:VWXYZ', + Account: accountId, + Arn: 'arn:aws:sts::123456789012:assumed-role/ROLE-NAME/VWXYZ', + }); - return awsProvider.getAccountInfo() - .then((result) => { - expect(stsGetCallerIdentityStub.calledOnce).to.equal(true); - expect(result.accountId).to.equal(accountId); - expect(result.partition).to.equal(partition); - awsProvider.request.restore(); - }); + return awsProvider.getAccountInfo().then(result => { + expect(stsGetCallerIdentityStub.calledOnce).to.equal(true); + expect(result.accountId).to.equal(accountId); + expect(result.partition).to.equal(partition); + awsProvider.request.restore(); + }); }); }); @@ -1372,67 +1354,60 @@ describe('AwsProvider', () => { it('should return the AWS account id', () => { const accountId = '12345678'; - const stsGetCallerIdentityStub = sinon - .stub(awsProvider, 'request') - .resolves({ - ResponseMetadata: { RequestId: '12345678-1234-1234-1234-123456789012' }, - UserId: 'ABCDEFGHIJKLMNOPQRSTU:VWXYZ', - Account: accountId, - Arn: 'arn:aws:sts::123456789012:assumed-role/ROLE-NAME/VWXYZ', - }); + const stsGetCallerIdentityStub = sinon.stub(awsProvider, 'request').resolves({ + ResponseMetadata: { RequestId: '12345678-1234-1234-1234-123456789012' }, + UserId: 'ABCDEFGHIJKLMNOPQRSTU:VWXYZ', + Account: accountId, + Arn: 'arn:aws:sts::123456789012:assumed-role/ROLE-NAME/VWXYZ', + }); - return awsProvider.getAccountId() - .then((result) => { - expect(stsGetCallerIdentityStub.calledOnce).to.equal(true); - expect(result).to.equal(accountId); - awsProvider.request.restore(); - }); + return awsProvider.getAccountId().then(result => { + expect(stsGetCallerIdentityStub.calledOnce).to.equal(true); + expect(result).to.equal(accountId); + awsProvider.request.restore(); + }); }); }); describe('#isS3TransferAccelerationEnabled()', () => { it('should return false by default', () => { awsProvider.options['aws-s3-accelerate'] = undefined; - return expect(awsProvider.isS3TransferAccelerationEnabled()) - .to.equal(false); + return expect(awsProvider.isS3TransferAccelerationEnabled()).to.equal(false); }); it('should return true when CLI option is provided', () => { awsProvider.options['aws-s3-accelerate'] = true; - return expect(awsProvider.isS3TransferAccelerationEnabled()) - .to.equal(true); + return expect(awsProvider.isS3TransferAccelerationEnabled()).to.equal(true); }); }); describe('#canUseS3TransferAcceleration()', () => { it('should return false by default with any input', () => { awsProvider.options['aws-s3-accelerate'] = undefined; - return expect(awsProvider.canUseS3TransferAcceleration('lambda', 'updateFunctionCode')) - .to.equal(false); + return expect( + awsProvider.canUseS3TransferAcceleration('lambda', 'updateFunctionCode') + ).to.equal(false); }); it('should return false by default with S3.upload too', () => { awsProvider.options['aws-s3-accelerate'] = undefined; - return expect(awsProvider.canUseS3TransferAcceleration('S3', 'upload')) - .to.equal(false); + return expect(awsProvider.canUseS3TransferAcceleration('S3', 'upload')).to.equal(false); }); it('should return false by default with S3.putObject too', () => { awsProvider.options['aws-s3-accelerate'] = undefined; - return expect(awsProvider.canUseS3TransferAcceleration('S3', 'putObject')) - .to.equal(false); + return expect(awsProvider.canUseS3TransferAcceleration('S3', 'putObject')).to.equal(false); }); it('should return false when CLI option is provided but not an S3 upload', () => { awsProvider.options['aws-s3-accelerate'] = true; - return expect(awsProvider.canUseS3TransferAcceleration('lambda', 'updateFunctionCode')) - .to.equal(false); + return expect( + awsProvider.canUseS3TransferAcceleration('lambda', 'updateFunctionCode') + ).to.equal(false); }); it('should return true when CLI option is provided for S3.upload', () => { awsProvider.options['aws-s3-accelerate'] = true; - return expect(awsProvider.canUseS3TransferAcceleration('S3', 'upload')) - .to.equal(true); + return expect(awsProvider.canUseS3TransferAcceleration('S3', 'upload')).to.equal(true); }); it('should return true when CLI option is provided for S3.putObject', () => { awsProvider.options['aws-s3-accelerate'] = true; - return expect(awsProvider.canUseS3TransferAcceleration('S3', 'putObject')) - .to.equal(true); + return expect(awsProvider.canUseS3TransferAcceleration('S3', 'putObject')).to.equal(true); }); }); diff --git a/lib/plugins/aws/remove/index.js b/lib/plugins/aws/remove/index.js index e7ca3ca5b..2007f68b6 100644 --- a/lib/plugins/aws/remove/index.js +++ b/lib/plugins/aws/remove/index.js @@ -15,11 +15,12 @@ class AwsRemove { Object.assign(this, validate, emptyS3Bucket, removeStack, monitorStack); this.hooks = { - 'remove:remove': () => BbPromise.bind(this) - .then(this.validate) - .then(this.emptyS3Bucket) - .then(this.removeStack) - .then((cfData) => this.monitorStack('removal', cfData)), + 'remove:remove': () => + BbPromise.bind(this) + .then(this.validate) + .then(this.emptyS3Bucket) + .then(this.removeStack) + .then(cfData => this.monitorStack('removal', cfData)), }; } } diff --git a/lib/plugins/aws/remove/index.test.js b/lib/plugins/aws/remove/index.test.js index 6ee7e20b2..b9f802b48 100644 --- a/lib/plugins/aws/remove/index.test.js +++ b/lib/plugins/aws/remove/index.test.js @@ -27,27 +27,22 @@ describe('AwsRemove', () => { }); it('should run promise chain in order', () => { - const validateStub = sinon - .stub(awsRemove, 'validate').resolves(); - const emptyS3BucketStub = sinon - .stub(awsRemove, 'emptyS3Bucket').resolves(); - const removeStackStub = sinon - .stub(awsRemove, 'removeStack').resolves(); - const monitorStackStub = sinon - .stub(awsRemove, 'monitorStack').resolves(); + const validateStub = sinon.stub(awsRemove, 'validate').resolves(); + const emptyS3BucketStub = sinon.stub(awsRemove, 'emptyS3Bucket').resolves(); + const removeStackStub = sinon.stub(awsRemove, 'removeStack').resolves(); + const monitorStackStub = sinon.stub(awsRemove, 'monitorStack').resolves(); - return awsRemove.hooks['remove:remove']() - .then(() => { - expect(validateStub.calledOnce).to.be.equal(true); - expect(emptyS3BucketStub.calledAfter(validateStub)).to.be.equal(true); - expect(removeStackStub.calledAfter(emptyS3BucketStub)).to.be.equal(true); - expect(monitorStackStub.calledAfter(emptyS3BucketStub)).to.be.equal(true); + return awsRemove.hooks['remove:remove']().then(() => { + expect(validateStub.calledOnce).to.be.equal(true); + expect(emptyS3BucketStub.calledAfter(validateStub)).to.be.equal(true); + expect(removeStackStub.calledAfter(emptyS3BucketStub)).to.be.equal(true); + expect(monitorStackStub.calledAfter(emptyS3BucketStub)).to.be.equal(true); - awsRemove.validate.restore(); - awsRemove.emptyS3Bucket.restore(); - awsRemove.removeStack.restore(); - awsRemove.monitorStack.restore(); - }); + awsRemove.validate.restore(); + awsRemove.emptyS3Bucket.restore(); + awsRemove.removeStack.restore(); + awsRemove.monitorStack.restore(); + }); }); }); }); diff --git a/lib/plugins/aws/remove/lib/bucket.js b/lib/plugins/aws/remove/lib/bucket.js index b67abf82e..f5d483321 100644 --- a/lib/plugins/aws/remove/lib/bucket.js +++ b/lib/plugins/aws/remove/lib/bucket.js @@ -4,10 +4,9 @@ const BbPromise = require('bluebird'); module.exports = { setServerlessDeploymentBucketName() { - return this.provider.getServerlessDeploymentBucketName() - .then((bucketName) => { - this.bucketName = bucketName; - }); + return this.provider.getServerlessDeploymentBucketName().then(bucketName => { + this.bucketName = bucketName; + }); }, listObjects() { @@ -16,19 +15,21 @@ module.exports = { this.serverless.cli.log('Getting all objects in S3 bucket...'); const serviceStage = `${this.serverless.service.service}/${this.provider.getStage()}`; - return this.provider.request('S3', 'listObjectsV2', { - Bucket: this.bucketName, - Prefix: `${this.provider.getDeploymentPrefix()}/${serviceStage}`, - }).then((result) => { - if (result) { - result.Contents.forEach((object) => { - this.objectsInBucket.push({ - Key: object.Key, + return this.provider + .request('S3', 'listObjectsV2', { + Bucket: this.bucketName, + Prefix: `${this.provider.getDeploymentPrefix()}/${serviceStage}`, + }) + .then(result => { + if (result) { + result.Contents.forEach(object => { + this.objectsInBucket.push({ + Key: object.Key, + }); }); - }); - } - return BbPromise.resolve(); - }); + } + return BbPromise.resolve(); + }); }, deleteObjects() { diff --git a/lib/plugins/aws/remove/lib/bucket.test.js b/lib/plugins/aws/remove/lib/bucket.test.js index 8714e96fc..703ab1750 100644 --- a/lib/plugins/aws/remove/lib/bucket.test.js +++ b/lib/plugins/aws/remove/lib/bucket.test.js @@ -39,49 +39,40 @@ describe('emptyS3Bucket', () => { describe('#listObjects()', () => { it('should resolve if no objects are present', () => { - const listObjectsStub = sinon.stub(awsRemove.provider, 'request') - .resolves(); + const listObjectsStub = sinon.stub(awsRemove.provider, 'request').resolves(); const stage = awsRemove.provider.getStage(); const prefix = awsRemove.provider.getDeploymentPrefix(); return awsRemove.listObjects().then(() => { expect(listObjectsStub.calledOnce).to.be.equal(true); - expect(listObjectsStub.calledWithExactly( - 'S3', - 'listObjectsV2', - { + expect( + listObjectsStub.calledWithExactly('S3', 'listObjectsV2', { Bucket: awsRemove.bucketName, Prefix: `${prefix}/${serverless.service.service}/${stage}`, - } - )).to.be.equal(true); + }) + ).to.be.equal(true); expect(awsRemove.objectsInBucket.length).to.equal(0); awsRemove.provider.request.restore(); }); }); it('should push objects to the array if present', () => { - const listObjectsStub = sinon.stub(awsRemove.provider, 'request') - .resolves({ - Contents: [ - { Key: 'object1' }, - { Key: 'object2' }, - ], - }); + const listObjectsStub = sinon.stub(awsRemove.provider, 'request').resolves({ + Contents: [{ Key: 'object1' }, { Key: 'object2' }], + }); const stage = awsRemove.provider.getStage(); const prefix = awsRemove.provider.getDeploymentPrefix(); return awsRemove.listObjects().then(() => { expect(listObjectsStub.calledOnce).to.be.equal(true); - expect(listObjectsStub.calledWithExactly( - 'S3', - 'listObjectsV2', - { + expect( + listObjectsStub.calledWithExactly('S3', 'listObjectsV2', { Bucket: awsRemove.bucketName, Prefix: `${prefix}/${serverless.service.service}/${stage}`, - } - )).to.be.equal(true); + }) + ).to.be.equal(true); expect(awsRemove.objectsInBucket[0]).to.deep.equal({ Key: 'object1' }); expect(awsRemove.objectsInBucket[1]).to.deep.equal({ Key: 'object2' }); awsRemove.provider.request.restore(); @@ -93,26 +84,23 @@ describe('emptyS3Bucket', () => { it('should delete all objects in the S3 bucket', () => { awsRemove.objectsInBucket = [{ Key: 'foo' }]; - const deleteObjectsStub = sinon.stub(awsRemove.provider, 'request') - .resolves(); + const deleteObjectsStub = sinon.stub(awsRemove.provider, 'request').resolves(); return awsRemove.deleteObjects().then(() => { expect(deleteObjectsStub.calledOnce).to.be.equal(true); - expect(deleteObjectsStub.calledWithExactly( - 'S3', - 'deleteObjects', - { + expect( + deleteObjectsStub.calledWithExactly('S3', 'deleteObjects', { Bucket: awsRemove.bucketName, Delete: { Objects: awsRemove.objectsInBucket, }, - } - )).to.be.equal(true); + }) + ).to.be.equal(true); awsRemove.provider.request.restore(); }); }); - it('should resolve if objectsInBucket is empty', (done) => { + it('should resolve if objectsInBucket is empty', done => { awsRemove.objectsInBucket = []; awsRemove.deleteObjects().then(() => { @@ -124,16 +112,16 @@ describe('emptyS3Bucket', () => { describe('#emptyS3Bucket()', () => { it('should run promise chain in order', () => { const setServerlessDeploymentBucketNameStub = sinon - .stub(awsRemove, 'setServerlessDeploymentBucketName').resolves(); - const listObjectsStub = sinon - .stub(awsRemove, 'listObjects').resolves(); - const deleteObjectsStub = sinon - .stub(awsRemove, 'deleteObjects').resolves(); + .stub(awsRemove, 'setServerlessDeploymentBucketName') + .resolves(); + const listObjectsStub = sinon.stub(awsRemove, 'listObjects').resolves(); + const deleteObjectsStub = sinon.stub(awsRemove, 'deleteObjects').resolves(); return awsRemove.emptyS3Bucket().then(() => { expect(setServerlessDeploymentBucketNameStub.calledOnce).to.be.equal(true); - expect(listObjectsStub.calledAfter(setServerlessDeploymentBucketNameStub)) - .to.be.equal(true); + expect(listObjectsStub.calledAfter(setServerlessDeploymentBucketNameStub)).to.be.equal( + true + ); expect(deleteObjectsStub.calledAfter(listObjectsStub)).to.be.equal(true); awsRemove.setServerlessDeploymentBucketName.restore(); diff --git a/lib/plugins/aws/remove/lib/stack.js b/lib/plugins/aws/remove/lib/stack.js index fe712b1da..8b36720e3 100644 --- a/lib/plugins/aws/remove/lib/stack.js +++ b/lib/plugins/aws/remove/lib/stack.js @@ -18,14 +18,10 @@ module.exports = { StackId: stackName, }; - return this.provider.request('CloudFormation', - 'deleteStack', - params) - .then(() => cfData); + return this.provider.request('CloudFormation', 'deleteStack', params).then(() => cfData); }, removeStack() { - return BbPromise.bind(this) - .then(this.remove); + return BbPromise.bind(this).then(this.remove); }, }; diff --git a/lib/plugins/aws/remove/lib/stack.test.js b/lib/plugins/aws/remove/lib/stack.test.js index 1916a84db..b90ddd8e0 100644 --- a/lib/plugins/aws/remove/lib/stack.test.js +++ b/lib/plugins/aws/remove/lib/stack.test.js @@ -25,24 +25,24 @@ describe('removeStack', () => { }); describe('#remove()', () => { - it('should remove a stack', () => awsRemove.remove().then(() => { - expect(removeStackStub.calledOnce).to.be.equal(true); - expect(removeStackStub.calledWithExactly( - 'CloudFormation', - 'deleteStack', - { - StackName: `${serverless.service.service}-${awsRemove.provider.getStage()}`, - } - )).to.be.equal(true); - awsRemove.provider.request.restore(); - })); + it('should remove a stack', () => + awsRemove.remove().then(() => { + expect(removeStackStub.calledOnce).to.be.equal(true); + expect( + removeStackStub.calledWithExactly('CloudFormation', 'deleteStack', { + StackName: `${serverless.service.service}-${awsRemove.provider.getStage()}`, + }) + ).to.be.equal(true); + awsRemove.provider.request.restore(); + })); it('should use CloudFormation service role if it is specified', () => { awsRemove.serverless.service.provider.cfnRole = 'arn:aws:iam::123456789012:role/myrole'; return awsRemove.remove().then(() => { - expect(removeStackStub.args[0][2].RoleARN) - .to.equal('arn:aws:iam::123456789012:role/myrole'); + expect(removeStackStub.args[0][2].RoleARN).to.equal( + 'arn:aws:iam::123456789012:role/myrole' + ); awsRemove.provider.request.restore(); }); }); @@ -50,8 +50,7 @@ describe('removeStack', () => { describe('#removeStack()', () => { it('should run promise chain in order', () => { - const removeStub = sinon - .stub(awsRemove, 'remove').resolves(); + const removeStub = sinon.stub(awsRemove, 'remove').resolves(); return awsRemove.removeStack().then(() => { expect(removeStub.calledOnce).to.be.equal(true); diff --git a/lib/plugins/aws/rollback/index.js b/lib/plugins/aws/rollback/index.js index 47ec0a101..acc314bb8 100644 --- a/lib/plugins/aws/rollback/index.js +++ b/lib/plugins/aws/rollback/index.js @@ -14,24 +14,19 @@ class AwsRollback { this.options = options; this.provider = this.serverless.getProvider('aws'); - Object.assign( - this, - validate, - setBucketName, - updateStack, - monitorStack - ); + Object.assign(this, validate, setBucketName, updateStack, monitorStack); this.hooks = { - 'before:rollback:initialize': () => BbPromise.bind(this) - .then(this.validate), + 'before:rollback:initialize': () => BbPromise.bind(this).then(this.validate), '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')); + 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; } @@ -50,17 +45,16 @@ class AwsRollback { const deploymentPrefix = this.provider.getDeploymentPrefix(); const prefix = `${deploymentPrefix}/${serviceName}/${stage}`; - return this.provider.request('S3', - 'listObjectsV2', - { + return this.provider + .request('S3', 'listObjectsV2', { Bucket: this.bucketName, Prefix: prefix, }) - .then((response) => { + .then(response => { const deployments = findAndGroupDeployments(response, deploymentPrefix, serviceName, stage); if (deployments.length === 0) { - const msg = 'Couldn\'t find any existing deployments.'; + const msg = "Couldn't find any existing deployments."; const hint = 'Please verify that stage and region are correct.'; return BbPromise.reject(`${msg} ${hint}`); } @@ -73,12 +67,12 @@ class AwsRollback { } const dateString = `${date.getTime().toString()}-${date.toISOString()}`; - const exists = _.some(deployments, (deployment) => ( + const exists = _.some(deployments, deployment => _.some(deployment, { directory: dateString, file: 'compiled-cloudformation-template.json', }) - )); + ); if (!exists) { const msg = `Couldn't find a deployment for the timestamp: ${this.options.timestamp}.`; diff --git a/lib/plugins/aws/rollback/index.test.js b/lib/plugins/aws/rollback/index.test.js index 6813ae909..301e15c8e 100644 --- a/lib/plugins/aws/rollback/index.test.js +++ b/lib/plugins/aws/rollback/index.test.js @@ -14,7 +14,7 @@ describe('AwsRollback', () => { let serverless; let provider; - const createInstance = (options) => { + const createInstance = options => { serverless = new Serverless(); provider = new AwsProvider(serverless, options); serverless.setProvider('aws', provider); @@ -59,20 +59,14 @@ describe('AwsRollback', () => { }); it('should run "rollback:rollback" promise chain in order', () => { - const setBucketNameStub = sinon - .stub(awsRollback, 'setBucketName').resolves(); - const setStackToUpdateStub = sinon - .stub(awsRollback, 'setStackToUpdate').resolves(); - const updateStackStub = sinon - .stub(awsRollback, 'updateStack').resolves(); + const setBucketNameStub = sinon.stub(awsRollback, 'setBucketName').resolves(); + const setStackToUpdateStub = sinon.stub(awsRollback, 'setStackToUpdate').resolves(); + const updateStackStub = sinon.stub(awsRollback, 'updateStack').resolves(); return awsRollback.hooks['rollback:rollback']().then(() => { - expect(setBucketNameStub.calledOnce) - .to.be.equal(true); - expect(setStackToUpdateStub.calledAfter(setBucketNameStub)) - .to.be.equal(true); - expect(updateStackStub.calledAfter(setStackToUpdateStub)) - .to.be.equal(true); + expect(setBucketNameStub.calledOnce).to.be.equal(true); + expect(setStackToUpdateStub.calledAfter(setBucketNameStub)).to.be.equal(true); + expect(updateStackStub.calledAfter(setStackToUpdateStub)).to.be.equal(true); }); }); @@ -97,7 +91,8 @@ describe('AwsRollback', () => { const s3Objects = [ { // eslint-disable-next-line max-len - Key: 'serverless/rollback/dev/1476779096930-2016-10-18T08:24:56.930Z/compiled-cloudformation-template.json', + Key: + 'serverless/rollback/dev/1476779096930-2016-10-18T08:24:56.930Z/compiled-cloudformation-template.json', }, { Key: 'serverless/rollback/dev/1476779096930-2016-10-18T08:24:56.930Z/test.zip', @@ -107,41 +102,37 @@ describe('AwsRollback', () => { Contents: s3Objects, }; - sinon.stub(awsRollback.provider, 'request') - .resolves(s3Response); + sinon.stub(awsRollback.provider, 'request').resolves(s3Response); - return awsRollback.setStackToUpdate() - .then(() => { - expect(awsRollback.serverless.service.package.artifactDirectoryName) - .to.be.equal('serverless/rollback/dev/1476779096930-2016-10-18T08:24:56.930Z'); + return awsRollback.setStackToUpdate().then(() => { + expect(awsRollback.serverless.service.package.artifactDirectoryName).to.be.equal( + 'serverless/rollback/dev/1476779096930-2016-10-18T08:24:56.930Z' + ); - awsRollback.provider.request.restore(); - }); + awsRollback.provider.request.restore(); + }); }); - it('should reject in case no deployments are available', () => { const s3Response = { Contents: [], }; - const listObjectsStub = sinon.stub(awsRollback.provider, 'request') - .resolves(s3Response); + const listObjectsStub = sinon.stub(awsRollback.provider, 'request').resolves(s3Response); - return awsRollback.setStackToUpdate() + return awsRollback + .setStackToUpdate() .then(() => { assert.isNotOk(true, 'setStackToUpdate should not resolve'); }) - .catch((errorMessage) => { - expect(errorMessage).to.include('Couldn\'t find any existing deployments'); + .catch(errorMessage => { + expect(errorMessage).to.include("Couldn't find any existing deployments"); expect(listObjectsStub.calledOnce).to.be.equal(true); - expect(listObjectsStub.calledWithExactly( - 'S3', - 'listObjectsV2', - { + expect( + listObjectsStub.calledWithExactly('S3', 'listObjectsV2', { Bucket: awsRollback.bucketName, Prefix: `${s3Key}`, - } - )).to.be.equal(true); + }) + ).to.be.equal(true); awsRollback.provider.request.restore(); }); }); @@ -150,7 +141,8 @@ describe('AwsRollback', () => { const s3Objects = [ { // eslint-disable-next-line max-len - Key: 'serverless/rollback/dev/2000000000000-2016-10-18T08:24:56.930Z/compiled-cloudformation-template.json', + Key: + 'serverless/rollback/dev/2000000000000-2016-10-18T08:24:56.930Z/compiled-cloudformation-template.json', }, { Key: 'serverless/rollback/dev/2000000000000-2016-10-18T08:24:56.930Z/test.zip', @@ -160,24 +152,22 @@ describe('AwsRollback', () => { Contents: s3Objects, }; - const listObjectsStub = sinon.stub(awsRollback.provider, 'request') - .resolves(s3Response); + const listObjectsStub = sinon.stub(awsRollback.provider, 'request').resolves(s3Response); - return awsRollback.setStackToUpdate() + return awsRollback + .setStackToUpdate() .then(() => { assert.isNotOk(true, 'setStackToUpdate should not resolve'); }) - .catch((errorMessage) => { - expect(errorMessage).to.include('Couldn\'t find a deployment for the timestamp'); + .catch(errorMessage => { + expect(errorMessage).to.include("Couldn't find a deployment for the timestamp"); expect(listObjectsStub.calledOnce).to.be.equal(true); - expect(listObjectsStub.calledWithExactly( - 'S3', - 'listObjectsV2', - { + expect( + listObjectsStub.calledWithExactly('S3', 'listObjectsV2', { Bucket: awsRollback.bucketName, Prefix: `${s3Key}`, - } - )).to.be.equal(true); + }) + ).to.be.equal(true); awsRollback.provider.request.restore(); }); }); @@ -186,7 +176,8 @@ describe('AwsRollback', () => { const s3Objects = [ { // eslint-disable-next-line max-len - Key: 'serverless/rollback/dev/1476779096930-2016-10-18T08:24:56.930Z/compiled-cloudformation-template.json', + Key: + 'serverless/rollback/dev/1476779096930-2016-10-18T08:24:56.930Z/compiled-cloudformation-template.json', }, { Key: 'serverless/rollback/dev/1476779096930-2016-10-18T08:24:56.930Z/test.zip', @@ -196,24 +187,21 @@ describe('AwsRollback', () => { Contents: s3Objects, }; - const listObjectsStub = sinon.stub(awsRollback.provider, 'request') - .resolves(s3Response); + const listObjectsStub = sinon.stub(awsRollback.provider, 'request').resolves(s3Response); - return awsRollback.setStackToUpdate() - .then(() => { - expect(awsRollback.serverless.service.package.artifactDirectoryName) - .to.be.equal('serverless/rollback/dev/1476779096930-2016-10-18T08:24:56.930Z'); - expect(listObjectsStub.calledOnce).to.be.equal(true); - expect(listObjectsStub.calledWithExactly( - 'S3', - 'listObjectsV2', - { - Bucket: awsRollback.bucketName, - Prefix: `${s3Key}`, - } - )).to.be.equal(true); - awsRollback.provider.request.restore(); - }); + return awsRollback.setStackToUpdate().then(() => { + expect(awsRollback.serverless.service.package.artifactDirectoryName).to.be.equal( + 'serverless/rollback/dev/1476779096930-2016-10-18T08:24:56.930Z' + ); + expect(listObjectsStub.calledOnce).to.be.equal(true); + expect( + listObjectsStub.calledWithExactly('S3', 'listObjectsV2', { + Bucket: awsRollback.bucketName, + Prefix: `${s3Key}`, + }) + ).to.be.equal(true); + awsRollback.provider.request.restore(); + }); }); }); }); diff --git a/lib/plugins/aws/rollbackFunction/index.js b/lib/plugins/aws/rollbackFunction/index.js index 98fe9820a..6306a94ea 100644 --- a/lib/plugins/aws/rollbackFunction/index.js +++ b/lib/plugins/aws/rollbackFunction/index.js @@ -17,9 +17,7 @@ class AwsRollbackFunction { commands: { function: { usage: 'Rollback the function to a specific version', - lifecycleEvents: [ - 'rollback', - ], + lifecycleEvents: ['rollback'], options: { function: { usage: 'Name of the function', @@ -46,11 +44,12 @@ class AwsRollbackFunction { }; this.hooks = { - 'rollback:function:rollback': () => BbPromise.bind(this) - .then(this.validate) - .then(this.getFunctionToBeRestored) - .then(this.fetchFunctionCode) - .then(this.restoreFunction), + 'rollback:function:rollback': () => + BbPromise.bind(this) + .then(this.validate) + .then(this.getFunctionToBeRestored) + .then(this.fetchFunctionCode) + .then(this.restoreFunction), }; } @@ -70,30 +69,27 @@ class AwsRollbackFunction { Qualifier: funcVersion, }; - return this.provider.request( - 'Lambda', - 'getFunction', - params - ) - .then((func) => func) - .catch((error) => { - if (error.message.match(/not found/)) { - const errorMessage = [ - `Function "${funcName}" with version "${funcVersion}" not found.`, - ` Please check if you've deployed "${funcName}"`, - ` and version "${funcVersion}" is available for this function.`, - ' Please check the docs for more info.', - ].join(''); - throw new Error(errorMessage); - } - throw new Error(error.message); - }); + return this.provider + .request('Lambda', 'getFunction', params) + .then(func => func) + .catch(error => { + if (error.message.match(/not found/)) { + const errorMessage = [ + `Function "${funcName}" with version "${funcVersion}" not found.`, + ` Please check if you've deployed "${funcName}"`, + ` and version "${funcVersion}" is available for this function.`, + ' Please check the docs for more info.', + ].join(''); + throw new Error(errorMessage); + } + throw new Error(error.message); + }); } fetchFunctionCode(func) { const codeUrl = func.Code.Location; - return fetch(codeUrl).then((response) => response.buffer()); + return fetch(codeUrl).then(response => response.buffer()); } restoreFunction(zipBuffer) { @@ -108,11 +104,7 @@ class AwsRollbackFunction { ZipFile: zipBuffer, }; - return this.provider.request( - 'Lambda', - 'updateFunctionCode', - params - ).then(() => { + return this.provider.request('Lambda', 'updateFunctionCode', params).then(() => { this.serverless.cli.log(`Successfully rolled back function "${this.options.function}"`); }); } diff --git a/lib/plugins/aws/rollbackFunction/index.test.js b/lib/plugins/aws/rollbackFunction/index.test.js index 25a5d54a7..6dc24b9fa 100644 --- a/lib/plugins/aws/rollbackFunction/index.test.js +++ b/lib/plugins/aws/rollbackFunction/index.test.js @@ -49,14 +49,12 @@ describe('AwsRollbackFunction', () => { let restoreFunctionStub; beforeEach(() => { - validateStub = sinon - .stub(awsRollbackFunction, 'validate').resolves(); + validateStub = sinon.stub(awsRollbackFunction, 'validate').resolves(); getFunctionToBeRestoredStub = sinon - .stub(awsRollbackFunction, 'getFunctionToBeRestored').resolves(); - fetchFunctionCodeStub = sinon - .stub(awsRollbackFunction, 'fetchFunctionCode').resolves(); - restoreFunctionStub = sinon - .stub(awsRollbackFunction, 'restoreFunction').resolves(); + .stub(awsRollbackFunction, 'getFunctionToBeRestored') + .resolves(); + fetchFunctionCodeStub = sinon.stub(awsRollbackFunction, 'fetchFunctionCode').resolves(); + restoreFunctionStub = sinon.stub(awsRollbackFunction, 'restoreFunction').resolves(); }); afterEach(() => { @@ -78,14 +76,13 @@ describe('AwsRollbackFunction', () => { expect(awsRollbackFunctionWithEmptyOptions.options).to.deep.equal({}); }); - it('should run promise chain in order', () => awsRollbackFunction - .hooks['rollback:function:rollback']().then(() => { + it('should run promise chain in order', () => + awsRollbackFunction.hooks['rollback:function:rollback']().then(() => { expect(validateStub.calledOnce).to.equal(true); expect(getFunctionToBeRestoredStub.calledAfter(validateStub)).to.equal(true); expect(fetchFunctionCodeStub.calledAfter(getFunctionToBeRestoredStub)).to.equal(true); expect(restoreFunctionStub.calledAfter(fetchFunctionCodeStub)).to.equal(true); - }) - ); + })); }); describe('#getFunctionToBeRestored()', () => { @@ -106,16 +103,14 @@ describe('AwsRollbackFunction', () => { awsRollbackFunction.options.function = 'hello'; awsRollbackFunction.options.version = '4711'; - return awsRollbackFunction.getFunctionToBeRestored().then((result) => { + return awsRollbackFunction.getFunctionToBeRestored().then(result => { expect(getFunctionStub.calledOnce).to.equal(true); - expect(getFunctionStub.calledWithExactly( - 'Lambda', - 'getFunction', - { + expect( + getFunctionStub.calledWithExactly('Lambda', 'getFunction', { FunctionName: 'service-dev-hello', Qualifier: '4711', - } - )).to.equal(true); + }) + ).to.equal(true); expect(consoleLogStub.called).to.equal(true); expect(result).to.deep.equal({ function: 'hello' }); }); @@ -139,17 +134,15 @@ describe('AwsRollbackFunction', () => { awsRollbackFunction.options.function = 'hello'; awsRollbackFunction.options.version = '4711'; - return awsRollbackFunction.getFunctionToBeRestored().catch((error) => { + return awsRollbackFunction.getFunctionToBeRestored().catch(error => { expect(error.message.match(/Function "hello" with version "4711" not found/)); expect(getFunctionStub.calledOnce).to.equal(true); - expect(getFunctionStub.calledWithExactly( - 'Lambda', - 'getFunction', - { + expect( + getFunctionStub.calledWithExactly('Lambda', 'getFunction', { FunctionName: 'service-dev-hello', Qualifier: '4711', - } - )).to.equal(true); + }) + ).to.equal(true); expect(consoleLogStub.called).to.equal(true); }); }); @@ -172,17 +165,15 @@ describe('AwsRollbackFunction', () => { awsRollbackFunction.options.function = 'hello'; awsRollbackFunction.options.version = '4711'; - return awsRollbackFunction.getFunctionToBeRestored().catch((error) => { + return awsRollbackFunction.getFunctionToBeRestored().catch(error => { expect(error.message.match(/something went wrong/)); expect(getFunctionStub.calledOnce).to.equal(true); - expect(getFunctionStub.calledWithExactly( - 'Lambda', - 'getFunction', - { + expect( + getFunctionStub.calledWithExactly('Lambda', 'getFunction', { FunctionName: 'service-dev-hello', Qualifier: '4711', - } - )).to.equal(true); + }) + ).to.equal(true); expect(consoleLogStub.called).to.equal(true); }); }); @@ -207,8 +198,7 @@ describe('AwsRollbackFunction', () => { let updateFunctionCodeStub; beforeEach(() => { - updateFunctionCodeStub = sinon - .stub(awsRollbackFunction.provider, 'request').resolves(); + updateFunctionCodeStub = sinon.stub(awsRollbackFunction.provider, 'request').resolves(); }); afterEach(() => { @@ -221,14 +211,12 @@ describe('AwsRollbackFunction', () => { return awsRollbackFunction.restoreFunction(zipBuffer).then(() => { expect(updateFunctionCodeStub.calledOnce).to.equal(true); - expect(updateFunctionCodeStub.calledWithExactly( - 'Lambda', - 'updateFunctionCode', - { + expect( + updateFunctionCodeStub.calledWithExactly('Lambda', 'updateFunctionCode', { FunctionName: 'service-dev-hello', ZipFile: zipBuffer, - } - )).to.equal(true); + }) + ).to.equal(true); expect(consoleLogStub.called).to.equal(true); }); }); diff --git a/lib/plugins/aws/utils/credentials.js b/lib/plugins/aws/utils/credentials.js new file mode 100644 index 000000000..5cf1953ea --- /dev/null +++ b/lib/plugins/aws/utils/credentials.js @@ -0,0 +1,120 @@ +'use strict'; + +const { join } = require('path'); +const { constants, readFile, writeFile, mkdir } = require('fs'); +const os = require('os'); +const BbPromise = require('bluebird'); + +const homedir = os.homedir(); +const awsConfigDirPath = join(homedir, '.aws'); +const credentialsFilePath = homedir ? join(awsConfigDirPath, 'credentials') : null; + +const isWindows = process.platform === 'win32'; +const profileNameRe = /^\[([^\]]+)]\s*$/; +const settingRe = /^([a-zA-Z0-9_]+)\s*=\s*([^\s]+)\s*$/; +const settingMap = new Map([ + ['aws_access_key_id', 'accessKeyId'], + ['aws_secret_access_key', 'secretAccessKey'], + ['aws_session_token', 'sessionToken'], +]); +const parseFileProfiles = content => { + const profiles = new Map(); + let currentProfile; + for (const line of content.split(/[\n\r]+/)) { + const profileNameMatches = line.match(profileNameRe); + if (profileNameMatches) { + currentProfile = {}; + profiles.set(profileNameMatches[1], currentProfile); + continue; + } + if (!currentProfile) continue; + const settingMatches = line.match(settingRe); + if (!settingMatches) continue; + let [, settingAwsName] = settingMatches; + settingAwsName = settingAwsName.toLowerCase(); + const settingName = settingMap.get(settingAwsName); + if (settingName) currentProfile[settingName] = settingMatches[2]; + } + for (const [profileName, profileData] of profiles) { + if (!profileData.sessionToken && (!profileData.accessKeyId || !profileData.secretAccessKey)) { + profiles.delete(profileName); + } + } + return profiles; +}; + +const writeCredentialsContent = content => + new BbPromise((resolve, reject) => + writeFile( + credentialsFilePath, + content, + !isWindows ? { mode: constants.S_IRUSR | constants.S_IWUSR } : null, + writeFileError => { + if (writeFileError) { + if (writeFileError.code === 'ENOENT') { + mkdir(awsConfigDirPath, !isWindows ? { mode: constants.S_IRWXU } : null, mkdirError => { + if (mkdirError) reject(mkdirError); + else resolve(writeCredentialsContent(content)); + }); + } else { + reject(writeFileError); + } + } else { + resolve(); + } + } + ) + ); + +module.exports = { + resolveFileProfiles() { + return new BbPromise((resolve, reject) => { + if (!credentialsFilePath) { + resolve(new Map()); + return; + } + readFile(credentialsFilePath, { encoding: 'utf8' }, (error, content) => { + if (error) { + if (error.code === 'ENOENT') { + resolve(new Map()); + return; + } + reject(error); + return; + } + resolve(parseFileProfiles(content)); + }); + }); + }, + + resolveEnvCredentials() { + if (!process.env.AWS_ACCESS_KEY_ID || !process.env.AWS_SECRET_ACCESS_KEY) return null; + return { + accessKeyId: process.env.AWS_ACCESS_KEY_ID, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, + }; + }, + + saveFileProfiles(profiles) { + return new BbPromise(resolve => { + if (!credentialsFilePath) throw new Error('Could not resolve path to user credentials file'); + resolve( + writeCredentialsContent( + `${Array.from(profiles) + .map(([name, data]) => { + const lineTokens = [`[${name}]`]; + if (data.sessionToken) lineTokens.push(`aws_session_token=${data.sessionToken}`); + else { + lineTokens.push( + `aws_access_key_id=${data.accessKeyId}`, + `aws_secret_access_key=${data.secretAccessKey}` + ); + } + return `${lineTokens.join('\n')}\n`; + }) + .join('\n')}` + ) + ); + }); + }, +}; diff --git a/lib/plugins/aws/utils/credentials.test.js b/lib/plugins/aws/utils/credentials.test.js new file mode 100644 index 000000000..eaa659cb5 --- /dev/null +++ b/lib/plugins/aws/utils/credentials.test.js @@ -0,0 +1,91 @@ +'use strict'; + +const expect = require('chai').expect; +const BbPromise = require('bluebird'); +const os = require('os'); +const path = require('path'); +const { outputFile } = require('fs-extra'); +const overrideEnv = require('process-utils/override-env'); +const credentials = require('./credentials'); + +describe('#credentials', () => { + const credentialsFilePath = path.join(os.homedir(), '.aws', 'credentials'); + + it('should resolve file profiles', () => { + const profiles = new Map([ + [ + 'my-profile1', + { + accessKeyId: 'my-old-profile-key1', + secretAccessKey: 'my-old-profile-secret1', + }, + ], + [ + 'my-profile2', + { + accessKeyId: 'my-old-profile-key2', + secretAccessKey: 'my-old-profile-secret2', + }, + ], + ]); + + const [[profile1Name, profile1], [profile2Name, profile2]] = Array.from(profiles); + const credentialsFileContent = `${[ + `[${[profile1Name]}]`, + `aws_access_key_id = ${profile1.accessKeyId}`, + `aws_secret_access_key = ${profile1.secretAccessKey}`, + '', + `[${[profile2Name]}]`, + `aws_access_key_id = ${profile2.accessKeyId}`, + `aws_secret_access_key = ${profile2.secretAccessKey}`, + ].join('\n')}\n`; + + return new BbPromise((resolve, reject) => { + outputFile(credentialsFilePath, credentialsFileContent, error => { + if (error) reject(error); + else resolve(); + }); + }).then(() => + credentials + .resolveFileProfiles() + .then(resolvedProfiles => expect(resolvedProfiles).to.deep.equal(profiles)) + ); + }); + + it('should resolve env credentials', () => + overrideEnv(() => { + process.env.AWS_ACCESS_KEY_ID = 'foo'; + process.env.AWS_SECRET_ACCESS_KEY = 'bar'; + expect(credentials.resolveEnvCredentials()).to.deep.equal({ + accessKeyId: 'foo', + secretAccessKey: 'bar', + }); + })); + + it('should save file profiles', () => { + const profiles = new Map([ + [ + 'my-profileA', + { + accessKeyId: 'my-old-profile-key1', + secretAccessKey: 'my-old-profile-secret1', + }, + ], + [ + 'my-profileB', + { + accessKeyId: 'my-old-profile-key2', + secretAccessKey: 'my-old-profile-secret2', + }, + ], + ]); + + return credentials + .saveFileProfiles(profiles) + .then(() => + credentials + .resolveFileProfiles() + .then(resolvedProfiles => expect(resolvedProfiles).to.deep.equal(profiles)) + ); + }); +}); diff --git a/lib/plugins/aws/utils/findAndGroupDeployments.js b/lib/plugins/aws/utils/findAndGroupDeployments.js index aef6e7d36..fd8262d59 100644 --- a/lib/plugins/aws/utils/findAndGroupDeployments.js +++ b/lib/plugins/aws/utils/findAndGroupDeployments.js @@ -5,8 +5,8 @@ const _ = require('lodash'); module.exports = (s3Response, prefix, service, stage) => { if (s3Response.Contents.length) { const regex = new RegExp(`${prefix}/${service}/${stage}/(.+-.+-.+-.+)/(.+)`); - const s3Objects = s3Response.Contents.filter((s3Object) => s3Object.Key.match(regex)); - const names = s3Objects.map((s3Object) => { + const s3Objects = s3Response.Contents.filter(s3Object => s3Object.Key.match(regex)); + const names = s3Objects.map(s3Object => { const match = s3Object.Key.match(regex); return { directory: match[1], @@ -14,7 +14,7 @@ module.exports = (s3Response, prefix, service, stage) => { }; }); const grouped = _.groupBy(names, 'directory'); - return _.map(grouped, (value) => value); + return _.map(grouped, value => value); } return []; }; diff --git a/lib/plugins/aws/utils/findAndGroupDeployments.test.js b/lib/plugins/aws/utils/findAndGroupDeployments.test.js index 17406cf84..2986ed854 100644 --- a/lib/plugins/aws/utils/findAndGroupDeployments.test.js +++ b/lib/plugins/aws/utils/findAndGroupDeployments.test.js @@ -16,21 +16,24 @@ describe('#findAndGroupDeployments()', () => { const s3Objects = [ { // eslint-disable-next-line max-len - Key: 'serverless/test/dev/1476779096930-2016-10-18T08:24:56.930Z/compiled-cloudformation-template.json', + Key: + 'serverless/test/dev/1476779096930-2016-10-18T08:24:56.930Z/compiled-cloudformation-template.json', }, { Key: 'serverless/test/dev/1476779096930-2016-10-18T08:24:56.930Z/test.zip', }, { // eslint-disable-next-line max-len - Key: 'serverless/test/dev/1476779278222-2016-10-18T08:27:58.222Z/compiled-cloudformation-template.json', + Key: + 'serverless/test/dev/1476779278222-2016-10-18T08:27:58.222Z/compiled-cloudformation-template.json', }, { Key: 'serverless/test/dev/1476779278222-2016-10-18T08:27:58.222Z/test.zip', }, { // eslint-disable-next-line max-len - Key: 'serverless/test/dev/1476781042481-2016-10-18T08:57:22.481Z/compiled-cloudformation-template.json', + Key: + 'serverless/test/dev/1476781042481-2016-10-18T08:57:22.481Z/compiled-cloudformation-template.json', }, { Key: 'serverless/test/dev/1476781042481-2016-10-18T08:57:22.481Z/test.zip', @@ -73,7 +76,8 @@ describe('#findAndGroupDeployments()', () => { ], ]; - expect(findAndGroupDeployments(s3Response, 'serverless', 'test', 'dev')) - .to.deep.equal(expected); + expect(findAndGroupDeployments(s3Response, 'serverless', 'test', 'dev')).to.deep.equal( + expected + ); }); }); diff --git a/lib/plugins/aws/utils/findReferences.test.js b/lib/plugins/aws/utils/findReferences.test.js index cc994712d..1e3681c00 100644 --- a/lib/plugins/aws/utils/findReferences.test.js +++ b/lib/plugins/aws/utils/findReferences.test.js @@ -9,8 +9,12 @@ describe('#findReferences()', () => { const withoutArgs = findReferences(); const nullArgs = findReferences(null); - expect(withoutArgs).to.be.a('Array').to.have.lengthOf(0); - expect(nullArgs).to.be.a('Array').have.lengthOf(0); + expect(withoutArgs) + .to.be.a('Array') + .to.have.lengthOf(0); + expect(nullArgs) + .to.be.a('Array') + .have.lengthOf(0); }); it('should return paths', () => { @@ -45,7 +49,9 @@ describe('#findReferences()', () => { ]; const paths = findReferences(testObject, 'hit'); - expect(paths).to.be.a('Array').to.have.lengthOf(4); + expect(paths) + .to.be.a('Array') + .to.have.lengthOf(4); expect(_.every(paths, path => _.includes(expectedResult, path))).to.equal(true); }); @@ -82,7 +88,9 @@ describe('#findReferences()', () => { ]; const paths = findReferences(testObject, 'hit'); - expect(paths).to.be.a('Array').to.have.lengthOf(4); + expect(paths) + .to.be.a('Array') + .to.have.lengthOf(4); expect(_.every(paths, path => _.includes(expectedResult, path))).to.equal(true); }); }); diff --git a/lib/plugins/aws/utils/formatLambdaLogEvent.js b/lib/plugins/aws/utils/formatLambdaLogEvent.js index a4aeee14f..1359ddb0b 100644 --- a/lib/plugins/aws/utils/formatLambdaLogEvent.js +++ b/lib/plugins/aws/utils/formatLambdaLogEvent.js @@ -4,7 +4,7 @@ const moment = require('moment'); const chalk = require('chalk'); const os = require('os'); -module.exports = (msgParam) => { +module.exports = msgParam => { let msg = msgParam; const dateFormat = 'YYYY-MM-DD HH:mm:ss.SSS (Z)'; @@ -27,10 +27,10 @@ module.exports = (msgParam) => { let date = ''; let reqId = ''; let level = ''; - if (!isNaN((new Date(splitted[0])).getTime())) { + if (!isNaN(new Date(splitted[0]).getTime())) { date = splitted[0]; reqId = splitted[1]; - } else if (!isNaN((new Date(splitted[1])).getTime())) { + } else if (!isNaN(new Date(splitted[1]).getTime())) { date = splitted[1]; reqId = splitted[2]; level = `${splitted[0]}\t`; diff --git a/lib/plugins/aws/utils/formatLambdaLogEvent.test.js b/lib/plugins/aws/utils/formatLambdaLogEvent.test.js index a5d4639df..dacc6cc0f 100644 --- a/lib/plugins/aws/utils/formatLambdaLogEvent.test.js +++ b/lib/plugins/aws/utils/formatLambdaLogEvent.test.js @@ -8,7 +8,8 @@ const formatLambdaLogEvent = require('./formatLambdaLogEvent'); describe('#formatLambdaLogEvent()', () => { it('should format invocation report', () => { - const msg = 'REPORT\tRequestId: 99c30000-b01a-11e5-93f7-b8e85631a00e\tDuration: 0.40 ms\tBilled Duration: 100 ms\tMemory Size: 512 MB\tMax Memory Used: 30 MB'; // eslint-disable-line + const msg = + 'REPORT\tRequestId: 99c30000-b01a-11e5-93f7-b8e85631a00e\tDuration: 0.40 ms\tBilled Duration: 100 ms\tMemory Size: 512 MB\tMax Memory Used: 30 MB'; // eslint-disable-line expect(formatLambdaLogEvent(msg)).to.equal(chalk.grey(msg + os.EOL)); }); @@ -31,7 +32,8 @@ describe('#formatLambdaLogEvent()', () => { }); it('should format lambda python logger lines', () => { - const pythonLoggerLine = '[INFO]\t2016-01-01T12:00:00Z\t99c30000-b01a-11e5-93f7-b8e85631a00e\ttest'; // eslint-disable-line + const pythonLoggerLine = + '[INFO]\t2016-01-01T12:00:00Z\t99c30000-b01a-11e5-93f7-b8e85631a00e\ttest'; // eslint-disable-line let expectedLogMessage = ''; const momentDate = moment('2016-01-01T12:00:00Z').format('YYYY-MM-DD HH:mm:ss.SSS (Z)'); diff --git a/lib/plugins/aws/utils/getS3ObjectsFromStacks.js b/lib/plugins/aws/utils/getS3ObjectsFromStacks.js index 1a98dc822..8d55d0f7f 100644 --- a/lib/plugins/aws/utils/getS3ObjectsFromStacks.js +++ b/lib/plugins/aws/utils/getS3ObjectsFromStacks.js @@ -2,8 +2,7 @@ const _ = require('lodash'); -module.exports = (stacks, prefix, service, stage) => ( - _.flatten(stacks).map((entry) => ( - { Key: `${prefix}/${service}/${stage}/${entry.directory}/${entry.file}` }) - ) -); +module.exports = (stacks, prefix, service, stage) => + _.flatten(stacks).map(entry => ({ + Key: `${prefix}/${service}/${stage}/${entry.directory}/${entry.file}`, + })); diff --git a/lib/plugins/aws/utils/getS3ObjectsFromStacks.test.js b/lib/plugins/aws/utils/getS3ObjectsFromStacks.test.js index df6f4beda..1b1d2dbee 100644 --- a/lib/plugins/aws/utils/getS3ObjectsFromStacks.test.js +++ b/lib/plugins/aws/utils/getS3ObjectsFromStacks.test.js @@ -34,10 +34,16 @@ describe('#getS3ObjectsFromStacks()', () => { const expected = [ // eslint-disable-next-line max-len - { Key: 'serverless/test/dev/1476779096930-2016-10-18T08:24:56.930Z/compiled-cloudformation-template.json' }, + { + Key: + 'serverless/test/dev/1476779096930-2016-10-18T08:24:56.930Z/compiled-cloudformation-template.json', + }, { Key: 'serverless/test/dev/1476779096930-2016-10-18T08:24:56.930Z/test.zip' }, // eslint-disable-next-line max-len - { Key: 'serverless/test/dev/1476779278222-2016-10-18T08:27:58.222Z/compiled-cloudformation-template.json' }, + { + Key: + 'serverless/test/dev/1476779278222-2016-10-18T08:27:58.222Z/compiled-cloudformation-template.json', + }, { Key: 'serverless/test/dev/1476779278222-2016-10-18T08:27:58.222Z/test.zip' }, ]; diff --git a/lib/plugins/config/config.js b/lib/plugins/config/config.js index 357e4e0f0..35cf6bfd4 100644 --- a/lib/plugins/config/config.js +++ b/lib/plugins/config/config.js @@ -4,10 +4,7 @@ const BbPromise = require('bluebird'); const userStats = require('../../utils/userStats'); // class wide constants -const validProviders = [ - 'aws', - 'spotinst', -]; +const validProviders = ['aws', 'spotinst']; // TODO: update to look like the list in the "create" plugin // once more than one provider is supported @@ -25,9 +22,7 @@ class Config { commands: { credentials: { usage: 'Configures a new provider profile for the Serverless Framework', - lifecycleEvents: [ - 'config', - ], + lifecycleEvents: ['config'], options: { provider: { usage: `Name of the provider. Supported providers: ${humanReadableProvidersList}`, diff --git a/lib/plugins/config/config.test.js b/lib/plugins/config/config.test.js index 1ef172c50..7ea4d175b 100644 --- a/lib/plugins/config/config.test.js +++ b/lib/plugins/config/config.test.js @@ -29,9 +29,7 @@ describe('Config', () => { }); it('should have the lifecycle event "config" for the "credentials" sub-command', () => { - expect(config.commands.config.commands.credentials.lifecycleEvents).to.deep.equal([ - 'config', - ]); + expect(config.commands.config.commands.credentials.lifecycleEvents).to.deep.equal(['config']); }); it('should have a required option "provider" for the "credentials" sub-command', () => { @@ -44,8 +42,7 @@ describe('Config', () => { }); it('should run promise chain in order for "before:config:credentials:config" hook', () => { - const configStub = sinon - .stub(config, 'validate').resolves(); + const configStub = sinon.stub(config, 'validate').resolves(); return config.hooks['before:config:credentials:config']().then(() => { expect(configStub.calledOnce).to.equal(true); @@ -61,7 +58,7 @@ describe('Config', () => { expect(() => config.validate()).to.throw(Error); }); - it('should resolve if user passed supported "provider" option', (done) => { + it('should resolve if user passed supported "provider" option', done => { config.options.provider = 'aws'; // aws is one example for a valid provider config.validate().then(() => done()); diff --git a/lib/plugins/create/create.js b/lib/plugins/create/create.js index fcc4d9b4a..9ebee0a93 100644 --- a/lib/plugins/create/create.js +++ b/lib/plugins/create/create.js @@ -7,6 +7,7 @@ const _ = require('lodash'); const untildify = require('untildify'); const ServerlessError = require('../../classes/Error').ServerlessError; +const createFromTemplate = require('../../utils/createFromTemplate'); const userStats = require('../../utils/userStats'); const download = require('../../utils/downloadTemplateFromRepo'); const renameService = require('../../utils/renameService').renameService; @@ -65,8 +66,10 @@ const validTemplates = [ 'hello-world', ]; -const humanReadableTemplateList = `${validTemplates.slice(0, -1) - .map((template) => `"${template}"`).join(', ')} and "${validTemplates.slice(-1)}"`; +const humanReadableTemplateList = `${validTemplates + .slice(0, -1) + .map(template => `"${template}"`) + .join(', ')} and "${validTemplates.slice(-1)}"`; class Create { constructor(serverless, options) { @@ -76,11 +79,9 @@ class Create { this.commands = { create: { usage: 'Create new Serverless service', - lifecycleEvents: [ - 'create', - ], + lifecycleEvents: ['create'], options: { - template: { + 'template': { usage: `Template for the service. Available templates: ${humanReadableTemplateList}`, shortcut: 't', }, @@ -91,11 +92,11 @@ class Create { 'template-path': { usage: 'Template local path for the service.', }, - path: { + 'path': { usage: 'The path where the service should be created (e.g. --path my-service)', shortcut: 'p', }, - name: { + 'name': { usage: 'Name for the service. Overwrites the default name of the created service.', shortcut: 'n', }, @@ -104,8 +105,7 @@ class Create { }; this.hooks = { - 'create:create': () => BbPromise.bind(this) - .then(this.create), + 'create:create': () => BbPromise.bind(this).then(this.create), }; } @@ -113,18 +113,22 @@ class Create { this.serverless.cli.log('Generating boilerplate...'); if ('template' in this.options) { - this.createFromTemplate(); + return this.createFromTemplate(); } else if ('template-url' in this.options) { - return download.downloadTemplateFromRepo( - this.options['template-url'], - this.options.name, - this.options.path - ) + return download + .downloadTemplateFromRepo( + this.options['template-url'], + this.options.name, + this.options.path + ) .then(serviceName => { const message = [ `Successfully installed "${serviceName}" `, - `${this.options.name && - this.options.name !== serviceName ? `as "${this.options.name}"` : ''}`, + `${ + this.options.name && this.options.name !== serviceName + ? `as "${this.options.name}"` + : '' + }`, ].join(''); this.serverless.cli.log(message); @@ -176,8 +180,13 @@ class Create { // store the custom options for the service if given const boilerplatePath = _.toString(this.options.path); const serviceName = _.toString(this.options.name); - const templateSrcDir = path.join(this.serverless.config.serverlessPath, - 'plugins', 'create', 'templates', this.options.template); + const templateSrcDir = path.join( + this.serverless.config.serverlessPath, + 'plugins', + 'create', + 'templates', + this.options.template + ); // create (if not yet present) and chdir into the directory for the service if (boilerplatePath) { @@ -217,47 +226,41 @@ class Create { this.serverless.config.update({ servicePath: process.cwd() }); } - // copy template files recursively to cwd - // while keeping template file tree - try { - this.serverless.utils.copyDirContentsSync(templateSrcDir, process.cwd()); + return createFromTemplate(this.options.template, process.cwd()).then( + () => { + // rename the service if the user has provided a path via options and is creating a service + if ((boilerplatePath || serviceName) && notPlugin) { + const newServiceName = serviceName || boilerplatePath.split(path.sep).pop(); - // NPM renames .gitignore to .npmignore on publish so we have to rename it. - if (fse.existsSync(path.join(process.cwd(), 'gitignore'))) { - fse.renameSync(path.join(process.cwd(), 'gitignore'), - path.join(process.cwd(), '.gitignore')); + renameService(newServiceName, this.serverless.config.servicePath); + } + + userStats.track('service_created', { + template: this.options.template, + serviceName, + }); + + this.serverless.cli.asciiGreeting(); + this.serverless.cli.log( + `Successfully generated boilerplate for template: "${this.options.template}"` + ); + + if (!(boilerplatePath || serviceName) && notPlugin) { + this.serverless.cli.log( + 'NOTE: Please update the "service" property in serverless.yml with your service name' + ); + } + }, + () => { + const errorMessage = [ + 'Error unable to create a service in this directory. ', + 'Please check that you have the required permissions to write to the directory', + ].join(''); + + throw new this.serverless.classes.Error(errorMessage); } - } catch (err) { - const errorMessage = [ - 'Error unable to create a service in this directory. ', - 'Please check that you have the required permissions to write to the directory', - ].join(''); - - throw new this.serverless.classes.Error(errorMessage); - } - - // rename the service if the user has provided a path via options and is creating a service - if ((boilerplatePath || serviceName) && notPlugin) { - const newServiceName = serviceName || boilerplatePath.split(path.sep).pop(); - - renameService(newServiceName, this.serverless.config.servicePath); - } - - userStats.track('service_created', { - template: this.options.template, - serviceName, - }); - - this.serverless.cli.asciiGreeting(); - this.serverless.cli - .log(`Successfully generated boilerplate for template: "${this.options.template}"`); - - if (!(boilerplatePath || serviceName) && notPlugin) { - this.serverless.cli - .log('NOTE: Please update the "service" property in serverless.yml with your service name'); - } + ); } - } module.exports = Create; diff --git a/lib/plugins/create/create.test.js b/lib/plugins/create/create.test.js index dcd44fc5b..683dc8b24 100644 --- a/lib/plugins/create/create.test.js +++ b/lib/plugins/create/create.test.js @@ -1,15 +1,19 @@ 'use strict'; -const expect = require('chai').expect; +const chai = require('chai'); const fs = require('fs'); const path = require('path'); const fse = require('fs-extra'); const Create = require('./create'); const Serverless = require('../../Serverless'); const sinon = require('sinon'); -const testUtils = require('../../../tests/utils'); const walkDirSync = require('../../utils/fs/walkDirSync'); const download = require('../../utils/downloadTemplateFromRepo'); +const userStats = require('../../utils/userStats'); +const { getTmpDirPath } = require('../../../tests/utils/fs'); + +chai.use(require('sinon-chai')); +const { expect } = require('chai'); describe('Create', () => { let create; @@ -21,6 +25,11 @@ describe('Create', () => { create = new Create(serverless, options); create.serverless.cli = new serverless.classes.CLI(); logSpy = sinon.spy(create.serverless.cli, 'log'); + sinon.stub(userStats, 'track').resolves(); + }); + + after(() => { + userStats.track.restore(); }); describe('#constructor()', () => { @@ -29,8 +38,7 @@ describe('Create', () => { it('should have hooks', () => expect(create.hooks).to.be.not.empty); it('should run promise chain in order for "create:create" hook', () => { - const createStub = sinon - .stub(create, 'create').resolves(); + const createStub = sinon.stub(create, 'create').resolves(); return create.hooks['create:create']().then(() => { expect(createStub.calledOnce).to.be.equal(true); @@ -45,7 +53,7 @@ describe('Create', () => { let cwd; beforeEach(() => { - tmpDir = testUtils.getTmpDirPath(); + tmpDir = getTmpDirPath(); fse.mkdirsSync(tmpDir); cwd = process.cwd(); }); @@ -70,7 +78,7 @@ describe('Create', () => { downloadStub.rejects(new Error('Wrong')); create.options['template-url'] = 'https://github.com/serverless/serverless'; - return create.create().catch((error) => { + return create.create().catch(error => { expect(error).to.match(/Wrong/); expect(downloadStub).to.have.been.calledOnce; // eslint-disable-line }); @@ -81,8 +89,9 @@ describe('Create', () => { create.options['template-url'] = 'https://github.com/serverless/serverless'; return create.create().then(() => { - const installationMessage = - logSpy.args.filter(arg => arg[0].includes('installed "serverless"')); + const installationMessage = logSpy.args.filter(arg => + arg[0].includes('installed "serverless"') + ); expect(downloadStub).to.have.been.calledOnce; // eslint-disable-line expect(installationMessage[0]).to.have.lengthOf(1); @@ -95,8 +104,9 @@ describe('Create', () => { create.options.name = 'sls'; return create.create().then(() => { - const installationMessage = - logSpy.args.filter(arg => arg[0].includes('installed "serverless" as "sls"')); + const installationMessage = logSpy.args.filter(arg => + arg[0].includes('installed "serverless" as "sls"') + ); expect(downloadStub).to.have.been.calledOnce; // eslint-disable-line expect(installationMessage[0]).to.have.lengthOf(1); @@ -115,9 +125,7 @@ describe('Create', () => { create.options.name = 'my_service'; return create.create().then(() => - create.serverless.yamlParser.parse( - path.join(tmpDir, 'serverless.yml') - ).then((obj) => { + create.serverless.yamlParser.parse(path.join(tmpDir, 'serverless.yml')).then(obj => { expect(obj.service).to.equal('my_service'); }) ); @@ -145,21 +153,18 @@ describe('Create', () => { create.options.template = 'aws-clojure-gradle'; return create.create().then(() => { - const dirContent = walkDirSync(tmpDir) - .map(elem => elem.replace(path.join(tmpDir, path.sep), '')); + const dirContent = walkDirSync(tmpDir).map(elem => + elem.replace(path.join(tmpDir, path.sep), '') + ); expect(dirContent).to.include('serverless.yml'); expect(dirContent).to.include('build.gradle'); expect(dirContent).to.include('gradlew'); expect(dirContent).to.include('gradlew.bat'); - expect(dirContent).to.include(path.join('gradle', 'wrapper', - 'gradle-wrapper.jar')); - expect(dirContent).to.include(path.join('gradle', 'wrapper', - 'gradle-wrapper.properties')); - expect(dirContent).to.include(path.join('src', 'main', 'resources', - 'log4j.properties')); - expect(dirContent).to.include(path.join('src', 'main', 'clojure', - 'hello.clj')); + expect(dirContent).to.include(path.join('gradle', 'wrapper', 'gradle-wrapper.jar')); + expect(dirContent).to.include(path.join('gradle', 'wrapper', 'gradle-wrapper.properties')); + expect(dirContent).to.include(path.join('src', 'main', 'resources', 'log4j.properties')); + expect(dirContent).to.include(path.join('src', 'main', 'clojure', 'hello.clj')); expect(dirContent).to.include('.gitignore'); }); }); @@ -169,19 +174,19 @@ describe('Create', () => { create.options.template = 'aws-clojurescript-gradle'; return create.create().then(() => { - const dirContent = walkDirSync(tmpDir) - .map(elem => elem.replace(path.join(tmpDir, path.sep), '')); + const dirContent = walkDirSync(tmpDir).map(elem => + elem.replace(path.join(tmpDir, path.sep), '') + ); expect(dirContent).to.include('serverless.yml'); expect(dirContent).to.include('build.gradle'); expect(dirContent).to.include('gradlew'); expect(dirContent).to.include('gradlew.bat'); - expect(dirContent).to.include(path.join('gradle', 'wrapper', - 'gradle-wrapper.jar')); - expect(dirContent).to.include(path.join('gradle', 'wrapper', - 'gradle-wrapper.properties')); - expect(dirContent).to.include(path.join('src', 'main', 'clojurescript', - 'serverless', 'functions.cljs')); + expect(dirContent).to.include(path.join('gradle', 'wrapper', 'gradle-wrapper.jar')); + expect(dirContent).to.include(path.join('gradle', 'wrapper', 'gradle-wrapper.properties')); + expect(dirContent).to.include( + path.join('src', 'main', 'clojurescript', 'serverless', 'functions.cljs') + ); expect(dirContent).to.include(path.join('scripts', 'node_repl.clj')); expect(dirContent).to.include('.gitignore'); }); @@ -214,28 +219,6 @@ describe('Create', () => { expect(dirContent).to.include('.gitignore'); }); }); - it('should generate scaffolding for "aws-nodejs-typescript" ' + - 'template and override service name if user passed', () => { - process.chdir(tmpDir); - create.options.template = 'aws-nodejs-typescript'; - create.options.name = 'my-awesome-service'; - - return create.create().then(() => { - const dirContent = fs.readdirSync(tmpDir); - expect(dirContent).to.include('serverless.yml'); - expect(dirContent).to.include('handler.ts'); - expect(dirContent).to.include('tsconfig.json'); - expect(dirContent).to.include('package.json'); - expect(dirContent).to.include('webpack.config.js'); - expect(dirContent).to.include('.gitignore'); - - // check if the service was renamed - const serverlessYmlfileContent = fse - .readFileSync(path.join(tmpDir, 'serverless.yml')).toString(); - expect((/service:\n {2}name: my-awesome-service/) - .test(serverlessYmlfileContent)).to.equal(true); - }); - }); it('should generate scaffolding for "aws-alexa-typescript" template', () => { process.chdir(tmpDir); @@ -251,28 +234,6 @@ describe('Create', () => { expect(dirContent).to.include('.gitignore'); }); }); - it('should generate scaffolding for "aws-alexa-typescript" ' + - 'template and override service name if user passed', () => { - process.chdir(tmpDir); - create.options.template = 'aws-alexa-typescript'; - create.options.name = 'my-awesome-service'; - - return create.create().then(() => { - const dirContent = fs.readdirSync(tmpDir); - expect(dirContent).to.include('serverless.yml'); - expect(dirContent).to.include('handler.ts'); - expect(dirContent).to.include('tsconfig.json'); - expect(dirContent).to.include('package.json'); - expect(dirContent).to.include('webpack.config.js'); - expect(dirContent).to.include('.gitignore'); - - // check if the service was renamed - const serverlessYmlfileContent = fse - .readFileSync(path.join(tmpDir, 'serverless.yml')).toString(); - expect((/service:\n {2}name: my-awesome-service/) - .test(serverlessYmlfileContent)).to.equal(true); - }); - }); it('should generate scaffolding for "aws-nodejs-ecma-script" template', () => { process.chdir(tmpDir); @@ -349,18 +310,22 @@ describe('Create', () => { create.options.template = 'aws-java-maven'; return create.create().then(() => { - const dirContent = walkDirSync(tmpDir) - .map(elem => elem.replace(path.join(tmpDir, path.sep), '')); + const dirContent = walkDirSync(tmpDir).map(elem => + elem.replace(path.join(tmpDir, path.sep), '') + ); expect(dirContent).to.include('serverless.yml'); expect(dirContent).to.include('pom.xml'); expect(dirContent).to.include(path.join('src', 'main', 'resources', 'log4j2.xml')); - expect(dirContent).to.include(path.join('src', 'main', 'java', 'com', 'serverless', - 'Handler.java')); - expect(dirContent).to.include(path.join('src', 'main', 'java', 'com', 'serverless', - 'ApiGatewayResponse.java')); - expect(dirContent).to.include(path.join('src', 'main', 'java', 'com', 'serverless', - 'Response.java')); + expect(dirContent).to.include( + path.join('src', 'main', 'java', 'com', 'serverless', 'Handler.java') + ); + expect(dirContent).to.include( + path.join('src', 'main', 'java', 'com', 'serverless', 'ApiGatewayResponse.java') + ); + expect(dirContent).to.include( + path.join('src', 'main', 'java', 'com', 'serverless', 'Response.java') + ); expect(dirContent).to.include(path.join('.gitignore')); }); }); @@ -370,17 +335,21 @@ describe('Create', () => { create.options.template = 'aws-kotlin-jvm-maven'; return create.create().then(() => { - const dirContent = walkDirSync(tmpDir) - .map(elem => elem.replace(path.join(tmpDir, path.sep), '')); + const dirContent = walkDirSync(tmpDir).map(elem => + elem.replace(path.join(tmpDir, path.sep), '') + ); expect(dirContent).to.include('serverless.yml'); expect(dirContent).to.include('pom.xml'); - expect(dirContent).to.include(path.join('src', 'main', 'kotlin', 'com', 'serverless', - 'Handler.kt')); - expect(dirContent).to.include(path.join('src', 'main', 'kotlin', 'com', 'serverless', - 'ApiGatewayResponse.kt')); - expect(dirContent).to.include(path.join('src', 'main', 'kotlin', 'com', 'serverless', - 'Response.kt')); + expect(dirContent).to.include( + path.join('src', 'main', 'kotlin', 'com', 'serverless', 'Handler.kt') + ); + expect(dirContent).to.include( + path.join('src', 'main', 'kotlin', 'com', 'serverless', 'ApiGatewayResponse.kt') + ); + expect(dirContent).to.include( + path.join('src', 'main', 'kotlin', 'com', 'serverless', 'Response.kt') + ); expect(dirContent).to.include(path.join('src', 'test', 'kotlin', '.gitkeep')); expect(dirContent).to.include(path.join('.gitignore')); }); @@ -391,17 +360,21 @@ describe('Create', () => { create.options.template = 'aws-kotlin-jvm-gradle'; return create.create().then(() => { - const dirContent = walkDirSync(tmpDir) - .map(elem => elem.replace(path.join(tmpDir, path.sep), '')); + const dirContent = walkDirSync(tmpDir).map(elem => + elem.replace(path.join(tmpDir, path.sep), '') + ); expect(dirContent).to.include('serverless.yml'); expect(dirContent).to.include('build.gradle'); - expect(dirContent).to.include(path.join('src', 'main', 'kotlin', 'com', 'serverless', - 'Handler.kt')); - expect(dirContent).to.include(path.join('src', 'main', 'kotlin', 'com', 'serverless', - 'ApiGatewayResponse.kt')); - expect(dirContent).to.include(path.join('src', 'main', 'kotlin', 'com', 'serverless', - 'Response.kt')); + expect(dirContent).to.include( + path.join('src', 'main', 'kotlin', 'com', 'serverless', 'Handler.kt') + ); + expect(dirContent).to.include( + path.join('src', 'main', 'kotlin', 'com', 'serverless', 'ApiGatewayResponse.kt') + ); + expect(dirContent).to.include( + path.join('src', 'main', 'kotlin', 'com', 'serverless', 'Response.kt') + ); expect(dirContent).to.include(path.join('src', 'test', 'kotlin', '.gitkeep')); expect(dirContent).to.include(path.join('.gitignore')); }); @@ -412,24 +385,26 @@ describe('Create', () => { create.options.template = 'aws-kotlin-nodejs-gradle'; return create.create().then(() => { - const dirContent = walkDirSync(tmpDir) - .map(elem => elem.replace(path.join(tmpDir, path.sep), '')); + const dirContent = walkDirSync(tmpDir).map(elem => + elem.replace(path.join(tmpDir, path.sep), '') + ); expect(dirContent).to.include('serverless.yml'); expect(dirContent).to.include('build.gradle'); expect(dirContent).to.include('gradlew'); expect(dirContent).to.include('gradlew.bat'); expect(dirContent).to.include('package.json'); - expect(dirContent).to.include(path.join('gradle', 'wrapper', - 'gradle-wrapper.jar')); - expect(dirContent).to.include(path.join('gradle', 'wrapper', - 'gradle-wrapper.properties')); - expect(dirContent).to.include(path.join('src', 'main', 'kotlin', 'com', 'serverless', - 'Handler.kt')); - expect(dirContent).to.include(path.join('src', 'main', 'kotlin', 'com', 'serverless', - 'ApiGatewayResponse.kt')); - expect(dirContent).to.include(path.join('src', 'main', 'kotlin', 'com', 'serverless', - 'Response.kt')); + expect(dirContent).to.include(path.join('gradle', 'wrapper', 'gradle-wrapper.jar')); + expect(dirContent).to.include(path.join('gradle', 'wrapper', 'gradle-wrapper.properties')); + expect(dirContent).to.include( + path.join('src', 'main', 'kotlin', 'com', 'serverless', 'Handler.kt') + ); + expect(dirContent).to.include( + path.join('src', 'main', 'kotlin', 'com', 'serverless', 'ApiGatewayResponse.kt') + ); + expect(dirContent).to.include( + path.join('src', 'main', 'kotlin', 'com', 'serverless', 'Response.kt') + ); expect(dirContent).to.include(path.join('src', 'test', 'kotlin', '.gitkeep')); expect(dirContent).to.include(path.join('.gitignore')); }); @@ -440,25 +415,26 @@ describe('Create', () => { create.options.template = 'aws-java-gradle'; return create.create().then(() => { - const dirContent = walkDirSync(tmpDir) - .map(elem => elem.replace(path.join(tmpDir, path.sep), '')); + const dirContent = walkDirSync(tmpDir).map(elem => + elem.replace(path.join(tmpDir, path.sep), '') + ); expect(dirContent).to.include('serverless.yml'); expect(dirContent).to.include('build.gradle'); expect(dirContent).to.include('gradlew'); expect(dirContent).to.include('gradlew.bat'); - expect(dirContent).to.include(path.join('gradle', 'wrapper', - 'gradle-wrapper.jar')); - expect(dirContent).to.include(path.join('gradle', 'wrapper', - 'gradle-wrapper.properties')); - expect(dirContent).to.include(path.join('src', 'main', 'resources', - 'log4j.properties')); - expect(dirContent).to.include(path.join('src', 'main', 'java', - 'com', 'serverless', 'Handler.java')); - expect(dirContent).to.include(path.join('src', 'main', 'java', - 'com', 'serverless', 'ApiGatewayResponse.java')); - expect(dirContent).to.include(path.join('src', 'main', 'java', - 'com', 'serverless', 'Response.java')); + expect(dirContent).to.include(path.join('gradle', 'wrapper', 'gradle-wrapper.jar')); + expect(dirContent).to.include(path.join('gradle', 'wrapper', 'gradle-wrapper.properties')); + expect(dirContent).to.include(path.join('src', 'main', 'resources', 'log4j.properties')); + expect(dirContent).to.include( + path.join('src', 'main', 'java', 'com', 'serverless', 'Handler.java') + ); + expect(dirContent).to.include( + path.join('src', 'main', 'java', 'com', 'serverless', 'ApiGatewayResponse.java') + ); + expect(dirContent).to.include( + path.join('src', 'main', 'java', 'com', 'serverless', 'Response.java') + ); expect(dirContent).to.include(path.join('.gitignore')); }); }); @@ -468,25 +444,26 @@ describe('Create', () => { create.options.template = 'aws-groovy-gradle'; return create.create().then(() => { - const dirContent = walkDirSync(tmpDir) - .map(elem => elem.replace(path.join(tmpDir, path.sep), '')); + const dirContent = walkDirSync(tmpDir).map(elem => + elem.replace(path.join(tmpDir, path.sep), '') + ); expect(dirContent).to.include('serverless.yml'); expect(dirContent).to.include('build.gradle'); expect(dirContent).to.include('gradlew'); expect(dirContent).to.include('gradlew.bat'); - expect(dirContent).to.include(path.join('gradle', 'wrapper', - 'gradle-wrapper.jar')); - expect(dirContent).to.include(path.join('gradle', 'wrapper', - 'gradle-wrapper.properties')); - expect(dirContent).to.include(path.join('src', 'main', 'resources', - 'log4j.properties')); - expect(dirContent).to.include(path.join('src', 'main', 'groovy', - 'com', 'serverless', 'Handler.groovy')); - expect(dirContent).to.include(path.join('src', 'main', 'groovy', - 'com', 'serverless', 'ApiGatewayResponse.groovy')); - expect(dirContent).to.include(path.join('src', 'main', 'groovy', - 'com', 'serverless', 'Response.groovy')); + expect(dirContent).to.include(path.join('gradle', 'wrapper', 'gradle-wrapper.jar')); + expect(dirContent).to.include(path.join('gradle', 'wrapper', 'gradle-wrapper.properties')); + expect(dirContent).to.include(path.join('src', 'main', 'resources', 'log4j.properties')); + expect(dirContent).to.include( + path.join('src', 'main', 'groovy', 'com', 'serverless', 'Handler.groovy') + ); + expect(dirContent).to.include( + path.join('src', 'main', 'groovy', 'com', 'serverless', 'ApiGatewayResponse.groovy') + ); + expect(dirContent).to.include( + path.join('src', 'main', 'groovy', 'com', 'serverless', 'Response.groovy') + ); expect(dirContent).to.include('.gitignore'); }); }); @@ -496,17 +473,15 @@ describe('Create', () => { create.options.template = 'aws-scala-sbt'; return create.create().then(() => { - const dirContent = walkDirSync(tmpDir) - .map(elem => elem.replace(path.join(tmpDir, path.sep), '')); + const dirContent = walkDirSync(tmpDir).map(elem => + elem.replace(path.join(tmpDir, path.sep), '') + ); expect(dirContent).to.include('serverless.yml'); expect(dirContent).to.include('build.sbt'); - expect(dirContent).to.include(path.join('src', 'main', 'scala', - 'hello', 'Handler.scala')); - expect(dirContent).to.include(path.join('src', 'main', 'scala', - 'hello', 'Request.scala')); - expect(dirContent).to.include(path.join('src', 'main', 'scala', - 'hello', 'Response.scala')); + expect(dirContent).to.include(path.join('src', 'main', 'scala', 'hello', 'Handler.scala')); + expect(dirContent).to.include(path.join('src', 'main', 'scala', 'hello', 'Request.scala')); + expect(dirContent).to.include(path.join('src', 'main', 'scala', 'hello', 'Response.scala')); expect(dirContent).to.include('.gitignore'); }); }); @@ -516,13 +491,16 @@ describe('Create', () => { create.options.template = 'openwhisk-java-maven'; return create.create().then(() => { - const dirContent = walkDirSync(tmpDir) - .map(elem => elem.replace(path.join(tmpDir, path.sep), '')); + const dirContent = walkDirSync(tmpDir).map(elem => + elem.replace(path.join(tmpDir, path.sep), '') + ); expect(dirContent).to.include('pom.xml'); - expect(dirContent).to.include(path.join('src', 'main', 'java', - 'com', 'example', 'FunctionApp.java')); - expect(dirContent).to.include(path.join('src', 'test', 'java', - 'com', 'example', 'FunctionAppTest.java')); + expect(dirContent).to.include( + path.join('src', 'main', 'java', 'com', 'example', 'FunctionApp.java') + ); + expect(dirContent).to.include( + path.join('src', 'test', 'java', 'com', 'example', 'FunctionAppTest.java') + ); expect(dirContent).to.include('.gitignore'); expect(dirContent).to.include('serverless.yml'); }); @@ -602,30 +580,10 @@ describe('Create', () => { expect(dirContent).to.include('package.json'); expect(dirContent).to.include('serverless.yml'); expect(dirContent).to.include('.gitignore'); - expect(dirContent).to.include('.funcignore'); expect(dirContent).to.include('host.json'); - expect(dirContent).to.include('local.settings.json'); - expect(dirContent).to.include('proxies.json'); expect(dirContent).to.include('README.md'); - // VS Code directory for local development - expect(dirContent).to.include('.vscode'); - const vsCodeDirContent = fs.readdirSync(`${tmpDir}/.vscode`); - expect(vsCodeDirContent).to.include('extensions.json'); - expect(vsCodeDirContent).to.include('launch.json'); - expect(vsCodeDirContent).to.include('settings.json'); - expect(vsCodeDirContent).to.include('tasks.json'); - // Directory containing first function handler and bindings - expect(dirContent).to.include('hello'); - const helloFunctionDirContent = fs.readdirSync(`${tmpDir}/hello`); - expect(helloFunctionDirContent).to.include('function.json'); - expect(helloFunctionDirContent).to.include('index.js'); - expect(helloFunctionDirContent).to.include('sample.dat'); - // Directory containing second function handler and bindings - expect(dirContent).to.include('goodbye'); - const goodbyeFunctionDirContent = fs.readdirSync(`${tmpDir}/goodbye`); - expect(goodbyeFunctionDirContent).to.include('function.json'); - expect(goodbyeFunctionDirContent).to.include('index.js'); - expect(goodbyeFunctionDirContent).to.include('sample.dat'); + expect(dirContent).to.include('hello.js'); + expect(dirContent).to.include('goodbye.js'); }); }); @@ -781,14 +739,16 @@ describe('Create', () => { create.options.template = 'spotinst-java8'; return create.create().then(() => { - const dirContent = walkDirSync(tmpDir) - .map(elem => elem.replace(path.join(tmpDir, path.sep), '')); + const dirContent = walkDirSync(tmpDir).map(elem => + elem.replace(path.join(tmpDir, path.sep), '') + ); expect(dirContent).to.include('package.json'); expect(dirContent).to.include('serverless.yml'); expect(dirContent).to.include('pom.xml'); - expect(dirContent).to.include(path.join('src', 'main', 'java', - 'com', 'serverless', 'Handler.java')); + expect(dirContent).to.include( + path.join('src', 'main', 'java', 'com', 'serverless', 'Handler.java') + ); expect(dirContent).to.include('.gitignore'); }); }); @@ -798,8 +758,9 @@ describe('Create', () => { create.options.template = 'fn-nodejs'; return create.create().then(() => { - const dirContent = walkDirSync(tmpDir) - .map(elem => elem.replace(path.join(tmpDir, path.sep), '')); + const dirContent = walkDirSync(tmpDir).map(elem => + elem.replace(path.join(tmpDir, path.sep), '') + ); expect(dirContent).to.include('package.json'); expect(dirContent).to.include('serverless.yml'); expect(dirContent).to.include('.gitignore'); @@ -814,8 +775,9 @@ describe('Create', () => { create.options.template = 'fn-go'; return create.create().then(() => { - const dirContent = walkDirSync(tmpDir) - .map(elem => elem.replace(path.join(tmpDir, path.sep), '')); + const dirContent = walkDirSync(tmpDir).map(elem => + elem.replace(path.join(tmpDir, path.sep), '') + ); expect(dirContent).to.include('package.json'); expect(dirContent).to.include('serverless.yml'); expect(dirContent).to.include('.gitignore'); @@ -880,9 +842,10 @@ describe('Create', () => { // check if the service was renamed const serverlessYmlfileContent = fse - .readFileSync(path.join(serviceDir, 'serverless.yml')).toString(); + .readFileSync(path.join(serviceDir, 'serverless.yml')) + .toString(); - expect((/service: my-new-service/).test(serverlessYmlfileContent)).to.equal(true); + expect(/service: my-new-service/.test(serverlessYmlfileContent)).to.equal(true); }); }); @@ -905,31 +868,35 @@ describe('Create', () => { }); }); - it('should create a custom renamed service in the directory if using ' + - 'the "path" and "name" option', () => { - process.chdir(tmpDir); + it( + 'should create a custom renamed service in the directory if using ' + + 'the "path" and "name" option', + () => { + process.chdir(tmpDir); - create.options.path = 'my-new-service'; - create.options.name = 'my-custom-new-service'; + create.options.path = 'my-new-service'; + create.options.name = 'my-custom-new-service'; - // using the nodejs template (this test is completely be independent from the template) - create.options.template = 'aws-nodejs'; + // using the nodejs template (this test is completely be independent from the template) + create.options.template = 'aws-nodejs'; - return create.create().then(() => { - const serviceDir = path.join(tmpDir, create.options.path); - const dirContent = fs.readdirSync(serviceDir); + return create.create().then(() => { + const serviceDir = path.join(tmpDir, create.options.path); + const dirContent = fs.readdirSync(serviceDir); - // check if files are created in the correct directory - expect(dirContent).to.include('serverless.yml'); - expect(dirContent).to.include('handler.js'); + // check if files are created in the correct directory + expect(dirContent).to.include('serverless.yml'); + expect(dirContent).to.include('handler.js'); - // check if the service was renamed - const serverlessYmlfileContent = fse - .readFileSync(path.join(serviceDir, 'serverless.yml')).toString(); + // check if the service was renamed + const serverlessYmlfileContent = fse + .readFileSync(path.join(serviceDir, 'serverless.yml')) + .toString(); - expect((/service: my-custom-new-service/).test(serverlessYmlfileContent)).to.equal(true); - }); - }); + expect(/service: my-custom-new-service/.test(serverlessYmlfileContent)).to.equal(true); + }); + } + ); it('should throw error if there are existing template files in cwd', () => { process.chdir(tmpDir); @@ -937,8 +904,16 @@ describe('Create', () => { // create existing files from nodejs template create.options.template = 'aws-nodejs'; create.options.path = ''; - create.serverless.utils.copyDirContentsSync(path.join(create.serverless.config.serverlessPath, - 'plugins', 'create', 'templates', create.options.template), tmpDir); + create.serverless.utils.copyDirContentsSync( + path.join( + create.serverless.config.serverlessPath, + 'plugins', + 'create', + 'templates', + create.options.template + ), + tmpDir + ); const dirContent = fs.readdirSync(tmpDir); @@ -973,9 +948,10 @@ describe('Create', () => { // check if the service was renamed const serverlessYmlfileContent = fse - .readFileSync(path.join(distDir, 'serverless.yml')).toString(); + .readFileSync(path.join(distDir, 'serverless.yml')) + .toString(); - expect((/service: aws-nodejs/).test(serverlessYmlfileContent)).to.equal(true); + expect(/service: aws-nodejs/.test(serverlessYmlfileContent)).to.equal(true); }); }); @@ -994,9 +970,10 @@ describe('Create', () => { // check if the service was renamed const serverlessYmlfileContent = fse - .readFileSync(path.join(tmpDir, 'my-awesome-service', 'serverless.yml')).toString(); + .readFileSync(path.join(tmpDir, 'my-awesome-service', 'serverless.yml')) + .toString(); - expect((/service: my-awesome-service/).test(serverlessYmlfileContent)).to.equal(true); + expect(/service: my-awesome-service/.test(serverlessYmlfileContent)).to.equal(true); }); }); @@ -1005,8 +982,9 @@ describe('Create', () => { create.options.template = 'aws-go'; return create.create().then(() => { - const dirContent = walkDirSync(tmpDir) - .map(elem => elem.replace(path.join(tmpDir, path.sep), '')); + const dirContent = walkDirSync(tmpDir).map(elem => + elem.replace(path.join(tmpDir, path.sep), '') + ); expect(dirContent).to.include('serverless.yml'); expect(dirContent).to.include(path.join('hello', 'main.go')); @@ -1021,8 +999,9 @@ describe('Create', () => { create.options.template = 'aws-go-dep'; return create.create().then(() => { - const dirContent = walkDirSync(tmpDir) - .map(elem => elem.replace(path.join(tmpDir, path.sep), '')); + const dirContent = walkDirSync(tmpDir).map(elem => + elem.replace(path.join(tmpDir, path.sep), '') + ); expect(dirContent).to.include('serverless.yml'); expect(dirContent).to.include(path.join('hello', 'main.go')); @@ -1038,8 +1017,9 @@ describe('Create', () => { create.options.template = 'aws-go-mod'; return create.create().then(() => { - const dirContent = walkDirSync(tmpDir) - .map(elem => elem.replace(path.join(tmpDir, path.sep), '')); + const dirContent = walkDirSync(tmpDir).map(elem => + elem.replace(path.join(tmpDir, path.sep), '') + ); expect(dirContent).to.include('serverless.yml'); expect(dirContent).to.include(path.join('hello', 'main.go')); @@ -1055,12 +1035,15 @@ describe('Create', () => { create.options.template = 'aws-ruby'; return create.create().then(() => { - expect(create.serverless.utils.fileExistsSync(path.join(tmpDir, 'serverless.yml'))) - .to.be.equal(true); - expect(create.serverless.utils.fileExistsSync(path.join(tmpDir, 'handler.rb'))) - .to.be.equal(true); - expect(create.serverless.utils.fileExistsSync(path.join(tmpDir, '.gitignore'))) - .to.be.equal(true); + expect( + create.serverless.utils.fileExistsSync(path.join(tmpDir, 'serverless.yml')) + ).to.be.equal(true); + expect(create.serverless.utils.fileExistsSync(path.join(tmpDir, 'handler.rb'))).to.be.equal( + true + ); + expect(create.serverless.utils.fileExistsSync(path.join(tmpDir, '.gitignore'))).to.be.equal( + true + ); }); }); @@ -1069,14 +1052,18 @@ describe('Create', () => { create.options.template = 'aws-provided'; return create.create().then(() => { - expect(create.serverless.utils.fileExistsSync(path.join(tmpDir, 'serverless.yml'))) - .to.be.equal(true); - expect(create.serverless.utils.fileExistsSync(path.join(tmpDir, 'handler.sh'))) - .to.be.equal(true); - expect(create.serverless.utils.fileExistsSync(path.join(tmpDir, '.gitignore'))) - .to.be.equal(true); - expect(create.serverless.utils.fileExistsSync(path.join(tmpDir, 'bootstrap'))) - .to.be.equal(true); + expect( + create.serverless.utils.fileExistsSync(path.join(tmpDir, 'serverless.yml')) + ).to.be.equal(true); + expect(create.serverless.utils.fileExistsSync(path.join(tmpDir, 'handler.sh'))).to.be.equal( + true + ); + expect(create.serverless.utils.fileExistsSync(path.join(tmpDir, '.gitignore'))).to.be.equal( + true + ); + expect(create.serverless.utils.fileExistsSync(path.join(tmpDir, 'bootstrap'))).to.be.equal( + true + ); }); }); }); diff --git a/lib/plugins/create/templates/aws-alexa-typescript/package.json b/lib/plugins/create/templates/aws-alexa-typescript/package.json index ba84ae3bd..58904e451 100644 --- a/lib/plugins/create/templates/aws-alexa-typescript/package.json +++ b/lib/plugins/create/templates/aws-alexa-typescript/package.json @@ -18,7 +18,6 @@ "typescript": "^3.2.4", "webpack": "^4.29.0" }, - "author": - "The serverless webpack authors (https://github.com/elastic-coders/serverless-webpack)", + "author": "The serverless webpack authors (https://github.com/elastic-coders/serverless-webpack)", "license": "MIT" } diff --git a/lib/plugins/create/templates/aws-alexa-typescript/serverless.yml b/lib/plugins/create/templates/aws-alexa-typescript/serverless.yml index 3dde4cb45..ec37bc364 100644 --- a/lib/plugins/create/templates/aws-alexa-typescript/serverless.yml +++ b/lib/plugins/create/templates/aws-alexa-typescript/serverless.yml @@ -1,5 +1,7 @@ service: name: aws-alexa-typescript +#app: your-app-name +#tenant: your-tenant-name plugins: - serverless-webpack @@ -7,14 +9,14 @@ plugins: provider: name: aws - runtime: nodejs8.10 + runtime: nodejs10.x custom: alexa: # Step 1: Run `sls alexa auth` to authenticate # Step 2: Run `sls alexa create --name "Serverless Alexa Typescript" --locale en-GB --type custom` to create a new skill skills: - # Step 3: Paste the skill id returned by the create command here: + # Step 3: Paste the skill id returned by the create command here: - id: amzn1.ask.skill.xxxx-xxxx-xxxx-xxxx-xxxx manifest: publishingInformation: diff --git a/lib/plugins/create/templates/aws-alexa-typescript/tsconfig.json b/lib/plugins/create/templates/aws-alexa-typescript/tsconfig.json index 20be6a1c6..583da3154 100644 --- a/lib/plugins/create/templates/aws-alexa-typescript/tsconfig.json +++ b/lib/plugins/create/templates/aws-alexa-typescript/tsconfig.json @@ -2,12 +2,8 @@ "compilerOptions": { "sourceMap": true, "target": "es6", - "lib": [ - "esnext" - ], + "lib": ["esnext"], "moduleResolution": "node" }, - "exclude": [ - "node_modules" - ] + "exclude": ["node_modules"] } diff --git a/lib/plugins/create/templates/aws-clojure-gradle/serverless.yml b/lib/plugins/create/templates/aws-clojure-gradle/serverless.yml index 0a68753e2..71b728895 100644 --- a/lib/plugins/create/templates/aws-clojure-gradle/serverless.yml +++ b/lib/plugins/create/templates/aws-clojure-gradle/serverless.yml @@ -13,6 +13,8 @@ service: aws-clojure # NOTE: update this with your service name +#app: your-app-name +#tenant: your-tenant-name # You can pin your service to only deploy with a specific Serverless version # Check out our docs for more details @@ -58,7 +60,6 @@ functions: handler: hello::handler tags: VERSION: ${file(build/build.json):version} - # The following are a few example events you can configure # NOTE: Please make sure to change your handler code to work with those events # Check the event documentation for details @@ -88,6 +89,12 @@ functions: # - cognitoUserPool: # pool: MyUserPool # trigger: PreSignUp +# - alb: +# listenerArn: arn:aws:elasticloadbalancing:us-east-1:XXXXXX:listener/app/my-load-balancer/50dc6c495c0c9188/ +# priority: 1 +# conditions: +# host: example.com +# path: /hello # Define function environment variables here # environment: diff --git a/lib/plugins/create/templates/aws-clojurescript-gradle/README.md b/lib/plugins/create/templates/aws-clojurescript-gradle/README.md index be8cfdeb6..1b08984a5 100644 --- a/lib/plugins/create/templates/aws-clojurescript-gradle/README.md +++ b/lib/plugins/create/templates/aws-clojurescript-gradle/README.md @@ -1,4 +1,3 @@ - # AWS Clojurescript Gradle Template This project compiles **Clojurescript** to a [NodeJS](https://nodejs.org/en/) module using the [Gradle Clojure Plugin](https://gradle-clojure.github.io/gradle-clojure/index.html). @@ -12,6 +11,7 @@ See [functions.cljs](./src/main/clojurescript/serverless/functions.cljs) as an e To include **NodeJS** dependencies, modify [build.gradle](./build.gradle) and add the module to the `closurescript .. npmDeps` section. ### Prerequisites + - Create an [Amazon Web Services](https://aws.amazon.com) account - Install and set-up [Serverless Framework CLI](https://serverless.com) - Install [Java 8](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) @@ -20,12 +20,12 @@ To include **NodeJS** dependencies, modify [build.gradle](./build.gradle) and ad - Install [Gradle](https://gradle.org/install/) ### Build and Deploy + - To build, run `./gradlew clean build` - To deploy, run `serverless deploy` - ### Using the Repl in IntelliJ Cursive IDE This project contains a [script](./scripts/node_repl.clj) the must be initialized in order to use the **Repl** in **IntelliJ**. -![](http://share.rowellbelen.com/5WvFH2+) \ No newline at end of file +![](http://share.rowellbelen.com/5WvFH2+) diff --git a/lib/plugins/create/templates/aws-clojurescript-gradle/serverless.yml b/lib/plugins/create/templates/aws-clojurescript-gradle/serverless.yml index 2b1f024fd..b71681702 100644 --- a/lib/plugins/create/templates/aws-clojurescript-gradle/serverless.yml +++ b/lib/plugins/create/templates/aws-clojurescript-gradle/serverless.yml @@ -12,6 +12,8 @@ # Happy Coding! service: aws-clojurescript-gradle +#app: your-app-name +#tenant: your-tenant-name # You can pin your service to only deploy with a specific Serverless version # Check out our docs for more details @@ -19,7 +21,7 @@ service: aws-clojurescript-gradle provider: name: aws - runtime: nodejs6.10 + runtime: nodejs10.x # you can overwrite defaults here # stage: dev @@ -57,17 +59,16 @@ functions: hello: handler: build/clojurescript/main/functions.hello events: - - http: - path: hello - method: get + - http: + path: hello + method: get now: handler: build/clojurescript/main/functions.now events: - - http: - path: now - method: get - + - http: + path: now + method: get # The following are a few example events you can configure # NOTE: Please make sure to change your handler code to work with those events # Check the event documentation for details @@ -97,6 +98,12 @@ functions: # - cognitoUserPool: # pool: MyUserPool # trigger: PreSignUp +# - alb: +# listenerArn: arn:aws:elasticloadbalancing:us-east-1:XXXXXX:listener/app/my-load-balancer/50dc6c495c0c9188/ +# priority: 1 +# conditions: +# host: example.com +# path: /hello # Define function environment variables here # environment: diff --git a/lib/plugins/create/templates/aws-csharp/serverless.yml b/lib/plugins/create/templates/aws-csharp/serverless.yml index 7350eac9b..f4f250a5f 100644 --- a/lib/plugins/create/templates/aws-csharp/serverless.yml +++ b/lib/plugins/create/templates/aws-csharp/serverless.yml @@ -12,6 +12,8 @@ # Happy Coding! service: aws-csharp # NOTE: update this with your service name +#app: your-app-name +#tenant: your-tenant-name # You can pin your service to only deploy with a specific Serverless version # Check out our docs for more details @@ -58,7 +60,6 @@ functions: # exclude: # - exclude-me.js # - exclude-me-dir/** - # The following are a few example events you can configure # NOTE: Please make sure to change your handler code to work with those events # Check the event documentation for details @@ -88,6 +89,12 @@ functions: # - cognitoUserPool: # pool: MyUserPool # trigger: PreSignUp +# - alb: +# listenerArn: arn:aws:elasticloadbalancing:us-east-1:XXXXXX:listener/app/my-load-balancer/50dc6c495c0c9188/ +# priority: 1 +# conditions: +# host: example.com +# path: /hello # Define function environment variables here # environment: diff --git a/lib/plugins/create/templates/aws-fsharp/serverless.yml b/lib/plugins/create/templates/aws-fsharp/serverless.yml index a81378e29..292a7053b 100644 --- a/lib/plugins/create/templates/aws-fsharp/serverless.yml +++ b/lib/plugins/create/templates/aws-fsharp/serverless.yml @@ -12,6 +12,8 @@ # Happy Coding! service: aws-fsharp # NOTE: update this with your service name +#app: your-app-name +#tenant: your-tenant-name # You can pin your service to only deploy with a specific Serverless version # Check out our docs for more details @@ -55,7 +57,6 @@ package: functions: hello: handler: FsharpHandlers::AwsDotnetFsharp.Handler::hello - # The following are a few example events you can configure # NOTE: Please make sure to change your handler code to work with those events # Check the event documentation for details @@ -85,6 +86,12 @@ functions: # - cognitoUserPool: # pool: MyUserPool # trigger: PreSignUp +# - alb: +# listenerArn: arn:aws:elasticloadbalancing:us-east-1:XXXXXX:listener/app/my-load-balancer/50dc6c495c0c9188/ +# priority: 1 +# conditions: +# host: example.com +# path: /hello # Define function environment variables here # environment: diff --git a/lib/plugins/create/templates/aws-go-dep/serverless.yml b/lib/plugins/create/templates/aws-go-dep/serverless.yml index 2ae460403..d9dd8badd 100644 --- a/lib/plugins/create/templates/aws-go-dep/serverless.yml +++ b/lib/plugins/create/templates/aws-go-dep/serverless.yml @@ -12,11 +12,13 @@ # Happy Coding! service: aws-go-dep # NOTE: update this with your service name +#app: your-app-name +#tenant: your-tenant-name # You can pin your service to only deploy with a specific Serverless version # Check out our docs for more details # frameworkVersion: "=X.X.X" -frameworkVersion: ">=1.28.0 <2.0.0" +frameworkVersion: '>=1.28.0 <2.0.0' provider: name: aws @@ -47,10 +49,10 @@ provider: # variable1: value1 package: - exclude: - - ./** - include: - - ./bin/** + exclude: + - ./** + include: + - ./bin/** functions: hello: @@ -65,7 +67,6 @@ functions: - http: path: world method: get - # The following are a few example events you can configure # NOTE: Please make sure to change your handler code to work with those events # Check the event documentation for details @@ -96,6 +97,12 @@ functions: # - cognitoUserPool: # pool: MyUserPool # trigger: PreSignUp +# - alb: +# listenerArn: arn:aws:elasticloadbalancing:us-east-1:XXXXXX:listener/app/my-load-balancer/50dc6c495c0c9188/ +# priority: 1 +# conditions: +# host: example.com +# path: /hello # Define function environment variables here # environment: diff --git a/lib/plugins/create/templates/aws-go-mod/serverless.yml b/lib/plugins/create/templates/aws-go-mod/serverless.yml index 896364433..137088cd1 100644 --- a/lib/plugins/create/templates/aws-go-mod/serverless.yml +++ b/lib/plugins/create/templates/aws-go-mod/serverless.yml @@ -12,11 +12,13 @@ # Happy Coding! service: aws-go-mod # NOTE: update this with your service name +#app: your-app-name +#tenant: your-tenant-name # You can pin your service to only deploy with a specific Serverless version # Check out our docs for more details # frameworkVersion: "=X.X.X" -frameworkVersion: ">=1.28.0 <2.0.0" +frameworkVersion: '>=1.28.0 <2.0.0' provider: name: aws @@ -47,10 +49,10 @@ provider: # variable1: value1 package: - exclude: - - ./** - include: - - ./bin/** + exclude: + - ./** + include: + - ./bin/** functions: hello: @@ -65,7 +67,6 @@ functions: - http: path: world method: get - # The following are a few example events you can configure # NOTE: Please make sure to change your handler code to work with those events # Check the event documentation for details @@ -96,6 +97,12 @@ functions: # - cognitoUserPool: # pool: MyUserPool # trigger: PreSignUp +# - alb: +# listenerArn: arn:aws:elasticloadbalancing:us-east-1:XXXXXX:listener/app/my-load-balancer/50dc6c495c0c9188/ +# priority: 1 +# conditions: +# host: example.com +# path: /hello # Define function environment variables here # environment: diff --git a/lib/plugins/create/templates/aws-go/serverless.yml b/lib/plugins/create/templates/aws-go/serverless.yml index 0feaf948f..f2e34e0fa 100644 --- a/lib/plugins/create/templates/aws-go/serverless.yml +++ b/lib/plugins/create/templates/aws-go/serverless.yml @@ -12,11 +12,13 @@ # Happy Coding! service: aws-go # NOTE: update this with your service name +#app: your-app-name +#tenant: your-tenant-name # You can pin your service to only deploy with a specific Serverless version # Check out our docs for more details # frameworkVersion: "=X.X.X" -frameworkVersion: ">=1.28.0 <2.0.0" +frameworkVersion: '>=1.28.0 <2.0.0' provider: name: aws @@ -47,10 +49,10 @@ provider: # variable1: value1 package: - exclude: - - ./** - include: - - ./bin/** + exclude: + - ./** + include: + - ./bin/** functions: hello: @@ -65,7 +67,6 @@ functions: - http: path: world method: get - # The following are a few example events you can configure # NOTE: Please make sure to change your handler code to work with those events # Check the event documentation for details @@ -96,6 +97,12 @@ functions: # - cognitoUserPool: # pool: MyUserPool # trigger: PreSignUp +# - alb: +# listenerArn: arn:aws:elasticloadbalancing:us-east-1:XXXXXX:listener/app/my-load-balancer/50dc6c495c0c9188/ +# priority: 1 +# conditions: +# host: example.com +# path: /hello # Define function environment variables here # environment: diff --git a/lib/plugins/create/templates/aws-groovy-gradle/serverless.yml b/lib/plugins/create/templates/aws-groovy-gradle/serverless.yml index fa290306e..553248644 100644 --- a/lib/plugins/create/templates/aws-groovy-gradle/serverless.yml +++ b/lib/plugins/create/templates/aws-groovy-gradle/serverless.yml @@ -12,6 +12,8 @@ # Happy Coding! service: aws-groovy-gradle # NOTE: update this with your service name +#app: your-app-name +#tenant: your-tenant-name # You can pin your service to only deploy with a specific Serverless version # Check out our docs for more details @@ -52,7 +54,6 @@ package: functions: hello: handler: com.serverless.Handler - # The following are a few example events you can configure # NOTE: Please make sure to change your handler code to work with those events # Check the event documentation for details @@ -82,6 +83,12 @@ functions: # - cognitoUserPool: # pool: MyUserPool # trigger: PreSignUp +# - alb: +# listenerArn: arn:aws:elasticloadbalancing:us-east-1:XXXXXX:listener/app/my-load-balancer/50dc6c495c0c9188/ +# priority: 1 +# conditions: +# host: example.com +# path: /hello # Define function environment variables here # environment: diff --git a/lib/plugins/create/templates/aws-java-gradle/serverless.yml b/lib/plugins/create/templates/aws-java-gradle/serverless.yml index 863e9b8a8..439b9431c 100644 --- a/lib/plugins/create/templates/aws-java-gradle/serverless.yml +++ b/lib/plugins/create/templates/aws-java-gradle/serverless.yml @@ -12,6 +12,8 @@ # Happy Coding! service: aws-java-gradle # NOTE: update this with your service name +#app: your-app-name +#tenant: your-tenant-name # You can pin your service to only deploy with a specific Serverless version # Check out our docs for more details @@ -52,7 +54,6 @@ package: functions: hello: handler: com.serverless.Handler - # The following are a few example events you can configure # NOTE: Please make sure to change your handler code to work with those events # Check the event documentation for details @@ -82,6 +83,12 @@ functions: # - cognitoUserPool: # pool: MyUserPool # trigger: PreSignUp +# - alb: +# listenerArn: arn:aws:elasticloadbalancing:us-east-1:XXXXXX:listener/app/my-load-balancer/50dc6c495c0c9188/ +# priority: 1 +# conditions: +# host: example.com +# path: /hello # Define function environment variables here # environment: diff --git a/lib/plugins/create/templates/aws-java-maven/serverless.yml b/lib/plugins/create/templates/aws-java-maven/serverless.yml index c42d11853..25f50aa50 100644 --- a/lib/plugins/create/templates/aws-java-maven/serverless.yml +++ b/lib/plugins/create/templates/aws-java-maven/serverless.yml @@ -12,6 +12,8 @@ # Happy Coding! service: aws-java-maven # NOTE: update this with your service name +#app: your-app-name +#tenant: your-tenant-name # You can pin your service to only deploy with a specific Serverless version # Check out our docs for more details @@ -52,7 +54,6 @@ package: functions: hello: handler: com.serverless.Handler - # The following are a few example events you can configure # NOTE: Please make sure to change your handler code to work with those events # Check the event documentation for details @@ -82,6 +83,12 @@ functions: # - cognitoUserPool: # pool: MyUserPool # trigger: PreSignUp +# - alb: +# listenerArn: arn:aws:elasticloadbalancing:us-east-1:XXXXXX:listener/app/my-load-balancer/50dc6c495c0c9188/ +# priority: 1 +# conditions: +# host: example.com +# path: /hello # Define function environment variables here # environment: diff --git a/lib/plugins/create/templates/aws-kotlin-jvm-gradle/serverless.yml b/lib/plugins/create/templates/aws-kotlin-jvm-gradle/serverless.yml index 9d8deccdf..f19577ea3 100644 --- a/lib/plugins/create/templates/aws-kotlin-jvm-gradle/serverless.yml +++ b/lib/plugins/create/templates/aws-kotlin-jvm-gradle/serverless.yml @@ -12,6 +12,8 @@ # Happy Coding! service: aws-kotlin-jvm-gradle # NOTE: update this with your service name +#app: your-app-name +#tenant: your-tenant-name # You can pin your service to only deploy with a specific Serverless version # Check out our docs for more details @@ -52,7 +54,6 @@ package: functions: hello: handler: com.serverless.Handler - # The following are a few example events you can configure # NOTE: Please make sure to change your handler code to work with those events # Check the event documentation for details @@ -82,6 +83,12 @@ functions: # - cognitoUserPool: # pool: MyUserPool # trigger: PreSignUp +# - alb: +# listenerArn: arn:aws:elasticloadbalancing:us-east-1:XXXXXX:listener/app/my-load-balancer/50dc6c495c0c9188/ +# priority: 1 +# conditions: +# host: example.com +# path: /hello # Define function environment variables here # environment: 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 e251df392..d1187e2ba 100644 --- a/lib/plugins/create/templates/aws-kotlin-jvm-maven/serverless.yml +++ b/lib/plugins/create/templates/aws-kotlin-jvm-maven/serverless.yml @@ -12,6 +12,8 @@ # Happy Coding! service: aws-kotlin-jvm-maven # NOTE: update this with your service name +#app: your-app-name +#tenant: your-tenant-name # You can pin your service to only deploy with a specific Serverless version # Check out our docs for more details @@ -52,7 +54,6 @@ package: functions: hello: handler: com.serverless.Handler - # The following are a few example events you can configure # NOTE: Please make sure to change your handler code to work with those events # Check the event documentation for details @@ -82,6 +83,12 @@ functions: # - cognitoUserPool: # pool: MyUserPool # trigger: PreSignUp +# - alb: +# listenerArn: arn:aws:elasticloadbalancing:us-east-1:XXXXXX:listener/app/my-load-balancer/50dc6c495c0c9188/ +# priority: 1 +# conditions: +# host: example.com +# path: /hello # Define function environment variables here # environment: 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 be7630fd7..ef5683baf 100644 --- a/lib/plugins/create/templates/aws-kotlin-nodejs-gradle/serverless.yml +++ b/lib/plugins/create/templates/aws-kotlin-nodejs-gradle/serverless.yml @@ -12,6 +12,8 @@ # Happy Coding! service: aws-kotlin-nodejs-gradle # NOTE: update this with your service name +#app: your-app-name +#tenant: your-tenant-name # You can pin your service to only deploy with a specific Serverless version # Check out our docs for more details @@ -19,7 +21,7 @@ service: aws-kotlin-nodejs-gradle # NOTE: update this with your service name provider: name: aws - runtime: nodejs6.10 + runtime: nodejs10.x # you can overwrite defaults here # stage: dev @@ -48,7 +50,6 @@ provider: functions: hello: handler: build/index.Handler - # The following are a few example events you can configure # NOTE: Please make sure to change your handler code to work with those events # Check the event documentation for details @@ -78,6 +79,12 @@ functions: # - cognitoUserPool: # pool: MyUserPool # trigger: PreSignUp +# - alb: +# listenerArn: arn:aws:elasticloadbalancing:us-east-1:XXXXXX:listener/app/my-load-balancer/50dc6c495c0c9188/ +# priority: 1 +# conditions: +# host: example.com +# path: /hello # Define function environment variables here # environment: diff --git a/lib/plugins/create/templates/aws-nodejs-ecma-script/first.js b/lib/plugins/create/templates/aws-nodejs-ecma-script/first.js index 8b8e34077..81886dd5c 100644 --- a/lib/plugins/create/templates/aws-nodejs-ecma-script/first.js +++ b/lib/plugins/create/templates/aws-nodejs-ecma-script/first.js @@ -1,12 +1,12 @@ // eslint-disable-next-line import/prefer-default-export export const hello = (event, context, callback) => { - const p = new Promise((resolve) => { + const p = new Promise(resolve => { resolve('success'); }); - p - .then(() => callback(null, { + p.then(() => + callback(null, { message: 'Go Serverless Webpack (Ecma Script) v1.0! First module!', event, - })) - .catch(e => callback(e)); + }) + ).catch(e => callback(e)); }; 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 bcfad7dd9..cd6b738a0 100644 --- a/lib/plugins/create/templates/aws-nodejs-ecma-script/package.json +++ b/lib/plugins/create/templates/aws-nodejs-ecma-script/package.json @@ -11,9 +11,9 @@ "babel-plugin-transform-runtime": "^6.23.0", "babel-polyfill": "^6.23.0", "babel-preset-env": "^1.6.0", - "serverless-webpack": "^3.1.1", - "webpack": "^3.3.0" + "serverless-webpack": "^5.3.1", + "webpack": "^4.35.2" }, "author": "The serverless webpack authors (https://github.com/elastic-coders/serverless-webpack)", "license": "MIT" -} \ No newline at end of file +} diff --git a/lib/plugins/create/templates/aws-nodejs-ecma-script/second.js b/lib/plugins/create/templates/aws-nodejs-ecma-script/second.js index a932eac05..9517398c3 100644 --- a/lib/plugins/create/templates/aws-nodejs-ecma-script/second.js +++ b/lib/plugins/create/templates/aws-nodejs-ecma-script/second.js @@ -1,16 +1,18 @@ // eslint-disable-next-line import/prefer-default-export export const hello = (event, context, cb) => { - const p = new Promise((resolve) => { + const p = new Promise(resolve => { resolve('success'); }); const response = { statusCode: 200, - body: JSON.stringify({ - message: 'Go Serverless Webpack (Ecma Script) v1.0! Second module!', - input: event, - }, null, 2), + body: JSON.stringify( + { + message: 'Go Serverless Webpack (Ecma Script) v1.0! Second module!', + input: event, + }, + null, + 2 + ), }; - p - .then(() => cb(null, response)) - .catch(e => cb(e)); + p.then(() => cb(null, response)).catch(e => cb(e)); }; 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 5f25d7ec4..e6fd291ab 100644 --- a/lib/plugins/create/templates/aws-nodejs-ecma-script/serverless.yml +++ b/lib/plugins/create/templates/aws-nodejs-ecma-script/serverless.yml @@ -1,5 +1,7 @@ service: name: aws-nodejs-ecma-script +#app: your-app-name +#tenant: your-tenant-name # Add the serverless-webpack plugin plugins: @@ -7,7 +9,7 @@ plugins: provider: name: aws - runtime: nodejs8.10 + runtime: nodejs10.x functions: first: diff --git a/lib/plugins/create/templates/aws-nodejs-ecma-script/webpack.config.js b/lib/plugins/create/templates/aws-nodejs-ecma-script/webpack.config.js index 4d173e306..62072d11d 100644 --- a/lib/plugins/create/templates/aws-nodejs-ecma-script/webpack.config.js +++ b/lib/plugins/create/templates/aws-nodejs-ecma-script/webpack.config.js @@ -4,18 +4,26 @@ const slsw = require('serverless-webpack'); module.exports = { entry: slsw.lib.entries, - target: 'node', - module: { - loaders: [{ - test: /\.js$/, - loaders: ['babel-loader'], - include: __dirname, - exclude: /node_modules/, - }], - }, output: { libraryTarget: 'commonjs', - path: path.join(__dirname, '.webpack'), filename: '[name].js', + path: path.join(__dirname, '.webpack'), + }, + mode: 'development', + target: 'node', + module: { + rules: [ + { + test: /\.js$/, // include .js files + enforce: 'pre', // preload the jshint loader + exclude: /node_modules/, // exclude any and all files in the node_modules folder + include: __dirname, + use: [ + { + loader: 'babel-loader', + }, + ], + }, + ], }, }; diff --git a/lib/plugins/create/templates/aws-nodejs-typescript/package.json b/lib/plugins/create/templates/aws-nodejs-typescript/package.json index a4ef1a31d..002a8fadf 100644 --- a/lib/plugins/create/templates/aws-nodejs-typescript/package.json +++ b/lib/plugins/create/templates/aws-nodejs-typescript/package.json @@ -17,7 +17,6 @@ "typescript": "^3.2.4", "webpack": "^4.29.0" }, - "author": - "The serverless webpack authors (https://github.com/elastic-coders/serverless-webpack)", + "author": "The serverless webpack authors (https://github.com/elastic-coders/serverless-webpack)", "license": "MIT" } diff --git a/lib/plugins/create/templates/aws-nodejs-typescript/serverless.yml b/lib/plugins/create/templates/aws-nodejs-typescript/serverless.yml index ef04a44b7..7ece1abf0 100644 --- a/lib/plugins/create/templates/aws-nodejs-typescript/serverless.yml +++ b/lib/plugins/create/templates/aws-nodejs-typescript/serverless.yml @@ -1,5 +1,7 @@ service: name: aws-nodejs-typescript +#app: your-app-name +#tenant: your-tenant-name # Add the serverless-webpack plugin plugins: @@ -7,7 +9,7 @@ plugins: provider: name: aws - runtime: nodejs8.10 + runtime: nodejs10.x functions: hello: diff --git a/lib/plugins/create/templates/aws-nodejs-typescript/tsconfig.json b/lib/plugins/create/templates/aws-nodejs-typescript/tsconfig.json index de011adf2..7aa5aad6a 100644 --- a/lib/plugins/create/templates/aws-nodejs-typescript/tsconfig.json +++ b/lib/plugins/create/templates/aws-nodejs-typescript/tsconfig.json @@ -1,8 +1,6 @@ { "compilerOptions": { - "lib": [ - "es2017" - ], + "lib": ["es2017"], "moduleResolution": "node", "noUnusedLocals": true, "noUnusedParameters": true, @@ -10,7 +8,5 @@ "target": "es2017", "outDir": "lib" }, - "exclude": [ - "node_modules" - ] + "exclude": ["node_modules"] } diff --git a/lib/plugins/create/templates/aws-nodejs/handler.js b/lib/plugins/create/templates/aws-nodejs/handler.js index 8d524e278..13dd03fcd 100644 --- a/lib/plugins/create/templates/aws-nodejs/handler.js +++ b/lib/plugins/create/templates/aws-nodejs/handler.js @@ -1,12 +1,16 @@ 'use strict'; -module.exports.hello = async (event) => { +module.exports.hello = async event => { return { statusCode: 200, - body: JSON.stringify({ - message: 'Go Serverless v1.0! Your function executed successfully!', - input: event, - }, null, 2), + body: JSON.stringify( + { + message: 'Go Serverless v1.0! Your function executed successfully!', + input: event, + }, + null, + 2 + ), }; // Use this code if you don't use the http event with the LAMBDA-PROXY integration diff --git a/lib/plugins/create/templates/aws-nodejs/serverless.yml b/lib/plugins/create/templates/aws-nodejs/serverless.yml index 2d59fb360..655fa0f54 100644 --- a/lib/plugins/create/templates/aws-nodejs/serverless.yml +++ b/lib/plugins/create/templates/aws-nodejs/serverless.yml @@ -12,6 +12,8 @@ # Happy Coding! service: aws-nodejs # NOTE: update this with your service name +#app: your-app-name +#tenant: your-tenant-name # You can pin your service to only deploy with a specific Serverless version # Check out our docs for more details @@ -19,7 +21,7 @@ service: aws-nodejs # NOTE: update this with your service name provider: name: aws - runtime: nodejs8.10 + runtime: nodejs10.x # you can overwrite defaults here # stage: dev @@ -57,7 +59,6 @@ provider: functions: hello: handler: handler.hello - # The following are a few example events you can configure # NOTE: Please make sure to change your handler code to work with those events # Check the event documentation for details @@ -87,6 +88,12 @@ functions: # - cognitoUserPool: # pool: MyUserPool # trigger: PreSignUp +# - alb: +# listenerArn: arn:aws:elasticloadbalancing:us-east-1:XXXXXX:listener/app/my-load-balancer/50dc6c495c0c9188/ +# priority: 1 +# conditions: +# host: example.com +# path: /hello # Define function environment variables here # environment: diff --git a/lib/plugins/create/templates/aws-provided/serverless.yml b/lib/plugins/create/templates/aws-provided/serverless.yml index 65559fa50..bcdbc3122 100644 --- a/lib/plugins/create/templates/aws-provided/serverless.yml +++ b/lib/plugins/create/templates/aws-provided/serverless.yml @@ -12,6 +12,8 @@ # Happy Coding! service: aws-provided # NOTE: update this with your service name +#app: your-app-name +#tenant: your-tenant-name # You can pin your service to only deploy with a specific Serverless version # Check out our docs for more details @@ -57,7 +59,6 @@ provider: functions: hello: handler: handler.hello - # The following are a few example events you can configure # NOTE: Please make sure to change your handler code to work with those events # Check the event documentation for details @@ -87,6 +88,12 @@ functions: # - cognitoUserPool: # pool: MyUserPool # trigger: PreSignUp +# - alb: +# listenerArn: arn:aws:elasticloadbalancing:us-east-1:XXXXXX:listener/app/my-load-balancer/50dc6c495c0c9188/ +# priority: 1 +# conditions: +# host: example.com +# path: /hello # Define function environment variables here # environment: diff --git a/lib/plugins/create/templates/aws-python/serverless.yml b/lib/plugins/create/templates/aws-python/serverless.yml index a6bd8a865..67cbc5448 100644 --- a/lib/plugins/create/templates/aws-python/serverless.yml +++ b/lib/plugins/create/templates/aws-python/serverless.yml @@ -12,6 +12,8 @@ # Happy Coding! service: aws-python # NOTE: update this with your service name +#app: your-app-name +#tenant: your-tenant-name # You can pin your service to only deploy with a specific Serverless version # Check out our docs for more details @@ -57,7 +59,6 @@ provider: functions: hello: handler: handler.hello - # The following are a few example events you can configure # NOTE: Please make sure to change your handler code to work with those events # Check the event documentation for details @@ -87,6 +88,12 @@ functions: # - cognitoUserPool: # pool: MyUserPool # trigger: PreSignUp +# - alb: +# listenerArn: arn:aws:elasticloadbalancing:us-east-1:XXXXXX:listener/app/my-load-balancer/50dc6c495c0c9188/ +# priority: 1 +# conditions: +# host: example.com +# path: /hello # Define function environment variables here # environment: diff --git a/lib/plugins/create/templates/aws-python3/serverless.yml b/lib/plugins/create/templates/aws-python3/serverless.yml index 5c2fba0fd..81974682d 100644 --- a/lib/plugins/create/templates/aws-python3/serverless.yml +++ b/lib/plugins/create/templates/aws-python3/serverless.yml @@ -12,6 +12,8 @@ # Happy Coding! service: aws-python3 # NOTE: update this with your service name +#app: your-app-name +#tenant: your-tenant-name # You can pin your service to only deploy with a specific Serverless version # Check out our docs for more details @@ -57,7 +59,6 @@ provider: functions: hello: handler: handler.hello - # The following are a few example events you can configure # NOTE: Please make sure to change your handler code to work with those events # Check the event documentation for details @@ -87,6 +88,12 @@ functions: # - cognitoUserPool: # pool: MyUserPool # trigger: PreSignUp +# - alb: +# listenerArn: arn:aws:elasticloadbalancing:us-east-1:XXXXXX:listener/app/my-load-balancer/50dc6c495c0c9188/ +# priority: 1 +# conditions: +# host: example.com +# path: /hello # Define function environment variables here # environment: diff --git a/lib/plugins/create/templates/aws-ruby/serverless.yml b/lib/plugins/create/templates/aws-ruby/serverless.yml index 0b42c6568..d5b816f26 100644 --- a/lib/plugins/create/templates/aws-ruby/serverless.yml +++ b/lib/plugins/create/templates/aws-ruby/serverless.yml @@ -12,6 +12,8 @@ # Happy Coding! service: aws-ruby # NOTE: update this with your service name +#app: your-app-name +#tenant: your-tenant-name # You can pin your service to only deploy with a specific Serverless version # Check out our docs for more details @@ -57,7 +59,6 @@ provider: functions: hello: handler: handler.hello - # The following are a few example events you can configure # NOTE: Please make sure to change your handler code to work with those events # Check the event documentation for details @@ -87,6 +88,12 @@ functions: # - cognitoUserPool: # pool: MyUserPool # trigger: PreSignUp +# - alb: +# listenerArn: arn:aws:elasticloadbalancing:us-east-1:XXXXXX:listener/app/my-load-balancer/50dc6c495c0c9188/ +# priority: 1 +# conditions: +# host: example.com +# path: /hello # Define function environment variables here # environment: diff --git a/lib/plugins/create/templates/aws-scala-sbt/build.sbt b/lib/plugins/create/templates/aws-scala-sbt/build.sbt index 399dd42bb..0412a0915 100644 --- a/lib/plugins/create/templates/aws-scala-sbt/build.sbt +++ b/lib/plugins/create/templates/aws-scala-sbt/build.sbt @@ -4,7 +4,7 @@ import sbtrelease.Version name := "hello" resolvers += Resolver.sonatypeRepo("public") -scalaVersion := "2.12.8" +scalaVersion := "2.13.0" releaseNextVersion := { ver => Version(ver).map(_.bumpMinor.string).getOrElse("Error") } diff --git a/lib/plugins/create/templates/aws-scala-sbt/serverless.yml b/lib/plugins/create/templates/aws-scala-sbt/serverless.yml index 366fd5302..2ebdc16ce 100644 --- a/lib/plugins/create/templates/aws-scala-sbt/serverless.yml +++ b/lib/plugins/create/templates/aws-scala-sbt/serverless.yml @@ -12,6 +12,8 @@ # Happy Coding! service: aws-scala-sbt # NOTE: update this with your service name +#app: your-app-name +#tenant: your-tenant-name # You can pin your service to only deploy with a specific Serverless version # Check out our docs for more details @@ -49,12 +51,11 @@ provider: # Make sure to run "sbt assembly" to create a jar file # with all your dependencies and put that jar file name here. package: - artifact: target/scala-2.12/hello.jar + artifact: target/scala-2.13/hello.jar functions: hello: handler: hello.Handler - # The following are a few example events you can configure # NOTE: Please make sure to change your handler code to work with those events # Check the event documentation for details @@ -84,6 +85,12 @@ functions: # - cognitoUserPool: # pool: MyUserPool # trigger: PreSignUp +# - alb: +# listenerArn: arn:aws:elasticloadbalancing:us-east-1:XXXXXX:listener/app/my-load-balancer/50dc6c495c0c9188/ +# priority: 1 +# conditions: +# host: example.com +# path: /hello # Define function environment variables here # environment: diff --git a/lib/plugins/create/templates/aws-scala-sbt/src/main/scala/hello/ApiGatewayResponse.scala b/lib/plugins/create/templates/aws-scala-sbt/src/main/scala/hello/ApiGatewayResponse.scala index 9a436e5d2..e0b121bbe 100644 --- a/lib/plugins/create/templates/aws-scala-sbt/src/main/scala/hello/ApiGatewayResponse.scala +++ b/lib/plugins/create/templates/aws-scala-sbt/src/main/scala/hello/ApiGatewayResponse.scala @@ -3,4 +3,4 @@ package hello import scala.beans.BeanProperty case class ApiGatewayResponse(@BeanProperty statusCode: Integer, @BeanProperty body: String, - @BeanProperty headers: java.util.Map[String, Object], @BeanProperty base64Encoded: Boolean = false) + @BeanProperty headers: java.util.Map[String, String], @BeanProperty base64Encoded: Boolean = false) diff --git a/lib/plugins/create/templates/aws-scala-sbt/src/main/scala/hello/Handler.scala b/lib/plugins/create/templates/aws-scala-sbt/src/main/scala/hello/Handler.scala index 7f0cae1d0..aeb4b3e52 100644 --- a/lib/plugins/create/templates/aws-scala-sbt/src/main/scala/hello/Handler.scala +++ b/lib/plugins/create/templates/aws-scala-sbt/src/main/scala/hello/Handler.scala @@ -3,7 +3,7 @@ package hello import com.amazonaws.services.lambda.runtime.{Context, RequestHandler} import org.apache.logging.log4j.{LogManager, Logger} -import scala.collection.JavaConverters +import scala.jdk.CollectionConverters._ class Handler extends RequestHandler[Request, Response] { @@ -20,7 +20,7 @@ class ApiGatewayHandler extends RequestHandler[Request, ApiGatewayResponse] { def handleRequest(input: Request, context: Context): ApiGatewayResponse = { val headers = Map("x-custom-response-header" -> "my custom response header value") ApiGatewayResponse(200, "Go Serverless v1.0! Your function executed successfully!", - JavaConverters.mapAsJavaMap[String, Object](headers), + headers.asJava, true) } } diff --git a/lib/plugins/create/templates/azure-nodejs/.funcignore b/lib/plugins/create/templates/azure-nodejs/.funcignore deleted file mode 100644 index 517922249..000000000 --- a/lib/plugins/create/templates/azure-nodejs/.funcignore +++ /dev/null @@ -1,7 +0,0 @@ -*.js.map -*.ts -.git* -.vscode -local.settings.json -test -tsconfig.json \ No newline at end of file diff --git a/lib/plugins/create/templates/azure-nodejs/.vscode/extensions.json b/lib/plugins/create/templates/azure-nodejs/.vscode/extensions.json deleted file mode 100644 index 26786f933..000000000 --- a/lib/plugins/create/templates/azure-nodejs/.vscode/extensions.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "recommendations": [ - "ms-azuretools.vscode-azurefunctions" - ] -} diff --git a/lib/plugins/create/templates/azure-nodejs/.vscode/launch.json b/lib/plugins/create/templates/azure-nodejs/.vscode/launch.json deleted file mode 100644 index 9306c8ade..000000000 --- a/lib/plugins/create/templates/azure-nodejs/.vscode/launch.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "name": "Attach to Node Functions", - "type": "node", - "request": "attach", - "port": 9229, - "preLaunchTask": "func: host start" - } - ] -} diff --git a/lib/plugins/create/templates/azure-nodejs/.vscode/settings.json b/lib/plugins/create/templates/azure-nodejs/.vscode/settings.json deleted file mode 100644 index c5fd41834..000000000 --- a/lib/plugins/create/templates/azure-nodejs/.vscode/settings.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "azureFunctions.deploySubpath": ".", - "azureFunctions.projectLanguage": "JavaScript", - "azureFunctions.projectRuntime": "~2", - "debug.internalConsoleOptions": "neverOpen", - "azureFunctions.preDeployTask": "npm prune" -} diff --git a/lib/plugins/create/templates/azure-nodejs/.vscode/tasks.json b/lib/plugins/create/templates/azure-nodejs/.vscode/tasks.json deleted file mode 100644 index 6a7a88b5c..000000000 --- a/lib/plugins/create/templates/azure-nodejs/.vscode/tasks.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "version": "2.0.0", - "tasks": [ - { - "type": "func", - "command": "host start", - "problemMatcher": "$func-watch", - "isBackground": true, - "dependsOn": "npm install" - }, - { - "type": "shell", - "label": "npm install", - "command": "npm install" - }, - { - "type": "shell", - "label": "npm prune", - "command": "npm prune --production", - "problemMatcher": [] - } - ] -} diff --git a/lib/plugins/create/templates/azure-nodejs/README.md b/lib/plugins/create/templates/azure-nodejs/README.md index 8f1ff8f98..e3f75575f 100644 --- a/lib/plugins/create/templates/azure-nodejs/README.md +++ b/lib/plugins/create/templates/azure-nodejs/README.md @@ -2,11 +2,11 @@ ## Pre-requisites -1. Node.js `v6.5.0` or later. *(v6.5.0 is the minimum runtime version supported by Azure Functions)* +1. Node.js `v6.5.0` or later. _(v6.5.0 is the minimum runtime version supported by Azure Functions)_ 2. Serverless CLI `v1.9.0` or later. You can run -`npm install -g serverless` to install it. + `npm install -g serverless` to install it. 3. Azure plugin that allows you to work with Azure Functions `npm install -g serverless-azure-functions` -4. An Azure account. If you don't already have one, you can sign up for a [free trial](https://azure.microsoft.com/en-us/free/) that includes $200 of free credit. +4. An Azure account. If you don't already have one, you can sign up for a [free trial](https://azure.microsoft.com/en-us/free/) that includes \$200 of free credit. 5. **Set-up your [Provider Credentials](./credentials.md)**. ## Create a new service @@ -47,46 +47,46 @@ Note: The file `{function name}/function.json` is included in the template for t 1. **Deploy the Service:** - Deploy your new service to Azure! The first time you do this, you will be asked - to authenticate with your Azure account, so the `serverless` CLI can manage - Functions on your behalf. Simply follow the provided instructions, and the - deployment will continue as soon as the authentication process is completed. +Deploy your new service to Azure! The first time you do this, you will be asked +to authenticate with your Azure account, so the `serverless` CLI can manage +Functions on your behalf. Simply follow the provided instructions, and the +deployment will continue as soon as the authentication process is completed. - ```bash - serverless deploy - ``` +```bash +serverless deploy +``` - > Note: Once you've authenticated, a new Azure "service principal" will be - created, and used for subsequent deployments. This prevents you from needing to - manually login again. See [below](#advanced-authentication) if you'd prefer to - use a custom service principal instead. +> Note: Once you've authenticated, a new Azure "service principal" will be +> created, and used for subsequent deployments. This prevents you from needing to +> manually login again. See [below](#advanced-authentication) if you'd prefer to +> use a custom service principal instead. 2. **Deploy the Function** - Use this to quickly upload and overwrite your function code,allowing you to - develop faster. If you're working on a single function, you can simply deploy - the specified function instead of the entire service. +Use this to quickly upload and overwrite your function code,allowing you to +develop faster. If you're working on a single function, you can simply deploy +the specified function instead of the entire service. - ```bash - serverless deploy function -f hello - ``` +```bash +serverless deploy function -f hello +``` 3. **Invoke the Function** - Invoke a function, in order to test that it works: +Invoke a function, in order to test that it works: - ```bash - serverless invoke -f hello - ``` +```bash +serverless invoke -f hello +``` 4. **Fetch the Function Logs** - Open up a separate tab in your console and stream all logs for a specific - Function using this command. +Open up a separate tab in your console and stream all logs for a specific +Function using this command. - ```bash - serverless logs -f hello -t - ``` +```bash +serverless logs -f hello -t +``` ## Cleanup @@ -109,6 +109,7 @@ yourself, you can indicate that this plugin should use its credentials instead, by setting the following environment variables: **Bash** + ```bash export azureSubId='' export azureServicePrincipalTenantId='' @@ -117,6 +118,7 @@ export azureServicePrincipalPassword='' ``` **Powershell** + ```powershell $env:azureSubId='' $env:azureServicePrincipalTenantId='' @@ -124,7 +126,6 @@ $env:azureServicePrincipalClientId='' $env:azureServicePrincipalPassword='' ``` - ## Issues / Feedback / Feature Requests? If you have any issues, comments or want to see new features, please file an issue in the project repository: diff --git a/lib/plugins/create/templates/azure-nodejs/goodbye.js b/lib/plugins/create/templates/azure-nodejs/goodbye.js new file mode 100644 index 000000000..cb7634b33 --- /dev/null +++ b/lib/plugins/create/templates/azure-nodejs/goodbye.js @@ -0,0 +1,17 @@ +'use strict'; + +module.exports.handler = async function(context, req) { + context.log('JavaScript HTTP trigger function processed a request.'); + + if (req.query.name || (req.body && req.body.name)) { + context.res = { + // status: 200, /* Defaults to 200 */ + body: 'Goodbye ' + (req.query.name || req.body.name), + }; + } else { + context.res = { + status: 400, + body: 'Please pass a name on the query string or in the request body', + }; + } +}; diff --git a/lib/plugins/create/templates/azure-nodejs/goodbye/function.json b/lib/plugins/create/templates/azure-nodejs/goodbye/function.json deleted file mode 100644 index 7eb1f8f2d..000000000 --- a/lib/plugins/create/templates/azure-nodejs/goodbye/function.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "bindings": [ - { - "authLevel": "anonymous", - "type": "httpTrigger", - "direction": "in", - "name": "req", - "methods": [ - "get", - "post" - ] - }, - { - "type": "http", - "direction": "out", - "name": "res" - } - ] -} diff --git a/lib/plugins/create/templates/azure-nodejs/goodbye/index.js b/lib/plugins/create/templates/azure-nodejs/goodbye/index.js deleted file mode 100644 index cd73874f4..000000000 --- a/lib/plugins/create/templates/azure-nodejs/goodbye/index.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict'; - -module.exports.handler = async function (context, req) { - context.log('JavaScript HTTP trigger function processed a request.'); - - if (req.query.name || (req.body && req.body.name)) { - context.res = { - // status: 200, /* Defaults to 200 */ - body: "Goodbye " + (req.query.name || req.body.name) - }; - } - else { - context.res = { - status: 400, - body: "Please pass a name on the query string or in the request body" - }; - } -}; \ No newline at end of file diff --git a/lib/plugins/create/templates/azure-nodejs/goodbye/sample.dat b/lib/plugins/create/templates/azure-nodejs/goodbye/sample.dat deleted file mode 100644 index 26aac46f1..000000000 --- a/lib/plugins/create/templates/azure-nodejs/goodbye/sample.dat +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name": "Azure" -} \ No newline at end of file diff --git a/lib/plugins/create/templates/azure-nodejs/hello.js b/lib/plugins/create/templates/azure-nodejs/hello.js new file mode 100644 index 000000000..2c3ea26e6 --- /dev/null +++ b/lib/plugins/create/templates/azure-nodejs/hello.js @@ -0,0 +1,17 @@ +'use strict'; + +module.exports.handler = async function(context, req) { + context.log('JavaScript HTTP trigger function processed a request.'); + + if (req.query.name || (req.body && req.body.name)) { + context.res = { + // status: 200, /* Defaults to 200 */ + body: 'Hello ' + (req.query.name || req.body.name), + }; + } else { + context.res = { + status: 400, + body: 'Please pass a name on the query string or in the request body', + }; + } +}; diff --git a/lib/plugins/create/templates/azure-nodejs/hello/function.json b/lib/plugins/create/templates/azure-nodejs/hello/function.json deleted file mode 100644 index 7eb1f8f2d..000000000 --- a/lib/plugins/create/templates/azure-nodejs/hello/function.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "bindings": [ - { - "authLevel": "anonymous", - "type": "httpTrigger", - "direction": "in", - "name": "req", - "methods": [ - "get", - "post" - ] - }, - { - "type": "http", - "direction": "out", - "name": "res" - } - ] -} diff --git a/lib/plugins/create/templates/azure-nodejs/hello/index.js b/lib/plugins/create/templates/azure-nodejs/hello/index.js deleted file mode 100644 index a63058384..000000000 --- a/lib/plugins/create/templates/azure-nodejs/hello/index.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict'; - -module.exports.handler = async function (context, req) { - context.log('JavaScript HTTP trigger function processed a request.'); - - if (req.query.name || (req.body && req.body.name)) { - context.res = { - // status: 200, /* Defaults to 200 */ - body: "Hello " + (req.query.name || req.body.name) - }; - } - else { - context.res = { - status: 400, - body: "Please pass a name on the query string or in the request body" - }; - } -}; \ No newline at end of file diff --git a/lib/plugins/create/templates/azure-nodejs/hello/sample.dat b/lib/plugins/create/templates/azure-nodejs/hello/sample.dat deleted file mode 100644 index 26aac46f1..000000000 --- a/lib/plugins/create/templates/azure-nodejs/hello/sample.dat +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name": "Azure" -} \ No newline at end of file diff --git a/lib/plugins/create/templates/azure-nodejs/host.json b/lib/plugins/create/templates/azure-nodejs/host.json index 4952c4375..d2059a46b 100644 --- a/lib/plugins/create/templates/azure-nodejs/host.json +++ b/lib/plugins/create/templates/azure-nodejs/host.json @@ -1,4 +1,3 @@ { - "version": "2.0" - } - \ No newline at end of file + "version": "2.0" +} diff --git a/lib/plugins/create/templates/azure-nodejs/local.settings.json b/lib/plugins/create/templates/azure-nodejs/local.settings.json deleted file mode 100644 index dbf98ec52..000000000 --- a/lib/plugins/create/templates/azure-nodejs/local.settings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "IsEncrypted": false, - "Values": { - "AzureWebJobsStorage": "", - "FUNCTIONS_WORKER_RUNTIME": "node" - } - } - \ No newline at end of file diff --git a/lib/plugins/create/templates/azure-nodejs/package.json b/lib/plugins/create/templates/azure-nodejs/package.json index 8db9f41a9..49b66beb9 100644 --- a/lib/plugins/create/templates/azure-nodejs/package.json +++ b/lib/plugins/create/templates/azure-nodejs/package.json @@ -14,4 +14,4 @@ "devDependencies": { "serverless-azure-functions": ">=0.7.0" } -} \ No newline at end of file +} diff --git a/lib/plugins/create/templates/azure-nodejs/proxies.json b/lib/plugins/create/templates/azure-nodejs/proxies.json deleted file mode 100644 index f02a6018b..000000000 --- a/lib/plugins/create/templates/azure-nodejs/proxies.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/proxies", - "proxies": {} - } - \ No newline at end of file diff --git a/lib/plugins/create/templates/azure-nodejs/serverless.yml b/lib/plugins/create/templates/azure-nodejs/serverless.yml index 74e94f9fd..c3e84ff1a 100644 --- a/lib/plugins/create/templates/azure-nodejs/serverless.yml +++ b/lib/plugins/create/templates/azure-nodejs/serverless.yml @@ -25,36 +25,37 @@ plugins: - serverless-azure-functions # you can add packaging information here -#package: -# include: -# - include-me.js -# - include-me-dir/** -# exclude: -# - exclude-me.js -# - exclude-me-dir/** +package: + # include: + # - include-me.js + # - include-me-dir/** + exclude: + # - exclude-me.js + # - exclude-me-dir/** + - local.settings.json + - .vscode/** functions: hello: - handler: hello/index.handler + handler: hello.handler events: - http: true x-azure-settings: - authLevel : anonymous + authLevel: anonymous - http: true x-azure-settings: direction: out name: res goodbye: - handler: goodbye/index.handler + handler: goodbye.handler events: - http: true x-azure-settings: - authLevel : anonymous + authLevel: anonymous - http: true x-azure-settings: direction: out name: res - # The following are a few examples of other events you can configure: # # events: diff --git a/lib/plugins/create/templates/cloudflare-workers-enterprise/bar.js b/lib/plugins/create/templates/cloudflare-workers-enterprise/bar.js index 14359baea..0d537fadf 100644 --- a/lib/plugins/create/templates/cloudflare-workers-enterprise/bar.js +++ b/lib/plugins/create/templates/cloudflare-workers-enterprise/bar.js @@ -1,7 +1,7 @@ addEventListener('fetch', event => { - event.respondWith(handleRequest(event.request)) - }) - - async function handleRequest(request) { - return new Response("Foo is not Bar") - } + event.respondWith(handleRequest(event.request)); +}); + +async function handleRequest(request) { + return new Response('Foo is not Bar'); +} diff --git a/lib/plugins/create/templates/cloudflare-workers-enterprise/helloWorld.js b/lib/plugins/create/templates/cloudflare-workers-enterprise/helloWorld.js index dbede8a62..8e780d2b0 100644 --- a/lib/plugins/create/templates/cloudflare-workers-enterprise/helloWorld.js +++ b/lib/plugins/create/templates/cloudflare-workers-enterprise/helloWorld.js @@ -1,7 +1,7 @@ addEventListener('fetch', event => { - event.respondWith(handleRequest(event.request)) - }) - - async function handleRequest(request) { - return new Response("Hello world") - } + event.respondWith(handleRequest(event.request)); +}); + +async function handleRequest(request) { + return new Response('Hello world'); +} diff --git a/lib/plugins/create/templates/cloudflare-workers-enterprise/serverless.yml b/lib/plugins/create/templates/cloudflare-workers-enterprise/serverless.yml index ff9776602..1b1d3b94d 100644 --- a/lib/plugins/create/templates/cloudflare-workers-enterprise/serverless.yml +++ b/lib/plugins/create/templates/cloudflare-workers-enterprise/serverless.yml @@ -1,5 +1,5 @@ service: - name: hello-world + name: hello-world provider: name: cloudflare diff --git a/lib/plugins/create/templates/cloudflare-workers-rust/helloWorld.js b/lib/plugins/create/templates/cloudflare-workers-rust/helloWorld.js index 2423e9165..7f7d962b5 100644 --- a/lib/plugins/create/templates/cloudflare-workers-rust/helloWorld.js +++ b/lib/plugins/create/templates/cloudflare-workers-rust/helloWorld.js @@ -1,6 +1,6 @@ addEventListener('fetch', event => { - event.respondWith(handleRequest(event.request)) -}) + event.respondWith(handleRequest(event.request)); +}); async function handleRequest(request) { try { diff --git a/lib/plugins/create/templates/cloudflare-workers-rust/serverless.yml b/lib/plugins/create/templates/cloudflare-workers-rust/serverless.yml index ea7091b8d..459553e35 100644 --- a/lib/plugins/create/templates/cloudflare-workers-rust/serverless.yml +++ b/lib/plugins/create/templates/cloudflare-workers-rust/serverless.yml @@ -1,5 +1,5 @@ service: - name: hello-world + name: hello-world provider: name: cloudflare @@ -27,4 +27,3 @@ functions: wasm: - variable: WASM file: rust-wasm/pkg/rust_wasm_bg.wasm - diff --git a/lib/plugins/create/templates/cloudflare-workers-rust/webpack.config.js b/lib/plugins/create/templates/cloudflare-workers-rust/webpack.config.js index 41aaf8755..cbf4211f1 100644 --- a/lib/plugins/create/templates/cloudflare-workers-rust/webpack.config.js +++ b/lib/plugins/create/templates/cloudflare-workers-rust/webpack.config.js @@ -1,29 +1,32 @@ -const path = require("path"); -const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin"); +const path = require('path'); +const WasmPackPlugin = require('@wasm-tool/wasm-pack-plugin'); const wasmDir = 'rust-wasm'; module.exports = { entry: { - helloWorld: [path.join(__dirname, `./${wasmDir}/pkg/rust_wasm.js`), path.join(__dirname, './helloWorld.js')] + helloWorld: [ + path.join(__dirname, `./${wasmDir}/pkg/rust_wasm.js`), + path.join(__dirname, './helloWorld.js'), + ], }, resolve: { - extensions: ['.js'] + extensions: ['.js'], }, output: { filename: '[name].js', - path: path.join(__dirname, 'dist') + path: path.join(__dirname, 'dist'), }, plugins: [ new WasmPackPlugin({ crateDirectory: path.resolve(__dirname, wasmDir), - extraArgs: "--no-typescript --target no-modules" - }) + extraArgs: '--no-typescript --target no-modules', + }), ], //devtool: 'inline-source-map', target: 'web', - mode: process.env.NODE_ENV || 'production' + mode: process.env.NODE_ENV || 'production', }; diff --git a/lib/plugins/create/templates/cloudflare-workers/helloWorld.js b/lib/plugins/create/templates/cloudflare-workers/helloWorld.js index dbede8a62..8e780d2b0 100644 --- a/lib/plugins/create/templates/cloudflare-workers/helloWorld.js +++ b/lib/plugins/create/templates/cloudflare-workers/helloWorld.js @@ -1,7 +1,7 @@ addEventListener('fetch', event => { - event.respondWith(handleRequest(event.request)) - }) - - async function handleRequest(request) { - return new Response("Hello world") - } + event.respondWith(handleRequest(event.request)); +}); + +async function handleRequest(request) { + return new Response('Hello world'); +} diff --git a/lib/plugins/create/templates/cloudflare-workers/serverless.yml b/lib/plugins/create/templates/cloudflare-workers/serverless.yml index efa295a7d..803b8ddc6 100644 --- a/lib/plugins/create/templates/cloudflare-workers/serverless.yml +++ b/lib/plugins/create/templates/cloudflare-workers/serverless.yml @@ -1,5 +1,5 @@ service: - name: hello-world + name: hello-world provider: name: cloudflare @@ -13,7 +13,7 @@ plugins: functions: hello: name: hello - script: helloWorld # there must be a file called helloWorld.js + script: helloWorld # there must be a file called helloWorld.js events: - http: url: example.com/hello/* diff --git a/lib/plugins/create/templates/fn-go/hello/test.json b/lib/plugins/create/templates/fn-go/hello/test.json index 391d9b42f..485898b57 100644 --- a/lib/plugins/create/templates/fn-go/hello/test.json +++ b/lib/plugins/create/templates/fn-go/hello/test.json @@ -1,26 +1,26 @@ { - "tests": [ - { - "input": { - "body": { - "name": "Johnny" - } - }, - "output": { - "body": { - "message": "Hello Johnny" - } - } - }, - { - "input": { - "body": "" - }, - "output": { - "body": { - "message": "Hello World" - } - } + "tests": [ + { + "input": { + "body": { + "name": "Johnny" } - ] + }, + "output": { + "body": { + "message": "Hello Johnny" + } + } + }, + { + "input": { + "body": "" + }, + "output": { + "body": { + "message": "Hello World" + } + } + } + ] } diff --git a/lib/plugins/create/templates/fn-go/serverless.yml b/lib/plugins/create/templates/fn-go/serverless.yml index b338b2a69..67e2e93d8 100644 --- a/lib/plugins/create/templates/fn-go/serverless.yml +++ b/lib/plugins/create/templates/fn-go/serverless.yml @@ -6,7 +6,7 @@ # The `service` block is the name of the service service: - name: hello-world + name: hello-world # config: # some: 'val' @@ -25,5 +25,5 @@ functions: format: json runtime: go events: - - http: - path: /hellogo + - http: + path: /hellogo diff --git a/lib/plugins/create/templates/fn-nodejs/hello/func.js b/lib/plugins/create/templates/fn-nodejs/hello/func.js index 3d7d26bd7..911c30efc 100644 --- a/lib/plugins/create/templates/fn-nodejs/hello/func.js +++ b/lib/plugins/create/templates/fn-nodejs/hello/func.js @@ -1,11 +1,11 @@ const fdk = require('@fnproject/fdk'); -fdk.handle((input) => { - let name = 'World'; - if (input.name) { - name = input.name; - } - const response = { message: `Hello ${name}` }; - console.error(`I show up in the logs name was: ${name}`); - return response; +fdk.handle(input => { + let name = 'World'; + if (input.name) { + name = input.name; + } + const response = { message: `Hello ${name}` }; + console.error(`I show up in the logs name was: ${name}`); + return response; }); diff --git a/lib/plugins/create/templates/fn-nodejs/hello/package.json b/lib/plugins/create/templates/fn-nodejs/hello/package.json index cfd79b538..377a1fe33 100644 --- a/lib/plugins/create/templates/fn-nodejs/hello/package.json +++ b/lib/plugins/create/templates/fn-nodejs/hello/package.json @@ -1,11 +1,11 @@ { - "name": "hellofn", - "version": "1.0.0", - "description": "example function", - "main": "func.js", - "author": "", - "license": "Apache-2.0", - "dependencies": { - "@fnproject/fdk": "0.x" - } + "name": "hellofn", + "version": "1.0.0", + "description": "example function", + "main": "func.js", + "author": "", + "license": "Apache-2.0", + "dependencies": { + "@fnproject/fdk": "0.x" + } } diff --git a/lib/plugins/create/templates/fn-nodejs/hello/test.json b/lib/plugins/create/templates/fn-nodejs/hello/test.json index 391d9b42f..485898b57 100644 --- a/lib/plugins/create/templates/fn-nodejs/hello/test.json +++ b/lib/plugins/create/templates/fn-nodejs/hello/test.json @@ -1,26 +1,26 @@ { - "tests": [ - { - "input": { - "body": { - "name": "Johnny" - } - }, - "output": { - "body": { - "message": "Hello Johnny" - } - } - }, - { - "input": { - "body": "" - }, - "output": { - "body": { - "message": "Hello World" - } - } + "tests": [ + { + "input": { + "body": { + "name": "Johnny" } - ] + }, + "output": { + "body": { + "message": "Hello Johnny" + } + } + }, + { + "input": { + "body": "" + }, + "output": { + "body": { + "message": "Hello World" + } + } + } + ] } diff --git a/lib/plugins/create/templates/fn-nodejs/serverless.yml b/lib/plugins/create/templates/fn-nodejs/serverless.yml index bd51faf04..de49bb131 100644 --- a/lib/plugins/create/templates/fn-nodejs/serverless.yml +++ b/lib/plugins/create/templates/fn-nodejs/serverless.yml @@ -6,7 +6,7 @@ # The `service` block is the name of the service service: - name: hello-world + name: hello-world # config: # some: 'val' @@ -25,9 +25,9 @@ functions: idletimeout: 45 format: json memory: 256 -# config: -# another: value + # config: + # another: value runtime: node events: - - http: - path: /hello + - http: + path: /hello diff --git a/lib/plugins/create/templates/google-go/serverless.yml b/lib/plugins/create/templates/google-go/serverless.yml index 5ba3875e1..2f8aae90c 100644 --- a/lib/plugins/create/templates/google-go/serverless.yml +++ b/lib/plugins/create/templates/google-go/serverless.yml @@ -25,18 +25,15 @@ functions: handler: http events: - http: path - # NOTE: the following uses an "event" event (pubSub event in this case). # Please create the corresponding resources in the Google Cloud # before deploying this service through Serverless - #second: # handler: event # events: # - event: # eventType: providers/cloud.pubsub/eventTypes/topic.publish # resource: projects/*/topics/my-topic - # you can define resources, templates etc. the same way you would in a # Google Cloud deployment configuration #resources: diff --git a/lib/plugins/create/templates/google-nodejs/serverless.yml b/lib/plugins/create/templates/google-nodejs/serverless.yml index 2f72ae814..d1693d5de 100644 --- a/lib/plugins/create/templates/google-nodejs/serverless.yml +++ b/lib/plugins/create/templates/google-nodejs/serverless.yml @@ -37,7 +37,6 @@ functions: # - event: # eventType: providers/cloud.pubsub/eventTypes/topic.publish # resource: projects/*/topics/my-topic - # you can define resources, templates etc. the same way you would in a # Google Cloud deployment configuration #resources: diff --git a/lib/plugins/create/templates/google-python/serverless.yml b/lib/plugins/create/templates/google-python/serverless.yml index 36ea8c5a3..7c99ff3dd 100644 --- a/lib/plugins/create/templates/google-python/serverless.yml +++ b/lib/plugins/create/templates/google-python/serverless.yml @@ -37,7 +37,6 @@ functions: # - event: # eventType: providers/cloud.pubsub/eventTypes/topic.publish # resource: projects/*/topics/my-topic - # you can define resources, templates etc. the same way you would in a # Google Cloud deployment configuration #resources: diff --git a/lib/plugins/create/templates/hello-world/serverless.yml b/lib/plugins/create/templates/hello-world/serverless.yml index 22f21292b..a2e0a0fd2 100644 --- a/lib/plugins/create/templates/hello-world/serverless.yml +++ b/lib/plugins/create/templates/hello-world/serverless.yml @@ -10,7 +10,7 @@ service: serverless-hello-world # The `provider` block defines where your service will be deployed provider: name: aws - runtime: nodejs6.10 + runtime: nodejs10.x # The `functions` block defines what code to deploy functions: diff --git a/lib/plugins/create/templates/openwhisk-java-maven/serverless.yml b/lib/plugins/create/templates/openwhisk-java-maven/serverless.yml index d3541cdf8..8b929cf78 100644 --- a/lib/plugins/create/templates/openwhisk-java-maven/serverless.yml +++ b/lib/plugins/create/templates/openwhisk-java-maven/serverless.yml @@ -23,7 +23,7 @@ provider: # you can add packaging information here package: - artifact: target/demo-function.jar + artifact: target/demo-function.jar functions: demo: @@ -32,8 +32,7 @@ functions: # extend the framework using plugins listed here: # https://github.com/serverless/plugins plugins: - - "serverless-openwhisk" - + - 'serverless-openwhisk' # you can define custom triggers and trigger feeds using the resources section. # #resources: diff --git a/lib/plugins/create/templates/openwhisk-nodejs/README.md b/lib/plugins/create/templates/openwhisk-nodejs/README.md index 6d3525c4d..7c804970f 100644 --- a/lib/plugins/create/templates/openwhisk-nodejs/README.md +++ b/lib/plugins/create/templates/openwhisk-nodejs/README.md @@ -8,17 +8,17 @@ This is a template Node.js service for the OpenWhisk platform. Before you can de Before you can deploy your service to OpenWhisk, you need to have an account registered with the platform. -- *Want to run the platform locally?* Please read the project's [*Quick Start*](https://github.com/openwhisk/openwhisk#quick-start) guide for deploying it locally. -- *Want to use a hosted provider?* Please sign up for an account with [IBM Bluemix](https://console.ng.bluemix.net/) and then follow the instructions for getting access to [OpenWhisk on Bluemix](https://console.ng.bluemix.net/openwhisk/). +- _Want to run the platform locally?_ Please read the project's [_Quick Start_](https://github.com/openwhisk/openwhisk#quick-start) guide for deploying it locally. +- _Want to use a hosted provider?_ Please sign up for an account with [IBM Bluemix](https://console.ng.bluemix.net/) and then follow the instructions for getting access to [OpenWhisk on Bluemix](https://console.ng.bluemix.net/openwhisk/). Account credentials for OpenWhisk can be provided through a configuration file or environment variables. This plugin requires the API endpoint, namespace and authentication credentials. -**Do you want to use a configuration file for storing these values?** Please [follow the instructions](https://console.ng.bluemix.net/openwhisk/cli) for setting up the OpenWhisk command-line utility. This tool stores account credentials in the `.wskprops` file in the user's home directory. The plugin automatically extracts credentials from this file at runtime. No further configuration is needed. +**Do you want to use a configuration file for storing these values?** Please [follow the instructions](https://console.ng.bluemix.net/openwhisk/cli) for setting up the OpenWhisk command-line utility. This tool stores account credentials in the `.wskprops` file in the user's home directory. The plugin automatically extracts credentials from this file at runtime. No further configuration is needed. **Do you want to use environment variables for credentials?** Use the following environment variables to be pass in account credentials. These values override anything extracted from the configuration file. -- *OW_APIHOST* - Platform endpoint, e.g. `openwhisk.ng.bluemix.net` -- *OW_AUTH* - Authentication key, e.g. `xxxxxx:yyyyy +- _OW_APIHOST_ - Platform endpoint, e.g. `openwhisk.ng.bluemix.net` +- _OW_AUTH_ - Authentication key, e.g. `xxxxxx:yyyyy ### Have you installed the provider plugin? @@ -38,8 +38,6 @@ Use the `serverless` command to deploy your service. The sample `handler.js` fil serverless deploy ``` - - ### Issues / Feedback / Feature Requests? If you have any issues, comments or want to see new features, please file an issue in the project repository: diff --git a/lib/plugins/create/templates/openwhisk-php/README.md b/lib/plugins/create/templates/openwhisk-php/README.md index 853d41590..acfb9171d 100644 --- a/lib/plugins/create/templates/openwhisk-php/README.md +++ b/lib/plugins/create/templates/openwhisk-php/README.md @@ -8,17 +8,17 @@ This is a template PHP service for the OpenWhisk platform. Before you can deploy Before you can deploy your service to OpenWhisk, you need to have an account registered with the platform. -- *Want to run the platform locally?* Please read the project's [*Quick Start*](https://github.com/openwhisk/openwhisk#quick-start) guide for deploying it locally. -- *Want to use a hosted provider?* Please sign up for an account with [IBM Bluemix](https://console.ng.bluemix.net/) and then follow the instructions for getting access to [OpenWhisk on Bluemix](https://console.ng.bluemix.net/openwhisk/). +- _Want to run the platform locally?_ Please read the project's [_Quick Start_](https://github.com/openwhisk/openwhisk#quick-start) guide for deploying it locally. +- _Want to use a hosted provider?_ Please sign up for an account with [IBM Bluemix](https://console.ng.bluemix.net/) and then follow the instructions for getting access to [OpenWhisk on Bluemix](https://console.ng.bluemix.net/openwhisk/). Account credentials for OpenWhisk can be provided through a configuration file or environment variables. This plugin requires the API endpoint, namespace and authentication credentials. -**Do you want to use a configuration file for storing these values?** Please [follow the instructions](https://console.ng.bluemix.net/openwhisk/cli) for setting up the OpenWhisk command-line utility. This tool stores account credentials in the `.wskprops` file in the user's home directory. The plugin automatically extracts credentials from this file at runtime. No further configuration is needed. +**Do you want to use a configuration file for storing these values?** Please [follow the instructions](https://console.ng.bluemix.net/openwhisk/cli) for setting up the OpenWhisk command-line utility. This tool stores account credentials in the `.wskprops` file in the user's home directory. The plugin automatically extracts credentials from this file at runtime. No further configuration is needed. **Do you want to use environment variables for credentials?** Use the following environment variables to be pass in account credentials. These values override anything extracted from the configuration file. -- *OW_APIHOST* - Platform endpoint, e.g. `openwhisk.ng.bluemix.net` -- *OW_AUTH* - Authentication key, e.g. `xxxxxx:yyyyy +- _OW_APIHOST_ - Platform endpoint, e.g. `openwhisk.ng.bluemix.net` +- _OW_AUTH_ - Authentication key, e.g. `xxxxxx:yyyyy ### Have you installed the provider plugin? @@ -38,8 +38,6 @@ Use the `serverless` command to deploy your service. The sample `handler.js` fil serverless deploy ``` - - ### Issues / Feedback / Feature Requests? If you have any issues, comments or want to see new features, please file an issue in the project repository: diff --git a/lib/plugins/create/templates/openwhisk-python/README.md b/lib/plugins/create/templates/openwhisk-python/README.md index d29e60d36..26748320b 100644 --- a/lib/plugins/create/templates/openwhisk-python/README.md +++ b/lib/plugins/create/templates/openwhisk-python/README.md @@ -8,17 +8,17 @@ This is a template Python service for the OpenWhisk platform. Before you can dep Before you can deploy your service to OpenWhisk, you need to have an account registered with the platform. -- *Want to run the platform locally?* Please read the project's [*Quick Start*](https://github.com/openwhisk/openwhisk#quick-start) guide for deploying it locally. -- *Want to use a hosted provider?* Please sign up for an account with [IBM Bluemix](https://console.ng.bluemix.net/) and then follow the instructions for getting access to [OpenWhisk on Bluemix](https://console.ng.bluemix.net/openwhisk/). +- _Want to run the platform locally?_ Please read the project's [_Quick Start_](https://github.com/openwhisk/openwhisk#quick-start) guide for deploying it locally. +- _Want to use a hosted provider?_ Please sign up for an account with [IBM Bluemix](https://console.ng.bluemix.net/) and then follow the instructions for getting access to [OpenWhisk on Bluemix](https://console.ng.bluemix.net/openwhisk/). Account credentials for OpenWhisk can be provided through a configuration file or environment variables. This plugin requires the API endpoint, namespace and authentication credentials. -**Do you want to use a configuration file for storing these values?** Please [follow the instructions](https://console.ng.bluemix.net/openwhisk/cli) for setting up the OpenWhisk command-line utility. This tool stores account credentials in the `.wskprops` file in the user's home directory. The plugin automatically extracts credentials from this file at runtime. No further configuration is needed. +**Do you want to use a configuration file for storing these values?** Please [follow the instructions](https://console.ng.bluemix.net/openwhisk/cli) for setting up the OpenWhisk command-line utility. This tool stores account credentials in the `.wskprops` file in the user's home directory. The plugin automatically extracts credentials from this file at runtime. No further configuration is needed. **Do you want to use environment variables for credentials?** Use the following environment variables to be pass in account credentials. These values override anything extracted from the configuration file. -- *OW_APIHOST* - Platform endpoint, e.g. `openwhisk.ng.bluemix.net` -- *OW_AUTH* - Authentication key, e.g. `xxxxxx:yyyyy` +- _OW_APIHOST_ - Platform endpoint, e.g. `openwhisk.ng.bluemix.net` +- _OW_AUTH_ - Authentication key, e.g. `xxxxxx:yyyyy` ### Have you installed the provider plugin? @@ -38,8 +38,6 @@ Use the `serverless` command to deploy your service. The sample `handler.js` fil serverless deploy ``` - - ### Issues / Feedback / Feature Requests? If you have any issues, comments or want to see new features, please file an issue in the project repository: diff --git a/lib/plugins/create/templates/openwhisk-ruby/README.md b/lib/plugins/create/templates/openwhisk-ruby/README.md index 19954a6b1..0d989c786 100644 --- a/lib/plugins/create/templates/openwhisk-ruby/README.md +++ b/lib/plugins/create/templates/openwhisk-ruby/README.md @@ -8,17 +8,17 @@ This is a template Ruby service for the OpenWhisk platform. Before you can deplo Before you can deploy your service to OpenWhisk, you need to have an account registered with the platform. -- *Want to run the platform locally?* Please read the project's [*Quick Start*](https://github.com/openwhisk/openwhisk#quick-start) guide for deploying it locally. -- *Want to use a hosted provider?* Please sign up for an account with [IBM Bluemix](https://console.ng.bluemix.net/) and then follow the instructions for getting access to [OpenWhisk on Bluemix](https://console.ng.bluemix.net/openwhisk/). +- _Want to run the platform locally?_ Please read the project's [_Quick Start_](https://github.com/openwhisk/openwhisk#quick-start) guide for deploying it locally. +- _Want to use a hosted provider?_ Please sign up for an account with [IBM Bluemix](https://console.ng.bluemix.net/) and then follow the instructions for getting access to [OpenWhisk on Bluemix](https://console.ng.bluemix.net/openwhisk/). Account credentials for OpenWhisk can be provided through a configuration file or environment variables. This plugin requires the API endpoint, namespace and authentication credentials. -**Do you want to use a configuration file for storing these values?** Please [follow the instructions](https://console.ng.bluemix.net/openwhisk/cli) for setting up the OpenWhisk command-line utility. This tool stores account credentials in the `.wskprops` file in the user's home directory. The plugin automatically extracts credentials from this file at runtime. No further configuration is needed. +**Do you want to use a configuration file for storing these values?** Please [follow the instructions](https://console.ng.bluemix.net/openwhisk/cli) for setting up the OpenWhisk command-line utility. This tool stores account credentials in the `.wskprops` file in the user's home directory. The plugin automatically extracts credentials from this file at runtime. No further configuration is needed. **Do you want to use environment variables for credentials?** Use the following environment variables to be pass in account credentials. These values override anything extracted from the configuration file. -- *OW_APIHOST* - Platform endpoint, e.g. `openwhisk.ng.bluemix.net` -- *OW_AUTH* - Authentication key, e.g. `xxxxxx:yyyyy +- _OW_APIHOST_ - Platform endpoint, e.g. `openwhisk.ng.bluemix.net` +- _OW_AUTH_ - Authentication key, e.g. `xxxxxx:yyyyy ### Have you installed the provider plugin? @@ -38,8 +38,6 @@ Use the `serverless` command to deploy your service. The sample `handler.js` fil serverless deploy ``` - - ### Issues / Feedback / Feature Requests? If you have any issues, comments or want to see new features, please file an issue in the project repository: diff --git a/lib/plugins/create/templates/openwhisk-swift/README.md b/lib/plugins/create/templates/openwhisk-swift/README.md index ab7f9328b..596eb2520 100644 --- a/lib/plugins/create/templates/openwhisk-swift/README.md +++ b/lib/plugins/create/templates/openwhisk-swift/README.md @@ -8,17 +8,17 @@ This is a template Swift service for the OpenWhisk platform. Before you can depl Before you can deploy your service to OpenWhisk, you need to have an account registered with the platform. -- *Want to run the platform locally?* Please read the project's [*Quick Start*](https://github.com/openwhisk/openwhisk#quick-start) guide for deploying it locally. -- *Want to use a hosted provider?* Please sign up for an account with [IBM Bluemix](https://console.ng.bluemix.net/) and then follow the instructions for getting access to [OpenWhisk on Bluemix](https://console.ng.bluemix.net/openwhisk/). +- _Want to run the platform locally?_ Please read the project's [_Quick Start_](https://github.com/openwhisk/openwhisk#quick-start) guide for deploying it locally. +- _Want to use a hosted provider?_ Please sign up for an account with [IBM Bluemix](https://console.ng.bluemix.net/) and then follow the instructions for getting access to [OpenWhisk on Bluemix](https://console.ng.bluemix.net/openwhisk/). Account credentials for OpenWhisk can be provided through a configuration file or environment variables. This plugin requires the API endpoint, namespace and authentication credentials. -**Do you want to use a configuration file for storing these values?** Please [follow the instructions](https://console.ng.bluemix.net/openwhisk/cli) for setting up the OpenWhisk command-line utility. This tool stores account credentials in the `.wskprops` file in the user's home directory. The plugin automatically extracts credentials from this file at runtime. No further configuration is needed. +**Do you want to use a configuration file for storing these values?** Please [follow the instructions](https://console.ng.bluemix.net/openwhisk/cli) for setting up the OpenWhisk command-line utility. This tool stores account credentials in the `.wskprops` file in the user's home directory. The plugin automatically extracts credentials from this file at runtime. No further configuration is needed. **Do you want to use environment variables for credentials?** Use the following environment variables to be pass in account credentials. These values override anything extracted from the configuration file. -- *OW_APIHOST* - Platform endpoint, e.g. `openwhisk.ng.bluemix.net` -- *OW_AUTH* - Authentication key, e.g. `xxxxxx:yyyyy +- _OW_APIHOST_ - Platform endpoint, e.g. `openwhisk.ng.bluemix.net` +- _OW_AUTH_ - Authentication key, e.g. `xxxxxx:yyyyy ### Have you installed the provider plugin? @@ -38,8 +38,6 @@ Use the `serverless` command to deploy your service. The sample `handler.js` fil serverless deploy ``` - - ### Issues / Feedback / Feature Requests? If you have any issues, comments or want to see new features, please file an issue in the project repository: diff --git a/lib/plugins/create/templates/plugin/index.js b/lib/plugins/create/templates/plugin/index.js index ddd6d4d6c..c4df77127 100644 --- a/lib/plugins/create/templates/plugin/index.js +++ b/lib/plugins/create/templates/plugin/index.js @@ -8,15 +8,12 @@ class ServerlessPlugin { this.commands = { welcome: { usage: 'Helps you start your first Serverless plugin', - lifecycleEvents: [ - 'hello', - 'world', - ], + lifecycleEvents: ['hello', 'world'], options: { message: { usage: - 'Specify the message you want to deploy ' - + '(e.g. "--message \'My Message\'" or "-m \'My Message\'")', + 'Specify the message you want to deploy ' + + '(e.g. "--message \'My Message\'" or "-m \'My Message\'")', required: true, shortcut: 'm', }, diff --git a/lib/plugins/create/templates/spotinst-nodejs/handler.js b/lib/plugins/create/templates/spotinst-nodejs/handler.js index 30832c0f6..a7e482e5c 100644 --- a/lib/plugins/create/templates/spotinst-nodejs/handler.js +++ b/lib/plugins/create/templates/spotinst-nodejs/handler.js @@ -12,12 +12,12 @@ * headers: {"Content-Type": "application/json"} * }) * -*/ + */ -module.exports.main = function main (event, context, callback) { - callback(null, { - statusCode: 200, - body: '{"hello":"from NodeJS8.3 function"}', - headers: {"Content-Type": "application/json"} - }); +module.exports.main = function main(event, context, callback) { + callback(null, { + statusCode: 200, + body: '{"hello":"from NodeJS8.3 function"}', + headers: { 'Content-Type': 'application/json' }, + }); }; diff --git a/lib/plugins/deploy/deploy.js b/lib/plugins/deploy/deploy.js index 2f215ecd0..ee85e5ae4 100644 --- a/lib/plugins/deploy/deploy.js +++ b/lib/plugins/deploy/deploy.js @@ -23,30 +23,30 @@ class Deploy { 'finalize', ], options: { - conceal: { + 'conceal': { usage: 'Hide secrets from the output (e.g. API Gateway key values)', }, - stage: { + 'stage': { usage: 'Stage of the service', shortcut: 's', }, - region: { + 'region': { usage: 'Region of the service', shortcut: 'r', }, - package: { + 'package': { usage: 'Path of the deployment package', shortcut: 'p', }, - verbose: { + 'verbose': { usage: 'Show all stack events during deployment', shortcut: 'v', }, - force: { + 'force': { usage: 'Forces a deployment to take place', }, - function: { - usage: 'Function name. Deploys a single function (see \'deploy function\')', + 'function': { + usage: "Function name. Deploys a single function (see 'deploy function')", shortcut: 'f', }, 'aws-s3-accelerate': { @@ -56,45 +56,38 @@ class Deploy { commands: { function: { usage: 'Deploy a single function from the service', - lifecycleEvents: [ - 'initialize', - 'packageFunction', - 'deploy', - ], + lifecycleEvents: ['initialize', 'packageFunction', 'deploy'], options: { - function: { + 'function': { usage: 'Name of the function', shortcut: 'f', required: true, }, - stage: { + 'stage': { usage: 'Stage of the function', shortcut: 's', }, - region: { + 'region': { usage: 'Region of the function', shortcut: 'r', }, - force: { + 'force': { usage: 'Forces a deployment to take place', }, 'update-config': { - usage: 'Updates function configuration, e.g. Timeout or Memory Size without deploying code', // eslint-disable-line max-len + usage: + 'Updates function configuration, e.g. Timeout or Memory Size without deploying code', // eslint-disable-line max-len shortcut: 'u', }, }, }, list: { usage: 'List deployed version of your Serverless Service', - lifecycleEvents: [ - 'log', - ], + lifecycleEvents: ['log'], commands: { functions: { usage: 'List all the deployed functions and their versions', - lifecycleEvents: [ - 'log', - ], + lifecycleEvents: ['log'], }, }, }, @@ -103,8 +96,8 @@ class Deploy { }; this.hooks = { - 'before:deploy:deploy': () => BbPromise.bind(this) - .then(() => { + 'before:deploy:deploy': () => + BbPromise.bind(this).then(() => { const provider = this.serverless.service.provider.name; if (!this.serverless.getProvider(provider)) { const errorMessage = `The specified provider "${provider}" does not exist.`; @@ -114,9 +107,9 @@ class Deploy { // If the user has given a function parameter, spawn a function deploy // and terminate execution right afterwards. We did not enter the // deploy lifecycle yet, so nothing has to be cleaned up. - return this.serverless.pluginManager.spawn( - 'deploy:function', { terminateLifecycleAfterExecution: true } - ); + return this.serverless.pluginManager.spawn('deploy:function', { + terminateLifecycleAfterExecution: true, + }); } if (!this.options.package && !this.serverless.service.package.path) { return this.serverless.pluginManager.spawn('package'); diff --git a/lib/plugins/deploy/deploy.test.js b/lib/plugins/deploy/deploy.test.js index 81d54b397..0ef4a0a55 100644 --- a/lib/plugins/deploy/deploy.test.js +++ b/lib/plugins/deploy/deploy.test.js @@ -29,7 +29,9 @@ describe('Deploy', () => { it('should have hooks', () => expect(deploy.hooks).to.be.not.empty); it('should work without options', () => { const noOptionDeploy = new Deploy(serverless); - expect(noOptionDeploy).to.have.property('options').to.be.eql({}); + expect(noOptionDeploy) + .to.have.property('options') + .to.be.eql({}); }); }); @@ -52,28 +54,31 @@ describe('Deploy', () => { deploy.options.package = false; deploy.serverless.service.package.path = 'some_path'; - return expect(deploy.hooks['before:deploy:deploy']()).to.be.fulfilled - .then(() => expect(spawnPackageStub).to.be.not.called); + return expect(deploy.hooks['before:deploy:deploy']()).to.be.fulfilled.then( + () => expect(spawnPackageStub).to.be.not.called + ); }); it('should resolve if the service package path is set', () => { deploy.options.package = 'some_path'; deploy.serverless.service.package.path = false; - return expect(deploy.hooks['before:deploy:deploy']()).to.be.fulfilled - .then(() => expect(spawnPackageStub).to.be.not.called); + return expect(deploy.hooks['before:deploy:deploy']()).to.be.fulfilled.then( + () => expect(spawnPackageStub).to.be.not.called + ); }); it('should use the default packaging mechanism if no packaging config is provided', () => { deploy.options.package = false; deploy.serverless.service.package.path = false; - return expect(deploy.hooks['before:deploy:deploy']()).to.be.fulfilled - .then(() => BbPromise.all([ + return expect(deploy.hooks['before:deploy:deploy']()).to.be.fulfilled.then(() => + BbPromise.all([ expect(spawnDeployFunctionStub).to.not.be.called, expect(spawnPackageStub).to.be.calledOnce, expect(spawnPackageStub).to.be.calledWithExactly('package'), - ])); + ]) + ); }); it('should execute deploy function if a function option is given', () => { @@ -81,16 +86,15 @@ describe('Deploy', () => { deploy.options.function = 'myfunc'; deploy.serverless.service.package.path = false; - return expect(deploy.hooks['before:deploy:deploy']()).to.be.fulfilled - .then(() => BbPromise.all([ - expect(spawnPackageStub).to.not.be.called, - expect(spawnDeployFunctionStub).to.be.calledOnce, - expect(spawnDeployFunctionStub).to.be - .calledWithExactly('deploy:function', { + return expect(deploy.hooks['before:deploy:deploy']()).to.be.fulfilled.then(() => + BbPromise.all([ + expect(spawnPackageStub).to.not.be.called, + expect(spawnDeployFunctionStub).to.be.calledOnce, + expect(spawnDeployFunctionStub).to.be.calledWithExactly('deploy:function', { terminateLifecycleAfterExecution: true, - } - ), - ])); + }), + ]) + ); }); it('should throw an error if provider does not exist', () => { diff --git a/lib/plugins/info/info.js b/lib/plugins/info/info.js index 6306d98db..ef5b17bbc 100644 --- a/lib/plugins/info/info.js +++ b/lib/plugins/info/info.js @@ -11,9 +11,7 @@ class Info { info: { usage: 'Display information about the service', configDependent: true, - lifecycleEvents: [ - 'info', - ], + lifecycleEvents: ['info'], options: { stage: { usage: 'Stage of the service', diff --git a/lib/plugins/info/info.test.js b/lib/plugins/info/info.test.js index be89c6234..b6d4de8ea 100644 --- a/lib/plugins/info/info.test.js +++ b/lib/plugins/info/info.test.js @@ -1,10 +1,14 @@ 'use strict'; -const expect = require('chai').expect; +const chai = require('chai'); const Info = require('./info'); const Serverless = require('../../Serverless'); const sinon = require('sinon'); +const expect = chai.expect; +chai.use(require('chai-as-promised')); +chai.use(require('sinon-chai')); + describe('Info', () => { let info; let serverless; @@ -30,8 +34,8 @@ describe('Info', () => { }); it('should run the validation', () => - expect(info.hooks['after:info:info']()) - .to.be.fulfilled.then(() => expect(trackStub).to.be.called) - ); + expect(info.hooks['after:info:info']()).to.be.fulfilled.then( + () => expect(trackStub).to.be.called + )); }); }); diff --git a/lib/plugins/install/install.js b/lib/plugins/install/install.js index 1d82642d9..b692ea30c 100644 --- a/lib/plugins/install/install.js +++ b/lib/plugins/install/install.js @@ -13,9 +13,7 @@ class Install { this.commands = { install: { usage: 'Install a Serverless service from GitHub or a plugin from the Serverless registry', - lifecycleEvents: [ - 'install', - ], + lifecycleEvents: ['install'], options: { url: { usage: 'URL of the Serverless service on GitHub', @@ -31,21 +29,25 @@ class Install { }; this.hooks = { - 'install:install': () => BbPromise.bind(this) - .then(this.install), + 'install:install': () => BbPromise.bind(this).then(this.install), }; } install() { - return download.downloadTemplateFromRepo(this.options.url, this.options.name) + return download + .downloadTemplateFromRepo(this.options.url, this.options.name) .then(serviceName => { const message = [ `Successfully installed "${serviceName}" `, - `${this.options.name && - this.options.name !== serviceName ? `as "${this.options.name}"` : ''}`, + `${ + this.options.name && this.options.name !== serviceName + ? `as "${this.options.name}"` + : '' + }`, ].join(''); userStats.track('service_installed', { - data: { // will be updated with core analtyics lib + data: { + // will be updated with core analtyics lib url: this.options.url, }, }); diff --git a/lib/plugins/install/install.test.js b/lib/plugins/install/install.test.js index 5f2b7a0f5..cdc00acdc 100644 --- a/lib/plugins/install/install.test.js +++ b/lib/plugins/install/install.test.js @@ -1,13 +1,17 @@ 'use strict'; -const expect = require('chai').expect; +const chai = require('chai'); const Serverless = require('../../Serverless'); const Install = require('./install.js'); const sinon = require('sinon'); -const testUtils = require('../../../tests/utils'); const download = require('../../utils/downloadTemplateFromRepo'); +const userStats = require('../../utils/userStats'); const fse = require('fs-extra'); const path = require('path'); +const { getTmpDirPath } = require('../../../tests/utils/fs'); + +chai.use(require('sinon-chai')); +const { expect } = require('chai'); describe('Install', () => { let install; @@ -18,7 +22,7 @@ describe('Install', () => { let servicePath; beforeEach(() => { - const tmpDir = testUtils.getTmpDirPath(); + const tmpDir = getTmpDirPath(); cwd = process.cwd(); fse.mkdirsSync(tmpDir); @@ -26,14 +30,18 @@ describe('Install', () => { servicePath = tmpDir; + sinon.stub(userStats, 'track').resolves(); + serverless = new Serverless(); install = new Install(serverless); - serverless.init(); - install.serverless.cli = new serverless.classes.CLI(); - logSpy = sinon.spy(install.serverless.cli, 'log'); + return serverless.init().then(() => { + install.serverless.cli = new serverless.classes.CLI(); + logSpy = sinon.spy(install.serverless.cli, 'log'); + }); }); afterEach(() => { + userStats.track.restore(); // change back to the old cwd process.chdir(cwd); }); @@ -42,8 +50,7 @@ describe('Install', () => { let installStub; beforeEach(() => { - installStub = sinon - .stub(install, 'install').resolves(); + installStub = sinon.stub(install, 'install').resolves(); }); afterEach(() => { @@ -54,8 +61,8 @@ describe('Install', () => { it('should have hooks', () => expect(install.hooks).to.be.not.empty); - it('should run promise chain in order for "install:install" hook', () => install - .hooks['install:install']().then(() => { + it('should run promise chain in order for "install:install" hook', () => + install.hooks['install:install']().then(() => { expect(installStub.calledOnce).to.be.equal(true); })); }); @@ -97,8 +104,9 @@ describe('Install', () => { downloadStub.resolves('remote-service'); return install.install().then(() => { - const installationMessage = - logSpy.args.filter(arg => arg[0].includes('installed "remote-service"')); + const installationMessage = logSpy.args.filter(arg => + arg[0].includes('installed "remote-service"') + ); expect(downloadStub).to.have.been.calledOnce; // eslint-disable-line expect(installationMessage[0]).to.have.lengthOf(1); @@ -111,8 +119,9 @@ describe('Install', () => { downloadStub.resolves('remote-service'); return install.install().then(() => { - const installationMessage = - logSpy.args.filter(arg => arg[0].includes('installed "remote-service" as "remote"')); + const installationMessage = logSpy.args.filter(arg => + arg[0].includes('installed "remote-service" as "remote"') + ); expect(downloadStub).to.have.been.calledOnce; // eslint-disable-line expect(installationMessage[0]).to.have.lengthOf(1); diff --git a/lib/plugins/interactiveCli/index.js b/lib/plugins/interactiveCli/index.js new file mode 100644 index 000000000..8e807ede7 --- /dev/null +++ b/lib/plugins/interactiveCli/index.js @@ -0,0 +1,41 @@ +'use strict'; + +const _ = require('lodash'); +const inquirer = require('./inquirer'); +const initializeService = require('./initializeService'); +const setupAws = require('./setupAws'); + +module.exports = class InteractiveCli { + constructor(serverless) { + if (!process.stdin.isTTY) return; + + // Expose customized inquirer for other plugins + serverless.interactiveCli = { inquirer }; + const { processedInput } = serverless; + if (processedInput.commands.length) return; + if (!_.isEmpty(processedInput.options)) return; + + // Enforce interactive CLI + processedInput.commands.push('interactiveCli'); + this.commands = { + interactiveCli: { + lifecycleEvents: ['initializeService', 'setupAws'], + }, + }; + + this.hooks = { + 'interactiveCli:initializeService': () => { + if (!initializeService.check(serverless)) return null; + process.stdout.write('\n'); + return initializeService.run(serverless); + }, + 'interactiveCli:setupAws': () => { + return setupAws.check(serverless).then(isApplicable => { + if (!isApplicable) return null; + process.stdout.write('\n'); + return setupAws.run(serverless); + }); + }, + }; + } +}; diff --git a/lib/plugins/interactiveCli/index.test.js b/lib/plugins/interactiveCli/index.test.js new file mode 100644 index 000000000..52905c395 --- /dev/null +++ b/lib/plugins/interactiveCli/index.test.js @@ -0,0 +1,28 @@ +'use strict'; + +const chai = require('chai'); +const InteractiveCli = require('./'); +const Serverless = require('../../Serverless'); + +const expect = chai.expect; +chai.use(require('chai-as-promised')); +chai.use(require('sinon-chai')); + +describe('interactiveCli', () => { + let interactiveCli; + let serverless; + + beforeEach(() => { + serverless = new Serverless(); + serverless.processedInput = { commands: [], options: {} }; + const backupIsTTY = process.stdin.isTTY; + process.stdin.isTTY = true; + try { + interactiveCli = new InteractiveCli(serverless); + } finally { + process.stdin.isTTY = backupIsTTY; + } + }); + + it('should have commands', () => expect(interactiveCli.commands).to.be.not.empty); +}); diff --git a/lib/plugins/interactiveCli/initializeService.js b/lib/plugins/interactiveCli/initializeService.js new file mode 100644 index 000000000..dd74c87d1 --- /dev/null +++ b/lib/plugins/interactiveCli/initializeService.js @@ -0,0 +1,95 @@ +'use strict'; + +const { join } = require('path'); +const chalk = require('chalk'); +const inquirer = require('./inquirer'); +const createFromTemplate = require('../../utils/createFromTemplate'); +const { + getConfigFilePath, + getServerlessConfigFile, +} = require('../../utils/getServerlessConfigFile'); +const { confirm } = require('./utils'); + +const isValidServiceName = RegExp.prototype.test.bind(/^[a-zA-Z][a-zA-Z0-9-]{0,100}$/); + +const projectTypeChoice = () => + inquirer + .prompt({ + message: 'What do you want to make?', + type: 'list', + name: 'projectType', + choices: [ + { name: 'AWS Node.js', value: 'aws-nodejs' }, + { name: 'AWS Python', value: 'aws-python' }, + { name: 'Other', value: 'other' }, + ], + }) + .then(({ projectType }) => projectType); + +const projectNameInput = workingDir => + inquirer + .prompt({ + message: 'What do you want to call this project?', + type: 'input', + name: 'projectName', + validate: input => { + input = input.trim(); + if (!isValidServiceName(input)) { + return ( + 'Project name is not valid.\n' + + ' - It should only contain alphanumeric and hyphens.\n' + + ' - It should start with an alphabetic character.\n' + + " - Shouldn't exceed 128 characters" + ); + } + return getConfigFilePath(join(workingDir, input)).then(configFilePath => { + return configFilePath ? `Serverless project already found at ${input} directory` : true; + }); + }, + }) + .then(({ projectName }) => projectName.trim()); + +module.exports = { + check(serverless) { + return !serverless.config.servicePath; + }, + run(serverless) { + const workingDir = process.cwd(); + return confirm('No project detected. Do you want to create a new one?').then(isConfirmed => { + if (!isConfirmed) return null; + return projectTypeChoice().then(projectType => { + if (projectType === 'other') { + process.stdout.write( + '\nRun “serverless create --help” to view available templates and create a new project ' + + 'from one of those templates.\n' + ); + return null; + } + return projectNameInput(workingDir).then(projectName => { + const projectDir = join(workingDir, projectName); + return createFromTemplate(projectType, projectDir) + .then(() => { + process.stdout.write( + `\n${chalk.green(`Project successfully created in '${projectName}' folder.`)}\n` + ); + + process.chdir(projectDir); + serverless.config.servicePath = projectDir; + getServerlessConfigFile.cache.delete(serverless); + getServerlessConfigFile(serverless); + }) + .then(serverlessConfigFile => { + serverless.pluginManager.serverlessConfigFile = serverlessConfigFile; + return serverless.service.load(); + }) + .then(() => serverless.variables.populateService()) + .then(() => { + serverless.service.mergeArrays(); + serverless.service.setFunctionNames(); + serverless.service.validate(); + }); + }); + }); + }); + }, +}; diff --git a/lib/plugins/interactiveCli/inquirer.js b/lib/plugins/interactiveCli/inquirer.js new file mode 100644 index 000000000..5508a4a89 --- /dev/null +++ b/lib/plugins/interactiveCli/inquirer.js @@ -0,0 +1,36 @@ +// Customize inquirer style + +'use strict'; + +const { dirname } = require('path'); +const requireUncached = require('ncjsm/require-uncached'); +const resolve = require('ncjsm/resolve/sync'); +const chalk = require('chalk'); + +const inquirersChalkPath = resolve(dirname(require.resolve('inquirer')), 'chalk'); + +module.exports = requireUncached(inquirersChalkPath, () => { + // Ensure distinct chalk instance for inquirer and hack it with altered styles + Object.defineProperties(require(inquirersChalkPath), { + cyan: { + get() { + return chalk.bold; + }, + }, + bold: { + get() { + return chalk.bold.yellow; + }, + }, + }); + + // 'Serverless:' prefix + const BasePrompt = require('inquirer/lib/prompts/base'); + const originalGetQuestion = BasePrompt.prototype.getQuestion; + BasePrompt.prototype.getQuestion = function() { + this.opt.prefix = 'Serverless:'; + return originalGetQuestion.call(this); + }; + + return require('inquirer'); +}); diff --git a/lib/plugins/interactiveCli/setupAws.js b/lib/plugins/interactiveCli/setupAws.js new file mode 100644 index 000000000..729d4a67c --- /dev/null +++ b/lib/plugins/interactiveCli/setupAws.js @@ -0,0 +1,101 @@ +'use strict'; + +const BbPromise = require('bluebird'); +const chalk = require('chalk'); +const inquirer = require('./inquirer'); +const awsCredentials = require('../aws/utils/credentials'); +const { confirm } = require('./utils'); + +const isValidAwsProfileName = RegExp.prototype.test.bind(/^[a-zA-Z][a-zA-Z0-9-]{0,100}$/); +const isValidAwsAccessKeyId = RegExp.prototype.test.bind(/^[A-Z0-9]{10,}$/); +const isValidAwsSecretAccessKey = RegExp.prototype.test.bind(/^[a-zA-Z0-9/]{10,}$/); + +const awsProfileNameInput = () => + inquirer + .prompt({ + message: 'AWS Profile name:', + type: 'input', + name: 'profileName', + validate: input => { + if (isValidAwsProfileName(input.trim())) return true; + return ( + 'AWS profile name is not valid.\n' + + ' - It should only contain alphanumeric and hyphens.\n' + + ' - It should start with an alphabetic character.\n' + + " - Shouldn't exceed 128 characters" + ); + }, + }) + .then(({ profileName }) => profileName.trim()); + +const awsAccessKeyIdInput = () => + inquirer + .prompt({ + message: 'AWS Access Key Id:', + type: 'input', + name: 'accessKeyId', + validate: input => { + if (isValidAwsAccessKeyId(input.trim())) return true; + return 'AWS Access Key Id seems not valid.\n Expected something like AKIAIOSFODNN7EXAMPLE'; + }, + }) + .then(({ accessKeyId }) => accessKeyId.trim()); + +const awsSecretAccessKeyInput = () => + inquirer + .prompt({ + message: 'AWS Secret Access Key:', + type: 'input', + name: 'secretAccessKey', + validate: input => { + if (isValidAwsSecretAccessKey(input.trim())) return true; + return ( + 'AWS Secret Access Key seems not valid.\n' + + ' Expected something like wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY' + ); + }, + }) + .then(({ secretAccessKey }) => secretAccessKey.trim()); + +module.exports = { + check(serverless) { + return BbPromise.try(() => { + if (serverless.service.provider.name !== 'aws') return null; + return BbPromise.all([ + awsCredentials.resolveFileProfiles(), + awsCredentials.resolveEnvCredentials(), + ]).then(([fileProfiles, envCredentials]) => !fileProfiles.size && !envCredentials); + }); + }, + run() { + process.stdout.write( + 'No AWS credentials were found on your computer, ' + + 'you need these to host your application.\n\n' + ); + return confirm('Do you want to set them up now?').then(isConfirmed => { + if (!isConfirmed) return null; + process.stdout.write( + `\nGo here to learn how to create your AWS credentials:\n${chalk.bold( + 'https://github.com/serverless/enterprise/blob/master/docs/setup-aws-account.md' + )}\nThen enter them here:\n\n` + ); + return awsProfileNameInput().then(profileName => + awsAccessKeyIdInput().then(accessKeyId => + awsSecretAccessKeyInput().then(secretAccessKey => + awsCredentials + .saveFileProfiles(new Map([[profileName, { accessKeyId, secretAccessKey }]])) + .then(() => + process.stdout.write( + `\n${chalk.green( + `AWS credentials saved on your machine at ${chalk.bold( + '~/.aws/credentials' + )}. Go there to change them at any time.` + )}\n` + ) + ) + ) + ) + ); + }); + }, +}; diff --git a/lib/plugins/interactiveCli/utils.js b/lib/plugins/interactiveCli/utils.js new file mode 100644 index 000000000..0f2f53f55 --- /dev/null +++ b/lib/plugins/interactiveCli/utils.js @@ -0,0 +1,14 @@ +'use strict'; + +const inquirer = require('./inquirer'); + +module.exports = { + confirm: message => + inquirer + .prompt({ + message, + type: 'confirm', + name: 'isConfirmed', + }) + .then(({ isConfirmed }) => isConfirmed), +}; diff --git a/lib/plugins/invoke/invoke.js b/lib/plugins/invoke/invoke.js index f9c5405b6..7121feb06 100644 --- a/lib/plugins/invoke/invoke.js +++ b/lib/plugins/invoke/invoke.js @@ -13,9 +13,7 @@ class Invoke { invoke: { usage: 'Invoke a deployed function', configDependent: true, - lifecycleEvents: [ - 'invoke', - ], + lifecycleEvents: ['invoke'], options: { function: { usage: 'The function name', @@ -49,50 +47,45 @@ class Invoke { raw: { usage: 'Flag to pass input data as a raw string', }, - }, commands: { local: { usage: 'Invoke function locally', - lifecycleEvents: [ - 'loadEnvVars', - 'invoke', - ], + lifecycleEvents: ['loadEnvVars', 'invoke'], options: { - function: { + 'function': { usage: 'Name of the function', shortcut: 'f', required: true, }, - path: { + 'path': { usage: 'Path to JSON or YAML file holding input data', shortcut: 'p', }, - data: { + 'data': { usage: 'input data', shortcut: 'd', }, - raw: { + 'raw': { usage: 'Flag to pass input data as a raw string', }, - context: { + 'context': { usage: 'Context of the service', shortcut: 'c', }, - contextPath: { + 'contextPath': { usage: 'Path to JSON or YAML file holding context data', shortcut: 'x', }, - env: { + 'env': { usage: 'Override environment variables. e.g. --env VAR1=val1 --env VAR2=val2', shortcut: 'e', }, - docker: { usage: 'Flag to turn on docker use for node/python/ruby/java' }, + 'docker': { usage: 'Flag to turn on docker use for node/python/ruby/java' }, 'docker-arg': { usage: 'Arguments to docker run command. e.g. --docker-arg "-p 9229:9229"', }, }, - }, }, }, @@ -127,11 +120,10 @@ class Invoke { // Turn zero or more --env options into an array // ...then split --env NAME=value and put into process.env. - _.concat(this.options.env || []) - .forEach(itm => { - const splitItm = _.split(itm, '='); - process.env[splitItm[0]] = splitItm[1] || ''; - }); + _.concat(this.options.env || []).forEach(itm => { + const splitItm = _.split(itm, '='); + process.env[splitItm[0]] = splitItm[1] || ''; + }); return BbPromise.resolve(); } diff --git a/lib/plugins/invoke/invoke.test.js b/lib/plugins/invoke/invoke.test.js index c4830bee6..71ac1b4f2 100644 --- a/lib/plugins/invoke/invoke.test.js +++ b/lib/plugins/invoke/invoke.test.js @@ -1,6 +1,7 @@ 'use strict'; const chai = require('chai'); +const overrideEnv = require('process-utils/override-env'); const Invoke = require('./invoke'); const Serverless = require('../../Serverless'); @@ -11,23 +12,26 @@ const expect = chai.expect; describe('Invoke', () => { let invoke; let serverless; + let restoreEnv; beforeEach(() => { + ({ restoreEnv } = overrideEnv()); serverless = new Serverless(); invoke = new Invoke(serverless); }); + afterEach(() => restoreEnv()); + describe('#constructor()', () => { it('should have commands', () => expect(invoke.commands).to.be.not.empty); it('should have hooks', () => expect(invoke.hooks).to.be.not.empty); }); describe('#loadEnvVarsForLocal()', () => { - it('should set IS_LOCAL', () => { - delete process.env.IS_LOCAL; - return expect(invoke.loadEnvVarsForLocal()).to.be.fulfilled - .then(() => expect(process.env.IS_LOCAL).to.equal('true')); - }); + it('should set IS_LOCAL', () => + expect(invoke.loadEnvVarsForLocal()).to.be.fulfilled.then(() => + expect(process.env.IS_LOCAL).to.equal('true') + )); }); describe('hooks', () => { @@ -36,24 +40,24 @@ describe('Invoke', () => { expect(invoke.commands.invoke.commands.local.lifecycleEvents).to.contain('loadEnvVars'); }); - it('should set IS_LOCAL', () => { - delete process.env.IS_LOCAL; - return expect(invoke.hooks['invoke:local:loadEnvVars']()).to.be.fulfilled - .then(() => expect(process.env.IS_LOCAL).to.equal('true')); - }); + it('should set IS_LOCAL', () => + expect(invoke.hooks['invoke:local:loadEnvVars']()).to.be.fulfilled.then(() => + expect(process.env.IS_LOCAL).to.equal('true') + )); it('should accept a single env option', () => { invoke.options = { env: 'NAME=value' }; - expect(invoke.hooks['invoke:local:loadEnvVars']()).to.be.fulfilled - .then(() => expect(process.env.NAME).to.equal('value')); + return expect(invoke.hooks['invoke:local:loadEnvVars']()).to.be.fulfilled.then(() => + expect(process.env.NAME).to.equal('value') + ); }); it('should accept multiple env options', () => { invoke.options = { env: ['NAME1=val1', 'NAME2=val2'] }; - expect(invoke.hooks['invoke:local:loadEnvVars']()).to.be.fulfilled - .then(() => expect(process.env.NAME1).to.equal('val1')) - .then(() => expect(process.env.NAME2).to.equal('val2')); + return expect(invoke.hooks['invoke:local:loadEnvVars']()) + .to.be.fulfilled.then(() => expect(process.env.NAME1).to.equal('val1')) + .then(() => expect(process.env.NAME2).to.equal('val2')); }); }); }); diff --git a/lib/plugins/logs/logs.js b/lib/plugins/logs/logs.js index a237c22eb..7c12aeb6d 100644 --- a/lib/plugins/logs/logs.js +++ b/lib/plugins/logs/logs.js @@ -12,9 +12,7 @@ class Logs { logs: { usage: 'Output the logs of a deployed function', configDependent: true, - lifecycleEvents: [ - 'logs', - ], + lifecycleEvents: ['logs'], options: { function: { usage: 'The function name', @@ -56,7 +54,7 @@ class Logs { const sls = this.serverless; if (sls && sls.processedInput && sls.processedInput.options) { const opts = sls.processedInput.options; - const type = (opts.tail) ? 'service_logsTailed' : 'service_logsViewed'; + const type = opts.tail ? 'service_logsTailed' : 'service_logsViewed'; this.userStats.track(type); } return BbPromise.resolve(); diff --git a/lib/plugins/logs/logs.test.js b/lib/plugins/logs/logs.test.js index 2348ab92e..14ebf4e06 100644 --- a/lib/plugins/logs/logs.test.js +++ b/lib/plugins/logs/logs.test.js @@ -30,8 +30,7 @@ describe('Logs', () => { const newLogs = new Logs(serverless); newLogs.userStats = userStats; - return newLogs.track() - .then(() => { + return newLogs.track().then(() => { expect(userStats.track.called).to.be.equal(false); }); }); @@ -44,10 +43,8 @@ describe('Logs', () => { const newLogs = new Logs(serverless); newLogs.userStats = userStats; - return newLogs.track() - .then(() => { - expect(userStats.track.calledWithExactly('service_logsViewed')) - .to.be.equal(true); + return newLogs.track().then(() => { + expect(userStats.track.calledWithExactly('service_logsViewed')).to.be.equal(true); }); }); @@ -57,10 +54,8 @@ describe('Logs', () => { const newLogs = new Logs(serverless); newLogs.userStats = userStats; - return newLogs.track() - .then(() => { - expect(userStats.track.calledWithExactly('service_logsTailed')) - .to.be.equal(true); + return newLogs.track().then(() => { + expect(userStats.track.calledWithExactly('service_logsTailed')).to.be.equal(true); }); }); }); diff --git a/lib/plugins/metrics/metrics.js b/lib/plugins/metrics/metrics.js index 747222069..ae2cd864a 100644 --- a/lib/plugins/metrics/metrics.js +++ b/lib/plugins/metrics/metrics.js @@ -12,9 +12,7 @@ class Metrics { metrics: { usage: 'Show metrics for a specific function', configDependent: true, - lifecycleEvents: [ - 'metrics', - ], + lifecycleEvents: ['metrics'], options: { function: { usage: 'The function name', diff --git a/lib/plugins/metrics/metrics.test.js b/lib/plugins/metrics/metrics.test.js index f96cd08d1..d8f6a5c1f 100644 --- a/lib/plugins/metrics/metrics.test.js +++ b/lib/plugins/metrics/metrics.test.js @@ -21,9 +21,7 @@ describe('Metrics', () => { }); it('should have a lifecycle event "metrics"', () => { - expect(metrics.commands.metrics.lifecycleEvents).to.deep.equal([ - 'metrics', - ]); + expect(metrics.commands.metrics.lifecycleEvents).to.deep.equal(['metrics']); }); }); }); diff --git a/lib/plugins/package/lib/packageService.js b/lib/plugins/package/lib/packageService.js index 2ba7159e2..0c7388f56 100644 --- a/lib/plugins/package/lib/packageService.js +++ b/lib/plugins/package/lib/packageService.js @@ -24,16 +24,20 @@ module.exports = { getExcludes(exclude, excludeLayers) { return serverlessConfigFileUtils - .getServerlessConfigFilePath(this.serverless.config.servicePath) + .getServerlessConfigFilePath(this.serverless) .then(configFilePath => { const packageExcludes = this.serverless.service.package.exclude || []; // add local service plugins Path - const pluginsLocalPath = this.serverless.pluginManager - .parsePluginsObject(this.serverless.service.plugins).localPath; + const pluginsLocalPath = this.serverless.pluginManager.parsePluginsObject( + this.serverless.service.plugins + ).localPath; const localPathExcludes = pluginsLocalPath ? [pluginsLocalPath] : []; // add layer paths - const layerExcludes = excludeLayers ? this.serverless.service.getAllLayers().map( - (layer) => `${this.serverless.service.getLayer(layer).path}/**`) : []; + const layerExcludes = excludeLayers + ? this.serverless.service + .getAllLayers() + .map(layer => `${this.serverless.service.getLayer(layer).path}/**`) + : []; // add defaults for exclude const serverlessConfigFileExclude = configFilePath ? [path.basename(configFilePath)] : []; @@ -63,22 +67,23 @@ module.exports = { if (functionObject.package.artifact) { return BbPromise.resolve(); } - if (functionObject.package.individually || this.serverless.service - .package.individually) { + if (functionObject.package.individually || this.serverless.service.package.individually) { return this.packageFunction(functionName); } shouldPackageService = true; return BbPromise.resolve(); }); const allLayers = this.serverless.service.getAllLayers(); - packagePromises = packagePromises.concat(_.map(allLayers, layerName => { - const layerObject = this.serverless.service.getLayer(layerName); - layerObject.package = layerObject.package || {}; - if (layerObject.package.artifact) { - return BbPromise.resolve(); - } - return this.packageLayer(layerName); - })); + packagePromises = packagePromises.concat( + _.map(allLayers, layerName => { + const layerObject = this.serverless.service.getLayer(layerName); + layerObject.package = layerObject.package || {}; + if (layerObject.package.artifact) { + return BbPromise.resolve(); + } + return this.packageLayer(layerName); + }) + ); return BbPromise.all(packagePromises).then(() => { if (shouldPackageService && !this.serverless.service.package.artifact) { @@ -95,13 +100,20 @@ module.exports = { * This is nearly imposible to actually set on a windows machine, so find all the Go handler * files and pass them into zipFiles as files to add with the execute bit in the zip file */ - const filesToChmodPlusX = process.platform !== 'win32' ? [] : - Object.values(this.serverless.service.functions) - .map(f => Object.assign({ - runtime: this.serverless.service.provider.runtime || 'node8.10', - }, f)) - .filter(f => f.runtime && f.runtime.startsWith('go')) - .map(f => f.handler); + const filesToChmodPlusX = + process.platform !== 'win32' + ? [] + : Object.values(this.serverless.service.functions) + .map(f => + Object.assign( + { + runtime: this.serverless.service.provider.runtime || 'node8.10', + }, + f + ) + ) + .filter(f => f.runtime && f.runtime.startsWith('go')) + .map(f => f.handler); return this.resolveFilePathsAll().then(filePaths => this.zipFiles(filePaths, zipFileName, undefined, filesToChmodPlusX).then(filePath => { @@ -130,8 +142,10 @@ module.exports = { // use the artifact in service config if provided // and if the function is not set to be packaged individually if (this.serverless.service.package.artifact && !funcPackageConfig.individually) { - const filePath = path.join(this.serverless.config.servicePath, - this.serverless.service.package.artifact); + const filePath = path.join( + this.serverless.config.servicePath, + this.serverless.service.package.artifact + ); funcPackageConfig.artifact = filePath; return BbPromise.resolve(filePath); } @@ -139,8 +153,8 @@ module.exports = { const zipFileName = `${functionName}.zip`; const filesToChmodPlusX = []; if (process.platform === 'win32') { - const runtime = functionName.runtime || this.serverless.service.provider.runtime - || 'node8.10'; + const runtime = + functionName.runtime || this.serverless.service.provider.runtime || 'node8.10'; if (runtime.startsWith('go')) { filesToChmodPlusX.push(functionObject.handler); } @@ -212,7 +226,7 @@ module.exports = { resolveFilePathsFromPatterns(params, prefix) { const patterns = []; - params.exclude.forEach((pattern) => { + params.exclude.forEach(pattern => { if (pattern.charAt(0) !== '!') { patterns.push(`!${pattern}`); } else { @@ -222,9 +236,10 @@ module.exports = { // push the include globs to the end of the array // (files and folders will be re-added again even if they were excluded beforehand) - params.include.forEach((pattern) => { + params.include.forEach(pattern => { patterns.push(pattern); }); + // NOTE: please keep this order of concatenating the include params // rather than doing it the other way round! // see https://github.com/serverless/serverless/pull/5825 for more information @@ -242,12 +257,13 @@ module.exports = { .forEach(p => { const exclude = p.startsWith('!'); const pattern = exclude ? p.slice(1) : p; - nanomatch(allFilePaths, [pattern], { dot: true }) - .forEach(key => { - filePathStates[key] = !exclude; - }); + nanomatch(allFilePaths, [pattern], { dot: true }).forEach(key => { + filePathStates[key] = !exclude; + }); }); - const filePaths = _.toPairs(filePathStates).filter(r => r[1] === true).map(r => r[0]); + const filePaths = _.toPairs(filePathStates) + .filter(r => r[1] === true) + .map(r => r[0]); if (filePaths.length !== 0) return filePaths; throw new this.serverless.classes.Error('No file matches include / exclude patterns'); }); diff --git a/lib/plugins/package/lib/packageService.test.js b/lib/plugins/package/lib/packageService.test.js index cda987239..098367a7e 100644 --- a/lib/plugins/package/lib/packageService.test.js +++ b/lib/plugins/package/lib/packageService.test.js @@ -12,7 +12,7 @@ const serverlessConfigFileUtils = require('../../../../lib/utils/getServerlessCo // Configure chai chai.use(require('chai-as-promised')); chai.use(require('sinon-chai')); -const expect = require('chai').expect; +const { expect } = require('chai'); describe('#packageService()', () => { let serverless; @@ -20,6 +20,7 @@ describe('#packageService()', () => { beforeEach(() => { serverless = new Serverless(); + serverless.processedInput = { options: {} }; packagePlugin = new Package(serverless, {}); packagePlugin.serverless.cli = new serverless.classes.CLI(); packagePlugin.serverless.service.functions = { @@ -38,33 +39,22 @@ describe('#packageService()', () => { }); it('should merge package includes', () => { - const packageIncludes = [ - 'dir', 'file.js', - ]; + const packageIncludes = ['dir', 'file.js']; serverless.service.package.include = packageIncludes; const include = packagePlugin.getIncludes(); - expect(include).to.deep.equal([ - 'dir', 'file.js', - ]); + expect(include).to.deep.equal(['dir', 'file.js']); }); it('should merge package and func includes', () => { - const funcIncludes = [ - 'lib', 'other.js', - ]; - const packageIncludes = [ - 'dir', 'file.js', - ]; + const funcIncludes = ['lib', 'other.js']; + const packageIncludes = ['dir', 'file.js']; serverless.service.package.include = packageIncludes; const include = packagePlugin.getIncludes(funcIncludes); - expect(include).to.deep.equal([ - 'dir', 'file.js', - 'lib', 'other.js', - ]); + expect(include).to.deep.equal(['dir', 'file.js', 'lib', 'other.js']); }); }); @@ -83,96 +73,95 @@ describe('#packageService()', () => { }); it('should exclude defaults and serverless config file being used', () => - expect(packagePlugin.getExcludes()).to.be.fulfilled - .then(exclude => BbPromise.join( + expect(packagePlugin.getExcludes()).to.be.fulfilled.then(exclude => + BbPromise.join( expect(getServerlessConfigFilePathStub).to.be.calledOnce, expect(exclude).to.deep.equal( _.union(packagePlugin.defaultExcludes, [serverlessConfigFileName]) ) - )) - ); + ) + )); it('should exclude plugins localPath defaults', () => { const localPath = './myplugins'; serverless.service.plugins = { localPath }; - return expect(packagePlugin.getExcludes()).to.be.fulfilled - .then(exclude => - expect(exclude).to.deep.equal( - _.union(packagePlugin.defaultExcludes, [serverlessConfigFileName], [localPath]) - ) - ); + return expect(packagePlugin.getExcludes()).to.be.fulfilled.then(exclude => + expect(exclude).to.deep.equal( + _.union(packagePlugin.defaultExcludes, [serverlessConfigFileName], [localPath]) + ) + ); }); it('should not exclude plugins localPath if it is empty', () => { const localPath = ''; serverless.service.plugins = { localPath }; - return expect(packagePlugin.getExcludes()).to.be.fulfilled - .then(exclude => - expect(exclude).to.deep.equal( - _.union(packagePlugin.defaultExcludes, [serverlessConfigFileName]) - ) - ); + return expect(packagePlugin.getExcludes()).to.be.fulfilled.then(exclude => + expect(exclude).to.deep.equal( + _.union(packagePlugin.defaultExcludes, [serverlessConfigFileName]) + ) + ); }); it('should not exclude plugins localPath if it is not a string', () => { const localPath = {}; serverless.service.plugins = { localPath }; - return expect(packagePlugin.getExcludes()).to.be.fulfilled - .then(exclude => - expect(exclude).to.deep.equal( - _.union(packagePlugin.defaultExcludes, [serverlessConfigFileName]) - ) - ); + return expect(packagePlugin.getExcludes()).to.be.fulfilled.then(exclude => + expect(exclude).to.deep.equal( + _.union(packagePlugin.defaultExcludes, [serverlessConfigFileName]) + ) + ); }); it('should not exclude serverlessConfigFilePath if is not found', () => { getServerlessConfigFilePathStub.returns(BbPromise.resolve(null)); - return expect(packagePlugin.getExcludes()).to.be.fulfilled - .then(exclude => - expect(exclude).to.deep.equal(packagePlugin.defaultExcludes) - ); + return expect(packagePlugin.getExcludes()).to.be.fulfilled.then(exclude => + expect(exclude).to.deep.equal(packagePlugin.defaultExcludes) + ); }); it('should merge defaults with plugin localPath and excludes', () => { const localPath = './myplugins'; serverless.service.plugins = { localPath }; - const packageExcludes = [ - 'dir', 'file.js', - ]; + const packageExcludes = ['dir', 'file.js']; serverless.service.package.exclude = packageExcludes; - return expect(packagePlugin.getExcludes()).to.be.fulfilled - .then(exclude => - expect(exclude).to.deep.equal(_.union(packagePlugin.defaultExcludes, - [serverlessConfigFileName], [localPath], packageExcludes) + return expect(packagePlugin.getExcludes()).to.be.fulfilled.then(exclude => + expect(exclude).to.deep.equal( + _.union( + packagePlugin.defaultExcludes, + [serverlessConfigFileName], + [localPath], + packageExcludes ) - ); + ) + ); }); it('should merge defaults with plugin localPath package and func excludes', () => { const localPath = './myplugins'; serverless.service.plugins = { localPath }; - const packageExcludes = [ - 'dir', 'file.js', - ]; + const packageExcludes = ['dir', 'file.js']; serverless.service.package.exclude = packageExcludes; - const funcExcludes = [ - 'lib', 'other.js', - ]; + const funcExcludes = ['lib', 'other.js']; - return expect(packagePlugin.getExcludes(funcExcludes)).to.be.fulfilled - .then(exclude => - expect(exclude).to.deep.equal(_.union(packagePlugin.defaultExcludes, - [serverlessConfigFileName], [localPath], packageExcludes, funcExcludes) + return expect(packagePlugin.getExcludes(funcExcludes)).to.be.fulfilled.then(exclude => + expect(exclude).to.deep.equal( + _.union( + packagePlugin.defaultExcludes, + [serverlessConfigFileName], + [localPath], + packageExcludes, + funcExcludes ) - ); + ) + ); }); }); @@ -180,11 +169,11 @@ describe('#packageService()', () => { it('should package all functions', () => { serverless.service.package.individually = false; - const packageAllStub = sinon - .stub(packagePlugin, 'packageAll').resolves(); + const packageAllStub = sinon.stub(packagePlugin, 'packageAll').resolves(); - return expect(packagePlugin.packageService()).to.be.fulfilled - .then(() => expect(packageAllStub).to.be.calledOnce); + return expect(packagePlugin.packageService()).to.be.fulfilled.then( + () => expect(packageAllStub).to.be.calledOnce + ); }); it('should package functions individually', () => { @@ -199,10 +188,12 @@ describe('#packageService()', () => { }; const packageFunctionStub = sinon - .stub(packagePlugin, 'packageFunction').resolves((func) => func.name); + .stub(packagePlugin, 'packageFunction') + .resolves(func => func.name); - return expect(packagePlugin.packageService()).to.be.fulfilled - .then(() => expect(packageFunctionStub).to.be.calledTwice); + return expect(packagePlugin.packageService()).to.be.fulfilled.then( + () => expect(packageFunctionStub).to.be.calledTwice + ); }); it('should package single function individually', () => { @@ -219,15 +210,16 @@ describe('#packageService()', () => { }; const packageFunctionStub = sinon - .stub(packagePlugin, 'packageFunction').resolves((func) => func.name); - const packageAllStub = sinon - .stub(packagePlugin, 'packageAll').resolves((func) => func.name); + .stub(packagePlugin, 'packageFunction') + .resolves(func => func.name); + const packageAllStub = sinon.stub(packagePlugin, 'packageAll').resolves(func => func.name); - return expect(packagePlugin.packageService()).to.be.fulfilled - .then(() => BbPromise.join( - expect(packageFunctionStub).to.be.calledOnce, - expect(packageAllStub).to.be.calledOnce - )); + return expect(packagePlugin.packageService()).to.be.fulfilled.then(() => + BbPromise.join( + expect(packageFunctionStub).to.be.calledOnce, + expect(packageAllStub).to.be.calledOnce + ) + ); }); it('should not package functions if package artifact specified', () => { @@ -235,8 +227,9 @@ describe('#packageService()', () => { const packageAllStub = sinon.stub(packagePlugin, 'packageAll').resolves(); - return expect(packagePlugin.packageService()).to.be.fulfilled - .then(() => expect(packageAllStub).to.not.be.called); + return expect(packagePlugin.packageService()).to.be.fulfilled.then( + () => expect(packageAllStub).to.not.be.called + ); }); it('should package functions individually if package artifact specified', () => { @@ -252,15 +245,16 @@ describe('#packageService()', () => { }; const packageFunctionStub = sinon - .stub(packagePlugin, 'packageFunction').resolves((func) => func.name); - const packageAllStub = sinon - .stub(packagePlugin, 'packageAll').resolves((func) => func.name); + .stub(packagePlugin, 'packageFunction') + .resolves(func => func.name); + const packageAllStub = sinon.stub(packagePlugin, 'packageAll').resolves(func => func.name); - return expect(packagePlugin.packageService()).to.be.fulfilled - .then(() => BbPromise.join( - expect(packageFunctionStub).to.be.calledTwice, - expect(packageAllStub).to.not.be.called - )); + return expect(packagePlugin.packageService()).to.be.fulfilled.then(() => + BbPromise.join( + expect(packageFunctionStub).to.be.calledTwice, + expect(packageAllStub).to.not.be.called + ) + ); }); it('should package single functions individually if package artifact specified', () => { @@ -278,15 +272,16 @@ describe('#packageService()', () => { }; const packageFunctionStub = sinon - .stub(packagePlugin, 'packageFunction').resolves((func) => func.name); - const packageAllStub = sinon - .stub(packagePlugin, 'packageAll').resolves((func) => func.name); + .stub(packagePlugin, 'packageFunction') + .resolves(func => func.name); + const packageAllStub = sinon.stub(packagePlugin, 'packageAll').resolves(func => func.name); - return expect(packagePlugin.packageService()).to.be.fulfilled - .then(() => BbPromise.join( + return expect(packagePlugin.packageService()).to.be.fulfilled.then(() => + BbPromise.join( expect(packageFunctionStub).to.be.calledOnce, expect(packageAllStub).to.not.be.called - )); + ) + ); }); }); @@ -302,13 +297,13 @@ describe('#packageService()', () => { beforeEach(() => { getExcludesStub = sinon - .stub(packagePlugin, 'getExcludes').returns(BbPromise.resolve(exclude)); - getIncludesStub = sinon - .stub(packagePlugin, 'getIncludes').returns(include); + .stub(packagePlugin, 'getExcludes') + .returns(BbPromise.resolve(exclude)); + getIncludesStub = sinon.stub(packagePlugin, 'getIncludes').returns(include); resolveFilePathsFromPatternsStub = sinon - .stub(packagePlugin, 'resolveFilePathsFromPatterns').returns(files); - zipFilesStub = sinon - .stub(packagePlugin, 'zipFiles').resolves(artifactFilePath); + .stub(packagePlugin, 'resolveFilePathsFromPatterns') + .returns(files); + zipFilesStub = sinon.stub(packagePlugin, 'zipFiles').resolves(artifactFilePath); }); afterEach(() => { @@ -324,65 +319,61 @@ describe('#packageService()', () => { serverless.config.servicePath = servicePath; - return expect(packagePlugin.packageService()).to.be.fulfilled - .then(() => BbPromise.all([ - expect(getExcludesStub).to.be.calledOnce, - expect(getIncludesStub).to.be.calledOnce, - expect(resolveFilePathsFromPatternsStub).to.be.calledOnce, - expect(zipFilesStub).to.be.calledOnce, - expect(zipFilesStub).to.have.been.calledWithExactly( - files, - zipFileName, - undefined, - [] - ), - ])); + return expect(packagePlugin.packageService()).to.be.fulfilled.then(() => + BbPromise.all([ + expect(getExcludesStub).to.be.calledOnce, + expect(getIncludesStub).to.be.calledOnce, + expect(resolveFilePathsFromPatternsStub).to.be.calledOnce, + expect(zipFilesStub).to.be.calledOnce, + expect(zipFilesStub).to.have.been.calledWithExactly(files, zipFileName, undefined, []), + ]) + ); }); (process.platfrom === 'win32' ? it : it.skip)( - 'should call zipService with settings & binaries to chmod for GoLang on win32', () => { + 'should call zipService with settings & binaries to chmod for GoLang on win32', + () => { const servicePath = 'test'; const zipFileName = `${serverless.service.service}.zip`; serverless.config.servicePath = servicePath; serverless.service.provider.runtime = 'go1.x'; - return expect(packagePlugin.packageService()).to.be.fulfilled - .then(() => BbPromise.all([ - expect(getExcludesStub).to.be.calledOnce, - expect(getIncludesStub).to.be.calledOnce, - expect(resolveFilePathsFromPatternsStub).to.be.calledOnce, - expect(zipFilesStub).to.be.calledOnce, - expect(zipFilesStub).to.have.been.calledWithExactly( - files, - zipFileName, - undefined, - ['foo'] - ), - ])); - }); + return expect(packagePlugin.packageService()).to.be.fulfilled.then(() => + BbPromise.all([ + expect(getExcludesStub).to.be.calledOnce, + expect(getIncludesStub).to.be.calledOnce, + expect(resolveFilePathsFromPatternsStub).to.be.calledOnce, + expect(zipFilesStub).to.be.calledOnce, + expect(zipFilesStub).to.have.been.calledWithExactly(files, zipFileName, undefined, [ + 'foo', + ]), + ]) + ); + } + ); (process.platfrom === 'win32' ? it : it.skip)( - 'should call zipService with settings & no binaries to chmod for non-go on win32', () => { + 'should call zipService with settings & no binaries to chmod for non-go on win32', + () => { const servicePath = 'test'; const zipFileName = `${serverless.service.service}.zip`; serverless.config.servicePath = servicePath; - return expect(packagePlugin.packageService()).to.be.fulfilled - .then(() => BbPromise.all([ - expect(getExcludesStub).to.be.calledOnce, - expect(getIncludesStub).to.be.calledOnce, - expect(resolveFilePathsFromPatternsStub).to.be.calledOnce, - expect(zipFilesStub).to.be.calledOnce, - expect(zipFilesStub).to.have.been.calledWithExactly( - files, - zipFileName, - undefined, - ['foo'] - ), - ])); - }); + return expect(packagePlugin.packageService()).to.be.fulfilled.then(() => + BbPromise.all([ + expect(getExcludesStub).to.be.calledOnce, + expect(getIncludesStub).to.be.calledOnce, + expect(resolveFilePathsFromPatternsStub).to.be.calledOnce, + expect(zipFilesStub).to.be.calledOnce, + expect(zipFilesStub).to.have.been.calledWithExactly(files, zipFileName, undefined, [ + 'foo', + ]), + ]) + ); + } + ); }); describe('#packageFunction()', () => { @@ -397,13 +388,13 @@ describe('#packageService()', () => { beforeEach(() => { getExcludesStub = sinon - .stub(packagePlugin, 'getExcludes').returns(BbPromise.resolve(exclude)); - getIncludesStub = sinon - .stub(packagePlugin, 'getIncludes').returns(include); + .stub(packagePlugin, 'getExcludes') + .returns(BbPromise.resolve(exclude)); + getIncludesStub = sinon.stub(packagePlugin, 'getIncludes').returns(include); resolveFilePathsFromPatternsStub = sinon - .stub(packagePlugin, 'resolveFilePathsFromPatterns').returns(files); - zipFilesStub = sinon - .stub(packagePlugin, 'zipFiles').resolves(artifactFilePath); + .stub(packagePlugin, 'resolveFilePathsFromPatterns') + .returns(files); + zipFilesStub = sinon.stub(packagePlugin, 'zipFiles').resolves(artifactFilePath); }); afterEach(() => { @@ -423,20 +414,18 @@ describe('#packageService()', () => { serverless.service.functions = {}; serverless.service.functions[funcName] = { name: `test-proj-${funcName}` }; - return expect(packagePlugin.packageFunction(funcName)).to.eventually.equal(artifactFilePath) - .then(() => BbPromise.all([ - expect(getExcludesStub).to.be.calledOnce, - expect(getIncludesStub).to.be.calledOnce, - expect(resolveFilePathsFromPatternsStub).to.be.calledOnce, + return expect(packagePlugin.packageFunction(funcName)) + .to.eventually.equal(artifactFilePath) + .then(() => + BbPromise.all([ + expect(getExcludesStub).to.be.calledOnce, + expect(getIncludesStub).to.be.calledOnce, + expect(resolveFilePathsFromPatternsStub).to.be.calledOnce, - expect(zipFilesStub).to.be.calledOnce, - expect(zipFilesStub).to.have.been.calledWithExactly( - files, - zipFileName, - undefined, - [] - ), - ])); + expect(zipFilesStub).to.be.calledOnce, + expect(zipFilesStub).to.have.been.calledWithExactly(files, zipFileName, undefined, []), + ]) + ); }); it('should return function artifact file path', () => { @@ -452,13 +441,15 @@ describe('#packageService()', () => { }, }; - return expect(packagePlugin.packageFunction(funcName)).to.eventually - .equal(path.join('test/artifact.zip')) - .then(() => BbPromise.all([ - expect(getExcludesStub).to.not.have.been.called, - expect(getIncludesStub).to.not.have.been.called, - expect(zipFilesStub).to.not.have.been.called, - ])); + return expect(packagePlugin.packageFunction(funcName)) + .to.eventually.equal(path.join('test/artifact.zip')) + .then(() => + BbPromise.all([ + expect(getExcludesStub).to.not.have.been.called, + expect(getIncludesStub).to.not.have.been.called, + expect(zipFilesStub).to.not.have.been.called, + ]) + ); }); it('should return service artifact file path', () => { @@ -474,13 +465,15 @@ describe('#packageService()', () => { name: `test-proj-${funcName}`, }; - return expect(packagePlugin.packageFunction(funcName)).to.eventually - .equal(path.join('test/artifact.zip')) - .then(() => BbPromise.all([ - expect(getExcludesStub).to.not.have.been.called, - expect(getIncludesStub).to.not.have.been.called, - expect(zipFilesStub).to.not.have.been.called, - ])); + return expect(packagePlugin.packageFunction(funcName)) + .to.eventually.equal(path.join('test/artifact.zip')) + .then(() => + BbPromise.all([ + expect(getExcludesStub).to.not.have.been.called, + expect(getIncludesStub).to.not.have.been.called, + expect(zipFilesStub).to.not.have.been.called, + ]) + ); }); it('should call zipService with settings if packaging individually without artifact', () => { @@ -499,22 +492,21 @@ describe('#packageService()', () => { package: { individually: true }, }; - return expect(packagePlugin.packageFunction(funcName)).to.eventually.equal(artifactFilePath) - .then(() => BbPromise.all([ - expect(getExcludesStub).to.be.calledOnce, - expect(getIncludesStub).to.be.calledOnce, - expect(resolveFilePathsFromPatternsStub).to.be.calledOnce, + return expect(packagePlugin.packageFunction(funcName)) + .to.eventually.equal(artifactFilePath) + .then(() => + BbPromise.all([ + expect(getExcludesStub).to.be.calledOnce, + expect(getIncludesStub).to.be.calledOnce, + expect(resolveFilePathsFromPatternsStub).to.be.calledOnce, - expect(zipFilesStub).to.be.calledOnce, - expect(zipFilesStub).to.have.been.calledWithExactly( - files, - zipFileName, - undefined, - [] - ), - ])); + expect(zipFilesStub).to.be.calledOnce, + expect(zipFilesStub).to.have.been.calledWithExactly(files, zipFileName, undefined, []), + ]) + ); }); }); + describe('#packageLayer()', () => { const exclude = ['test-exclude']; const include = ['test-include']; @@ -527,13 +519,13 @@ describe('#packageService()', () => { beforeEach(() => { getExcludesStub = sinon - .stub(packagePlugin, 'getExcludes').returns(BbPromise.resolve(exclude)); - getIncludesStub = sinon - .stub(packagePlugin, 'getIncludes').returns(include); + .stub(packagePlugin, 'getExcludes') + .returns(BbPromise.resolve(exclude)); + getIncludesStub = sinon.stub(packagePlugin, 'getIncludes').returns(include); resolveFilePathsFromPatternsStub = sinon - .stub(packagePlugin, 'resolveFilePathsFromPatterns').returns(files); - zipFilesStub = sinon - .stub(packagePlugin, 'zipFiles').resolves(artifactFilePath); + .stub(packagePlugin, 'resolveFilePathsFromPatterns') + .returns(files); + zipFilesStub = sinon.stub(packagePlugin, 'zipFiles').resolves(artifactFilePath); }); afterEach(() => { @@ -553,19 +545,22 @@ describe('#packageService()', () => { serverless.service.layers = {}; serverless.service.layers[layerName] = { path: './foobar' }; - return expect(packagePlugin.packageLayer(layerName)).to.eventually.equal(artifactFilePath) - .then(() => BbPromise.all([ - expect(getExcludesStub).to.be.calledOnce, - expect(getIncludesStub).to.be.calledOnce, - expect(resolveFilePathsFromPatternsStub).to.be.calledOnce, + return expect(packagePlugin.packageLayer(layerName)) + .to.eventually.equal(artifactFilePath) + .then(() => + BbPromise.all([ + expect(getExcludesStub).to.be.calledOnce, + expect(getIncludesStub).to.be.calledOnce, + expect(resolveFilePathsFromPatternsStub).to.be.calledOnce, - expect(zipFilesStub).to.be.calledOnce, - expect(zipFilesStub).to.have.been.calledWithExactly( - files, - zipFileName, - path.resolve('./foobar') - ), - ])); + expect(zipFilesStub).to.be.calledOnce, + expect(zipFilesStub).to.have.been.calledWithExactly( + files, + zipFileName, + path.resolve('./foobar') + ), + ]) + ); }); }); }); diff --git a/lib/plugins/package/lib/zipService.js b/lib/plugins/package/lib/zipService.js index b72381942..739d40b9b 100644 --- a/lib/plugins/package/lib/zipService.js +++ b/lib/plugins/package/lib/zipService.js @@ -1,7 +1,5 @@ 'use strict'; -/* eslint-disable no-use-before-define */ - const BbPromise = require('bluebird'); const archiver = require('archiver'); const os = require('os'); @@ -39,7 +37,7 @@ module.exports = { return BbPromise.bind(this) .then(() => excludeNodeDevDependencies(servicePath)) - .then((exAndInNode) => { + .then(exAndInNode => { params.exclude = _.union(params.exclude, exAndInNode.exclude); //eslint-disable-line params.include = _.union(params.include, exAndInNode.include); //eslint-disable-line return params; @@ -52,7 +50,8 @@ module.exports = { zip(params) { return this.resolveFilePathsFromPatterns(params).then(filePaths => - this.zipFiles(filePaths, params.zipFileName)); + this.zipFiles(filePaths, params.zipFileName) + ); }, /** @@ -71,7 +70,8 @@ module.exports = { const zip = archiver.create('zip'); // Create artifact in temp path and move it to the package path (if any) later - const artifactFilePath = path.join(this.serverless.config.servicePath, + const artifactFilePath = path.join( + this.serverless.config.servicePath, '.serverless', zipFileName ); @@ -81,44 +81,48 @@ module.exports = { return new BbPromise((resolve, reject) => { output.on('close', () => resolve(artifactFilePath)); - output.on('error', (err) => reject(err)); - zip.on('error', (err) => reject(err)); - + output.on('error', err => reject(err)); + zip.on('error', err => reject(err)); output.on('open', () => { zip.pipe(output); - BbPromise.all(files.map(this.getFileContentAndStat.bind(this))).then((contents) => { - _.forEach(_.sortBy(contents, ['filePath']), (file) => { - const name = file.filePath.slice(prefix ? `${prefix}${path.sep}`.length : 0); - let mode = file.stat.mode; - if (filesToChmodPlusX && _.includes(filesToChmodPlusX, name) - && file.stat.mode % 2 === 0) { - mode += 1; - } - zip.append(file.data, { - name, - mode, - date: new Date(0), // necessary to get the same hash when zipping the same content - }); - }); + const normalizedFiles = _.uniq(files.map(file => path.normalize(file))); - zip.finalize(); - }).catch(reject); + return BbPromise.all(normalizedFiles.map(this.getFileContentAndStat.bind(this))) + .then(contents => { + _.forEach(_.sortBy(contents, ['filePath']), file => { + const name = file.filePath.slice(prefix ? `${prefix}${path.sep}`.length : 0); + let mode = file.stat.mode; + if ( + filesToChmodPlusX && + _.includes(filesToChmodPlusX, name) && + file.stat.mode % 2 === 0 + ) { + mode += 1; + } + zip.append(file.data, { + name, + mode, + date: new Date(0), // necessary to get the same hash when zipping the same content + }); + }); + + zip.finalize(); + }) + .catch(reject); }); }); }, getFileContentAndStat(filePath) { - const fullPath = path.resolve( - this.serverless.config.servicePath, - filePath - ); + const fullPath = path.resolve(this.serverless.config.servicePath, filePath); - return BbPromise.all([ // Get file contents and stat in parallel + return BbPromise.all([ + // Get file contents and stat in parallel this.getFileContent(fullPath), fs.statAsync(fullPath), - ]).then((result) => ({ + ]).then(result => ({ data: result[0], stat: result[1], filePath, @@ -145,19 +149,22 @@ function excludeNodeDevDependencies(servicePath) { const nodeProdDepFile = path.join(tmpDir, `node-dependencies-${randHash}-prod`); try { - const packageJsonFilePaths = globby.sync([ - '**/package.json', - // TODO add glob for node_modules filtering - ], { - cwd: servicePath, - dot: true, - silent: true, - follow: true, - nosort: true, - }); + const packageJsonFilePaths = globby.sync( + [ + '**/package.json', + // TODO add glob for node_modules filtering + ], + { + cwd: servicePath, + dot: true, + silent: true, + follow: true, + nosort: true, + } + ); // filter out non node_modules file paths - const packageJsonPaths = _.filter(packageJsonFilePaths, (filePath) => { + const packageJsonPaths = _.filter(packageJsonFilePaths, filePath => { const isNodeModulesDir = !!filePath.match(/node_modules/); return !isNodeModulesDir; }); @@ -167,82 +174,93 @@ function excludeNodeDevDependencies(servicePath) { } // NOTE: using mapSeries here for a sequential computation (w/o race conditions) - return BbPromise.mapSeries(packageJsonPaths, (packageJsonPath) => { - // the path where the package.json file lives - const fullPath = path.join(servicePath, packageJsonPath); - const dirWithPackageJson = fullPath.replace(path.join(path.sep, 'package.json'), ''); + return ( + BbPromise.mapSeries(packageJsonPaths, packageJsonPath => { + // the path where the package.json file lives + const fullPath = path.join(servicePath, packageJsonPath); + const dirWithPackageJson = fullPath.replace(path.join(path.sep, 'package.json'), ''); - // we added a catch which resolves so that npm commands with an exit code of 1 - // (e.g. if the package.json is invalid) won't crash the dev dependency exclusion process - return BbPromise.map(['dev', 'prod'], (env) => { - const depFile = env === 'dev' ? nodeDevDepFile : nodeProdDepFile; - return childProcess.execAsync( - `npm ls --${env}=true --parseable=true --long=false --silent >> ${depFile}`, - { cwd: dirWithPackageJson } - ).catch(() => BbPromise.resolve()); - }); - }) - // NOTE: using mapSeries here for a sequential computation (w/o race conditions) - .then(() => BbPromise.mapSeries(['dev', 'prod'], (env) => { - const depFile = env === 'dev' ? nodeDevDepFile : nodeProdDepFile; - return fs.readFileAsync(depFile) - .then((fileContent) => _.compact( - (_.uniq(_.split(fileContent.toString('utf8'), '\n'))), - elem => elem.length > 0 - )).catch(() => BbPromise.resolve()); - })) - .then((devAndProDependencies) => { - const devDependencies = devAndProDependencies[0]; - const prodDependencies = devAndProDependencies[1]; + // we added a catch which resolves so that npm commands with an exit code of 1 + // (e.g. if the package.json is invalid) won't crash the dev dependency exclusion process + return BbPromise.map(['dev', 'prod'], env => { + const depFile = env === 'dev' ? nodeDevDepFile : nodeProdDepFile; + return childProcess + .execAsync( + `npm ls --${env}=true --parseable=true --long=false --silent >> ${depFile}`, + { cwd: dirWithPackageJson } + ) + .catch(() => BbPromise.resolve()); + }); + }) + // NOTE: using mapSeries here for a sequential computation (w/o race conditions) + .then(() => + BbPromise.mapSeries(['dev', 'prod'], env => { + const depFile = env === 'dev' ? nodeDevDepFile : nodeProdDepFile; + return fs + .readFileAsync(depFile) + .then(fileContent => + _.compact( + _.uniq(_.split(fileContent.toString('utf8'), '\n')), + elem => elem.length > 0 + ) + ) + .catch(() => BbPromise.resolve()); + }) + ) + .then(devAndProDependencies => { + const devDependencies = devAndProDependencies[0]; + const prodDependencies = devAndProDependencies[1]; - // NOTE: the order for _.difference is important - const dependencies = _.difference(devDependencies, prodDependencies); - const nodeModulesRegex = new RegExp(`${path.join('node_modules', path.sep)}.*`, 'g'); + // NOTE: the order for _.difference is important + const dependencies = _.difference(devDependencies, prodDependencies); + const nodeModulesRegex = new RegExp(`${path.join('node_modules', path.sep)}.*`, 'g'); - if (!_.isEmpty(dependencies)) { - return BbPromise - .map(dependencies, (item) => item.replace(path.join(servicePath, path.sep), '')) - .filter((item) => item.length > 0 && item.match(nodeModulesRegex)) - .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); + if (!_.isEmpty(dependencies)) { + return BbPromise.map(dependencies, item => + item.replace(path.join(servicePath, path.sep), '') + ) + .filter(item => item.length > 0 && item.match(nodeModulesRegex)) + .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 packageJson = JSON.parse(packageJsonFile); + const bin = packageJson.bin; - const baseGlobs = [path.join(item, '**')]; + 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)); + // 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); }); - // only 1 executable with same name as lib - } else if (typeof bin === 'string') { - baseGlobs.push(path.join(modulePath, '.bin', moduleName)); - } + }, []) + .then(globs => { + exAndIn.exclude = exAndIn.exclude.concat(globs); + return exAndIn; + }); + } - return globs.concat(baseGlobs); - }); - }, []) - .then((globs) => { - exAndIn.exclude = exAndIn.exclude.concat(globs); - return exAndIn; - }); - } - - return exAndIn; - }) - .then(() => { - // cleanup - fs.unlinkSync(nodeDevDepFile); - fs.unlinkSync(nodeProdDepFile); - return exAndIn; - }) - .catch(() => exAndIn); + return exAndIn; + }) + .then(() => { + // cleanup + fs.unlinkSync(nodeDevDepFile); + fs.unlinkSync(nodeProdDepFile); + return exAndIn; + }) + .catch(() => exAndIn) + ); } catch (e) { // fail silently } diff --git a/lib/plugins/package/lib/zipService.test.js b/lib/plugins/package/lib/zipService.test.js index 562663229..b7050dd64 100644 --- a/lib/plugins/package/lib/zipService.test.js +++ b/lib/plugins/package/lib/zipService.test.js @@ -14,12 +14,12 @@ const childProcess = BbPromise.promisifyAll(require('child_process')); const sinon = require('sinon'); const Package = require('../package'); const Serverless = require('../../../Serverless'); -const testUtils = require('../../../../tests/utils'); +const { getTmpDirPath } = require('../../../../tests/utils/fs'); // Configure chai chai.use(require('chai-as-promised')); chai.use(require('sinon-chai')); -const expect = require('chai').expect; +const { expect } = require('chai'); describe('zipService', () => { let tmpDirPath; @@ -28,7 +28,7 @@ describe('zipService', () => { let params; beforeEach(() => { - tmpDirPath = testUtils.getTmpDirPath(); + tmpDirPath = getTmpDirPath(); serverless = new Serverless(); serverless.service.service = 'first-service'; serverless.config.servicePath = tmpDirPath; @@ -60,11 +60,12 @@ describe('zipService', () => { const include = params.include; const zipFileName = params.zipFileName; - return expect(packagePlugin.zipService(exclude, include, zipFileName)).to.be - .fulfilled.then(() => { + return expect(packagePlugin.zipService(exclude, include, zipFileName)).to.be.fulfilled.then( + () => { expect(excludeDevDependenciesStub).to.have.been.calledOnce; expect(zipStub).to.have.been.calledOnce; - }); + } + ); }); }); @@ -82,10 +83,9 @@ describe('zipService', () => { fs.writeFileSync(filePath, buf); - return expect(packagePlugin.getFileContentAndStat(filePath)).to.be.fulfilled - .then((result) => { - expect(result.data).to.deep.equal(buf); - }); + return expect(packagePlugin.getFileContentAndStat(filePath)).to.be.fulfilled.then(result => { + expect(result.data).to.deep.equal(buf); + }); }); }); @@ -103,10 +103,9 @@ describe('zipService', () => { fs.writeFileSync(filePath, buf); - return expect(packagePlugin.getFileContent(filePath)).to.be.fulfilled - .then((result) => { - expect(result).to.deep.equal(buf); - }); + return expect(packagePlugin.getFileContent(filePath)).to.be.fulfilled.then(result => { + expect(result).to.deep.equal(buf); + }); }); }); @@ -114,10 +113,11 @@ describe('zipService', () => { it('should resolve when opted out of dev dependency exclusion', () => { packagePlugin.serverless.service.package.excludeDevDependencies = false; - return expect(packagePlugin.excludeDevDependencies(params)).to.be - .fulfilled.then((updatedParams) => { + return expect(packagePlugin.excludeDevDependencies(params)).to.be.fulfilled.then( + updatedParams => { expect(updatedParams).to.deep.equal(params); - }); + } + ); }); describe('when dealing with Node.js runtimes', () => { @@ -144,27 +144,23 @@ describe('zipService', () => { globbySyncStub.returns(filePaths); - return expect(packagePlugin.excludeDevDependencies(params)).to.be - .fulfilled.then((updatedParams) => { + return expect(packagePlugin.excludeDevDependencies(params)).to.be.fulfilled.then( + updatedParams => { expect(globbySyncStub).to.have.been.calledOnce; expect(execAsyncStub).to.not.have.been.called; expect(readFileAsyncStub).to.not.have.been.called; - 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', - ]); - expect(updatedParams.include).to.deep.equal([ - 'user-defined-include-me', - ]); + 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']); + expect(updatedParams.include).to.deep.equal(['user-defined-include-me']); expect(updatedParams.zipFileName).to.equal(params.zipFileName); - }); + } + ); }); it('should do nothing if no dependencies are found', () => { @@ -175,53 +171,46 @@ describe('zipService', () => { const depPaths = ''; readFileAsyncStub.resolves(depPaths); - return expect(packagePlugin.excludeDevDependencies(params)).to.be - .fulfilled.then((updatedParams) => { + 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(globbySyncStub).to.have.been - .calledWithExactly(['**/package.json'], { - cwd: packagePlugin.serverless.config.servicePath, - dot: true, - silent: true, - follow: true, - nosort: true, - }); - expect(execAsyncStub.args[0][0]).to - .match(/npm ls --dev=true --parseable=true --long=false --silent >> .+/); - expect(execAsyncStub.args[0][1].cwd).to - .match(/.+/); - expect(execAsyncStub.args[1][0]).to - .match(/npm ls --prod=true --parseable=true --long=false --silent >> .+/); - expect(execAsyncStub.args[1][1].cwd).to - .match(/.+/); - expect(updatedParams.exclude).to.deep.equal([ - 'user-defined-exclude-me', - ]); - expect(updatedParams.include).to.deep.equal([ - 'user-defined-include-me', - ]); + expect(globbySyncStub).to.have.been.calledWithExactly(['**/package.json'], { + cwd: packagePlugin.serverless.config.servicePath, + dot: true, + silent: true, + follow: true, + nosort: true, + }); + expect(execAsyncStub.args[0][0]).to.match( + /npm ls --dev=true --parseable=true --long=false --silent >> .+/ + ); + expect(execAsyncStub.args[0][1].cwd).to.match(/.+/); + expect(execAsyncStub.args[1][0]).to.match( + /npm ls --prod=true --parseable=true --long=false --silent >> .+/ + ); + expect(execAsyncStub.args[1][1].cwd).to.match(/.+/); + expect(updatedParams.exclude).to.deep.equal(['user-defined-exclude-me']); + expect(updatedParams.include).to.deep.equal(['user-defined-include-me']); expect(updatedParams.zipFileName).to.equal(params.zipFileName); - }); + } + ); }); it('should return excludes and includes if an error is thrown in the global scope', () => { - globbySyncStub.rejects(); + globbySyncStub.throws(); - return expect(packagePlugin.excludeDevDependencies(params)).to.be - .fulfilled.then((updatedParams) => { + return expect(packagePlugin.excludeDevDependencies(params)).to.be.fulfilled.then( + updatedParams => { expect(globbySyncStub).to.have.been.calledOnce; expect(execAsyncStub).to.not.have.been.called; expect(readFileAsyncStub).to.not.have.been.called; - expect(updatedParams.exclude).to.deep.equal([ - 'user-defined-exclude-me', - ]); - expect(updatedParams.include).to.deep.equal([ - 'user-defined-include-me', - ]); + expect(updatedParams.exclude).to.deep.equal(['user-defined-exclude-me']); + expect(updatedParams.include).to.deep.equal(['user-defined-include-me']); expect(updatedParams.zipFileName).to.equal(params.zipFileName); - }); + } + ); }); it('should return excludes and includes if a exec Promise is rejected', () => { @@ -232,19 +221,16 @@ describe('zipService', () => { execAsyncStub.onCall(1).rejects(); readFileAsyncStub.resolves(); - return expect(packagePlugin.excludeDevDependencies(params)).to.be - .fulfilled.then((updatedParams) => { + 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.been.calledTwice; - expect(updatedParams.exclude).to.deep.equal([ - 'user-defined-exclude-me', - ]); - expect(updatedParams.include).to.deep.equal([ - 'user-defined-include-me', - ]); + expect(updatedParams.exclude).to.deep.equal(['user-defined-exclude-me']); + expect(updatedParams.include).to.deep.equal(['user-defined-include-me']); expect(updatedParams.zipFileName).to.equal(params.zipFileName); - }); + } + ); }); it('should return excludes and includes if a readFile Promise is rejected', () => { @@ -256,25 +242,23 @@ describe('zipService', () => { readFileAsyncStub.onCall(0).resolves(); readFileAsyncStub.onCall(1).rejects(); - return expect(packagePlugin.excludeDevDependencies(params)).to.be - .fulfilled.then((updatedParams) => { + 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.been.calledTwice; - expect(updatedParams.exclude).to.deep.equal([ - 'user-defined-exclude-me', - ]); - expect(updatedParams.include).to.deep.equal([ - 'user-defined-include-me', - ]); + expect(updatedParams.exclude).to.deep.equal(['user-defined-exclude-me']); + expect(updatedParams.include).to.deep.equal(['user-defined-include-me']); expect(updatedParams.zipFileName).to.equal(params.zipFileName); - }); + } + ); }); it('should fail silently and continue if "npm ls" call throws an error', () => { const filePaths = [ // root of the service - 'package.json', 'node_modules', + 'package.json', + 'node_modules', // nested-dir // NOTE: reading the dependencies in this directory will fail in this tests path.join('1st', 'package.json'), @@ -304,65 +288,54 @@ describe('zipService', () => { readFileAsyncStub.onCall(4).resolves('{}'); readFileAsyncStub.onCall(5).resolves('{}'); - return expect(packagePlugin.excludeDevDependencies(params)).to.be - .fulfilled.then((updatedParams) => { + 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(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, - dot: true, - silent: true, - follow: true, - nosort: true, - }); - expect(execAsyncStub.args[0][0]).to - .match(/npm ls --dev=true --parseable=true --long=false --silent >> .+/); - expect(execAsyncStub.args[0][1].cwd).to - .match(/.+/); - expect(execAsyncStub.args[1][0]).to - .match(/npm ls --prod=true --parseable=true --long=false --silent >> .+/); - expect(execAsyncStub.args[1][1].cwd).to - .match(/.+/); - expect(execAsyncStub.args[2][0]).to - .match(/npm ls --dev=true --parseable=true --long=false --silent >> .+/); - expect(execAsyncStub.args[2][1].cwd).to - .match(/.+/); - expect(execAsyncStub.args[3][0]).to - .match(/npm ls --prod=true --parseable=true --long=false --silent >> .+/); - expect(execAsyncStub.args[3][1].cwd).to - .match(/.+/); - expect(execAsyncStub.args[4][0]).to - .match(/npm ls --dev=true --parseable=true --long=false --silent >> .+/); - expect(execAsyncStub.args[4][1].cwd).to - .match(/.+/); - expect(execAsyncStub.args[5][0]).to - .match(/npm ls --prod=true --parseable=true --long=false --silent >> .+/); - expect(execAsyncStub.args[5][1].cwd).to - .match(/.+/); + 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, + dot: true, + silent: true, + follow: true, + nosort: true, + }); + expect(execAsyncStub.args[0][0]).to.match( + /npm ls --dev=true --parseable=true --long=false --silent >> .+/ + ); + expect(execAsyncStub.args[0][1].cwd).to.match(/.+/); + expect(execAsyncStub.args[1][0]).to.match( + /npm ls --prod=true --parseable=true --long=false --silent >> .+/ + ); + expect(execAsyncStub.args[1][1].cwd).to.match(/.+/); + expect(execAsyncStub.args[2][0]).to.match( + /npm ls --dev=true --parseable=true --long=false --silent >> .+/ + ); + expect(execAsyncStub.args[2][1].cwd).to.match(/.+/); + expect(execAsyncStub.args[3][0]).to.match( + /npm ls --prod=true --parseable=true --long=false --silent >> .+/ + ); + expect(execAsyncStub.args[3][1].cwd).to.match(/.+/); + expect(execAsyncStub.args[4][0]).to.match( + /npm ls --dev=true --parseable=true --long=false --silent >> .+/ + ); + expect(execAsyncStub.args[4][1].cwd).to.match(/.+/); + expect(execAsyncStub.args[5][0]).to.match( + /npm ls --prod=true --parseable=true --long=false --silent >> .+/ + ); + expect(execAsyncStub.args[5][1].cwd).to.match(/.+/); expect(updatedParams.exclude).to.deep.equal([ 'user-defined-exclude-me', path.join('node_modules', 'module-1', '**'), @@ -370,12 +343,10 @@ describe('zipService', () => { 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', - ]); + expect(updatedParams.include).to.deep.equal(['user-defined-include-me']); expect(updatedParams.zipFileName).to.equal(params.zipFileName); - }); + } + ); }); it('should exclude dev dependencies in the services root directory', () => { @@ -392,47 +363,48 @@ describe('zipService', () => { readFileAsyncStub.onCall(2).resolves('{}'); readFileAsyncStub.onCall(3).resolves('{}'); - return expect(packagePlugin.excludeDevDependencies(params)).to.be - .fulfilled.then((updatedParams) => { + 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.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, - dot: true, - silent: true, - follow: true, - nosort: true, - }); - expect(execAsyncStub.args[0][0]).to - .match(/npm ls --dev=true --parseable=true --long=false --silent >> .+/); - expect(execAsyncStub.args[0][1].cwd).to - .match(/.+/); - expect(execAsyncStub.args[1][0]).to - .match(/npm ls --prod=true --parseable=true --long=false --silent >> .+/); - expect(execAsyncStub.args[1][1].cwd).to - .match(/.+/); + 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, + dot: true, + silent: true, + follow: true, + nosort: true, + }); + expect(execAsyncStub.args[0][0]).to.match( + /npm ls --dev=true --parseable=true --long=false --silent >> .+/ + ); + expect(execAsyncStub.args[0][1].cwd).to.match(/.+/); + expect(execAsyncStub.args[1][0]).to.match( + /npm ls --prod=true --parseable=true --long=false --silent >> .+/ + ); + expect(execAsyncStub.args[1][1].cwd).to.match(/.+/); expect(updatedParams.exclude).to.deep.equal([ 'user-defined-exclude-me', path.join('node_modules', 'module-1', '**'), path.join('node_modules', 'module-2', '**'), ]); - expect(updatedParams.include).to.deep.equal([ - 'user-defined-include-me', - ]); + expect(updatedParams.include).to.deep.equal(['user-defined-include-me']); expect(updatedParams.zipFileName).to.equal(params.zipFileName); - }); + } + ); }); it('should exclude dev dependencies in deeply nested services directories', () => { const filePaths = [ // root of the service - 'package.json', 'node_modules', + 'package.json', + 'node_modules', // nested-dir path.join('1st', 'package.json'), path.join('1st', 'node_modules'), @@ -460,43 +432,42 @@ describe('zipService', () => { readFileAsyncStub.onCall(6).resolves('{}'); readFileAsyncStub.onCall(7).resolves('{}'); - return expect(packagePlugin.excludeDevDependencies(params)).to.be - .fulfilled.then((updatedParams) => { + 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); - expect(globbySyncStub).to.have.been - .calledWithExactly(['**/package.json'], { - cwd: packagePlugin.serverless.config.servicePath, - dot: true, - silent: true, - follow: true, - nosort: true, - }); - expect(execAsyncStub.args[0][0]).to - .match(/npm ls --dev=true --parseable=true --long=false --silent >> .+/); - expect(execAsyncStub.args[0][1].cwd).to - .match(/.+/); - expect(execAsyncStub.args[1][0]).to - .match(/npm ls --prod=true --parseable=true --long=false --silent >> .+/); - expect(execAsyncStub.args[1][1].cwd).to - .match(/.+/); - expect(execAsyncStub.args[2][0]).to - .match(/npm ls --dev=true --parseable=true --long=false --silent >> .+/); - expect(execAsyncStub.args[2][1].cwd).to - .match(/.+/); - expect(execAsyncStub.args[3][0]).to - .match(/npm ls --prod=true --parseable=true --long=false --silent >> .+/); - expect(execAsyncStub.args[3][1].cwd).to - .match(/.+/); - expect(execAsyncStub.args[4][0]).to - .match(/npm ls --dev=true --parseable=true --long=false --silent >> .+/); - expect(execAsyncStub.args[4][1].cwd).to - .match(/.+/); - expect(execAsyncStub.args[5][0]).to - .match(/npm ls --prod=true --parseable=true --long=false --silent >> .+/); - expect(execAsyncStub.args[5][1].cwd).to - .match(/.+/); + expect(globbySyncStub).to.have.been.calledWithExactly(['**/package.json'], { + cwd: packagePlugin.serverless.config.servicePath, + dot: true, + silent: true, + follow: true, + nosort: true, + }); + expect(execAsyncStub.args[0][0]).to.match( + /npm ls --dev=true --parseable=true --long=false --silent >> .+/ + ); + expect(execAsyncStub.args[0][1].cwd).to.match(/.+/); + expect(execAsyncStub.args[1][0]).to.match( + /npm ls --prod=true --parseable=true --long=false --silent >> .+/ + ); + expect(execAsyncStub.args[1][1].cwd).to.match(/.+/); + expect(execAsyncStub.args[2][0]).to.match( + /npm ls --dev=true --parseable=true --long=false --silent >> .+/ + ); + expect(execAsyncStub.args[2][1].cwd).to.match(/.+/); + expect(execAsyncStub.args[3][0]).to.match( + /npm ls --prod=true --parseable=true --long=false --silent >> .+/ + ); + expect(execAsyncStub.args[3][1].cwd).to.match(/.+/); + expect(execAsyncStub.args[4][0]).to.match( + /npm ls --dev=true --parseable=true --long=false --silent >> .+/ + ); + expect(execAsyncStub.args[4][1].cwd).to.match(/.+/); + expect(execAsyncStub.args[5][0]).to.match( + /npm ls --prod=true --parseable=true --long=false --silent >> .+/ + ); + expect(execAsyncStub.args[5][1].cwd).to.match(/.+/); expect(updatedParams.exclude).to.deep.equal([ 'user-defined-exclude-me', path.join('node_modules', 'module-1', '**'), @@ -506,11 +477,10 @@ describe('zipService', () => { 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', - ]); + expect(updatedParams.include).to.deep.equal(['user-defined-include-me']); expect(updatedParams.zipFileName).to.equal(params.zipFileName); - }); + } + ); }); it('should not include packages if in both dependencies and devDependencies', () => { @@ -525,44 +495,41 @@ describe('zipService', () => { ].join('\n'); readFileAsyncStub.withArgs(sinon.match(/dev$/)).resolves(devDepPaths); - const prodDepPaths = [ - path.join(servicePath, 'node_modules', 'module-2'), - ]; + const prodDepPaths = [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) => { + 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.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, - dot: true, - silent: true, - follow: true, - nosort: true, - }); - expect(execAsyncStub.args[0][0]).to - .match(/npm ls --dev=true --parseable=true --long=false --silent >> .+/); - expect(execAsyncStub.args[0][1].cwd).to - .match(/.+/); - expect(execAsyncStub.args[1][0]).to - .match(/npm ls --prod=true --parseable=true --long=false --silent >> .+/); - expect(execAsyncStub.args[1][1].cwd).to - .match(/.+/); + 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, + dot: true, + silent: true, + follow: true, + nosort: true, + }); + expect(execAsyncStub.args[0][0]).to.match( + /npm ls --dev=true --parseable=true --long=false --silent >> .+/ + ); + expect(execAsyncStub.args[0][1].cwd).to.match(/.+/); + expect(execAsyncStub.args[1][0]).to.match( + /npm ls --prod=true --parseable=true --long=false --silent >> .+/ + ); + expect(execAsyncStub.args[1][1].cwd).to.match(/.+/); expect(updatedParams.exclude).to.deep.equal([ 'user-defined-exclude-me', path.join('node_modules', 'module-1', '**'), ]); - expect(updatedParams.include).to.deep.equal([ - 'user-defined-include-me', - ]); + 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', () => { @@ -573,19 +540,14 @@ describe('zipService', () => { 'node_modules/meowmix', ]; - const prodPaths = [ - 'node_modules/node-dude', - ]; + const prodPaths = ['node_modules/node-dude']; - const filePaths = [ - 'node_modules/', - 'package.json', - ].concat(devPaths).concat(prodPaths); + const filePaths = ['node_modules/', 'package.json'].concat(devPaths).concat(prodPaths); globbySyncStub.returns(filePaths); execAsyncStub.resolves(); - const mapper = (depPath) => path.join(`${servicePath}`, depPath); + const mapper = depPath => path.join(`${servicePath}`, depPath); const devDepPaths = devPaths.map(mapper).join('\n'); readFileAsyncStub.withArgs(sinon.match(/dev$/)).resolves(devDepPaths); @@ -593,9 +555,7 @@ describe('zipService', () => { 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(2).resolves('{"name": "bro-module", "bin": "main.js"}'); readFileAsyncStub .onCall(3) .resolves('{"name": "lumo-clj", "bin": {"lumo": "./bin/lumo.js"}}'); @@ -604,18 +564,21 @@ describe('zipService', () => { // 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) => { + 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(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', @@ -627,16 +590,16 @@ describe('zipService', () => { path.join('node_modules', '.bin', 'meow'), path.join('node_modules', '.bin', 'mix'), ]); - expect(updatedParams.include).to.deep.equal([ - 'user-defined-include-me', - ]); + 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', + 'package.json', + 'node_modules', path.join('1st', 'package.json'), path.join('1st', 'node_modules'), path.join('1st', '2nd', 'package.json'), @@ -653,7 +616,7 @@ describe('zipService', () => { '1st/2nd/node_modules/module-1', '1st/2nd/node_modules/module-2', ]; - const depPaths = deps.map((depPath) => path.join(`${servicePath}`, depPath)); + const depPaths = deps.map(depPath => path.join(`${servicePath}`, depPath)); readFileAsyncStub.withArgs(sinon.match(/dev$/)).resolves(depPaths.join('\n')); readFileAsyncStub.withArgs(sinon.match(/prod$/)).resolves([]); @@ -675,23 +638,23 @@ describe('zipService', () => { readFileAsyncStub.onCall(6).resolves(module1PackageJson); readFileAsyncStub.onCall(7).resolves(module2PackageJson); - return expect(packagePlugin.excludeDevDependencies(params)).to.be - .fulfilled.then((updatedParams) => { + 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(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(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', @@ -708,11 +671,10 @@ describe('zipService', () => { 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', - ]); + expect(updatedParams.include).to.deep.equal(['user-defined-include-me']); expect(updatedParams.zipFileName).to.equal(params.zipFileName); - }); + } + ); }); }); }); @@ -729,7 +691,7 @@ describe('zipService', () => { 'file-2': 'some content', }, // bin - bin: { + 'bin': { 'binary-777': { content: 'some content', permissions: 777, @@ -740,7 +702,7 @@ describe('zipService', () => { }, }, // lib - lib: { + 'lib': { 'file-1.js': 'some content', }, 'lib/directory-1': { @@ -758,7 +720,7 @@ describe('zipService', () => { }; function getTestArtifactFileName(testName) { - return `test-${testName}-${(new Date()).getTime().toString()}.zip`; + return `test-${testName}-${new Date().getTime().toString()}.zip`; } beforeEach(() => { @@ -789,120 +751,122 @@ describe('zipService', () => { it('should zip a whole service (without include / exclude usage)', () => { params.zipFileName = getTestArtifactFileName('whole-service'); - return expect(packagePlugin.zip(params)).to.eventually.be - .equal(path.join(serverless.config.servicePath, '.serverless', params.zipFileName)) - .then(artifact => { - const data = fs.readFileSync(artifact); - return expect(zip.loadAsync(data)).to.be.fulfilled; - }) - .then(unzippedData => { - const unzippedFileData = unzippedData.files; + return expect(packagePlugin.zip(params)) + .to.eventually.be.equal( + path.join(serverless.config.servicePath, '.serverless', params.zipFileName) + ) + .then(artifact => { + const data = fs.readFileSync(artifact); + return expect(zip.loadAsync(data)).to.be.fulfilled; + }) + .then(unzippedData => { + const unzippedFileData = unzippedData.files; - expect(Object.keys(unzippedFileData) - .filter(file => !unzippedFileData[file].dir)) - .to.be.lengthOf(12); + expect( + Object.keys(unzippedFileData).filter(file => !unzippedFileData[file].dir) + ).to.be.lengthOf(12); - // root directory - expect(unzippedFileData['event.json'].name) - .to.equal('event.json'); - expect(unzippedFileData['handler.js'].name) - .to.equal('handler.js'); - expect(unzippedFileData['file-1'].name) - .to.equal('file-1'); - expect(unzippedFileData['file-2'].name) - .to.equal('file-2'); + // root directory + expect(unzippedFileData['event.json'].name).to.equal('event.json'); + expect(unzippedFileData['handler.js'].name).to.equal('handler.js'); + expect(unzippedFileData['file-1'].name).to.equal('file-1'); + expect(unzippedFileData['file-2'].name).to.equal('file-2'); - // bin directory - expect(unzippedFileData['bin/binary-777'].name) - .to.equal('bin/binary-777'); - expect(unzippedFileData['bin/binary-444'].name) - .to.equal('bin/binary-444'); + // bin directory + expect(unzippedFileData['bin/binary-777'].name).to.equal('bin/binary-777'); + expect(unzippedFileData['bin/binary-444'].name).to.equal('bin/binary-444'); - // lib directory - expect(unzippedFileData['lib/file-1.js'].name) - .to.equal('lib/file-1.js'); - expect(unzippedFileData['lib/directory-1/file-1.js'].name) - .to.equal('lib/directory-1/file-1.js'); + // lib directory + expect(unzippedFileData['lib/file-1.js'].name).to.equal('lib/file-1.js'); + expect(unzippedFileData['lib/directory-1/file-1.js'].name).to.equal( + 'lib/directory-1/file-1.js' + ); - // node_modules directory - expect(unzippedFileData['node_modules/directory-1/file-1'].name) - .to.equal('node_modules/directory-1/file-1'); - expect(unzippedFileData['node_modules/directory-1/file-2'].name) - .to.equal('node_modules/directory-1/file-2'); - expect(unzippedFileData['node_modules/directory-2/file-1'].name) - .to.equal('node_modules/directory-2/file-1'); - expect(unzippedFileData['node_modules/directory-2/file-2'].name) - .to.equal('node_modules/directory-2/file-2'); - }); + // node_modules directory + expect(unzippedFileData['node_modules/directory-1/file-1'].name).to.equal( + 'node_modules/directory-1/file-1' + ); + expect(unzippedFileData['node_modules/directory-1/file-2'].name).to.equal( + 'node_modules/directory-1/file-2' + ); + expect(unzippedFileData['node_modules/directory-2/file-1'].name).to.equal( + 'node_modules/directory-2/file-1' + ); + expect(unzippedFileData['node_modules/directory-2/file-2'].name).to.equal( + 'node_modules/directory-2/file-2' + ); + }); }); it('should keep file permissions', () => { params.zipFileName = getTestArtifactFileName('file-permissions'); - return expect(packagePlugin.zip(params)).to.eventually.be - .equal(path.join(serverless.config.servicePath, '.serverless', params.zipFileName)) - .then(artifact => { - const data = fs.readFileSync(artifact); - return expect(zip.loadAsync(data)).to.be.fulfilled; - }).then(unzippedData => { - const unzippedFileData = unzippedData.files; + return expect(packagePlugin.zip(params)) + .to.eventually.be.equal( + path.join(serverless.config.servicePath, '.serverless', params.zipFileName) + ) + .then(artifact => { + const data = fs.readFileSync(artifact); + return expect(zip.loadAsync(data)).to.be.fulfilled; + }) + .then(unzippedData => { + const unzippedFileData = unzippedData.files; - if (os.platform() === 'win32') { - // chmod does not work right on windows. this is better than nothing? - expect(unzippedFileData['bin/binary-777'].unixPermissions) - .to.not.equal(unzippedFileData['bin/binary-444'].unixPermissions); - } else { - // binary file is set with chmod of 777 - expect(unzippedFileData['bin/binary-777'].unixPermissions) - .to.equal(Math.pow(2, 15) + 777); + if (os.platform() === 'win32') { + // chmod does not work right on windows. this is better than nothing? + expect(unzippedFileData['bin/binary-777'].unixPermissions).to.not.equal( + unzippedFileData['bin/binary-444'].unixPermissions + ); + } else { + // binary file is set with chmod of 777 + expect(unzippedFileData['bin/binary-777'].unixPermissions).to.equal( + Math.pow(2, 15) + 777 + ); - // read only file is set with chmod of 444 - expect(unzippedFileData['bin/binary-444'].unixPermissions) - .to.equal(Math.pow(2, 15) + 444); - } - }); + // read only file is set with chmod of 444 + expect(unzippedFileData['bin/binary-444'].unixPermissions).to.equal( + Math.pow(2, 15) + 444 + ); + } + }); }); it('should exclude with globs', () => { params.zipFileName = getTestArtifactFileName('exclude-with-globs'); - params.exclude = [ - 'event.json', - 'lib/**', - 'node_modules/directory-1/**', - ]; + params.exclude = ['event.json', 'lib/**', 'node_modules/directory-1/**']; - return expect(packagePlugin.zip(params)).to.eventually.be - .equal(path.join(serverless.config.servicePath, '.serverless', params.zipFileName)) - .then(artifact => { - const data = fs.readFileSync(artifact); - return expect(zip.loadAsync(data)).to.be.fulfilled; - }).then(unzippedData => { - const unzippedFileData = unzippedData.files; + return expect(packagePlugin.zip(params)) + .to.eventually.be.equal( + path.join(serverless.config.servicePath, '.serverless', params.zipFileName) + ) + .then(artifact => { + const data = fs.readFileSync(artifact); + return expect(zip.loadAsync(data)).to.be.fulfilled; + }) + .then(unzippedData => { + const unzippedFileData = unzippedData.files; - expect(Object.keys(unzippedFileData) - .filter(file => !unzippedFileData[file].dir)) - .to.be.lengthOf(7); + expect( + Object.keys(unzippedFileData).filter(file => !unzippedFileData[file].dir) + ).to.be.lengthOf(7); - // root directory - expect(unzippedFileData['handler.js'].name) - .to.equal('handler.js'); - expect(unzippedFileData['file-1'].name) - .to.equal('file-1'); - expect(unzippedFileData['file-2'].name) - .to.equal('file-2'); + // root directory + expect(unzippedFileData['handler.js'].name).to.equal('handler.js'); + expect(unzippedFileData['file-1'].name).to.equal('file-1'); + expect(unzippedFileData['file-2'].name).to.equal('file-2'); - // bin directory - expect(unzippedFileData['bin/binary-777'].name) - .to.equal('bin/binary-777'); - expect(unzippedFileData['bin/binary-444'].name) - .to.equal('bin/binary-444'); + // bin directory + expect(unzippedFileData['bin/binary-777'].name).to.equal('bin/binary-777'); + expect(unzippedFileData['bin/binary-444'].name).to.equal('bin/binary-444'); - // node_modules directory - expect(unzippedFileData['node_modules/directory-2/file-1'].name) - .to.equal('node_modules/directory-2/file-1'); - expect(unzippedFileData['node_modules/directory-2/file-2'].name) - .to.equal('node_modules/directory-2/file-2'); - }); + // node_modules directory + expect(unzippedFileData['node_modules/directory-2/file-1'].name).to.equal( + 'node_modules/directory-2/file-1' + ); + expect(unzippedFileData['node_modules/directory-2/file-2'].name).to.equal( + 'node_modules/directory-2/file-2' + ); + }); }); it('should re-include files using ! glob pattern', () => { @@ -916,118 +880,131 @@ describe('zipService', () => { '!lib/**', // re-include ]; - return expect(packagePlugin.zip(params)).to.eventually.be - .equal(path.join(serverless.config.servicePath, '.serverless', params.zipFileName)) - .then(artifact => { - const data = fs.readFileSync(artifact); - return expect(zip.loadAsync(data)).to.be.fulfilled; - }).then(unzippedData => { - const unzippedFileData = unzippedData.files; + return expect(packagePlugin.zip(params)) + .to.eventually.be.equal( + path.join(serverless.config.servicePath, '.serverless', params.zipFileName) + ) + .then(artifact => { + const data = fs.readFileSync(artifact); + return expect(zip.loadAsync(data)).to.be.fulfilled; + }) + .then(unzippedData => { + const unzippedFileData = unzippedData.files; - expect(Object.keys(unzippedFileData) - .filter(file => !unzippedFileData[file].dir)) - .to.be.lengthOf(10); + expect( + Object.keys(unzippedFileData).filter(file => !unzippedFileData[file].dir) + ).to.be.lengthOf(10); - // root directory - expect(unzippedFileData['event.json'].name) - .to.equal('event.json'); - expect(unzippedFileData['handler.js'].name) - .to.equal('handler.js'); - expect(unzippedFileData['file-1'].name) - .to.equal('file-1'); - expect(unzippedFileData['file-2'].name) - .to.equal('file-2'); + // root directory + expect(unzippedFileData['event.json'].name).to.equal('event.json'); + expect(unzippedFileData['handler.js'].name).to.equal('handler.js'); + expect(unzippedFileData['file-1'].name).to.equal('file-1'); + expect(unzippedFileData['file-2'].name).to.equal('file-2'); - // bin directory - expect(unzippedFileData['bin/binary-777'].name) - .to.equal('bin/binary-777'); - expect(unzippedFileData['bin/binary-444'].name) - .to.equal('bin/binary-444'); + // bin directory + expect(unzippedFileData['bin/binary-777'].name).to.equal('bin/binary-777'); + expect(unzippedFileData['bin/binary-444'].name).to.equal('bin/binary-444'); - // lib directory - expect(unzippedFileData['lib/file-1.js'].name) - .to.equal('lib/file-1.js'); - expect(unzippedFileData['lib/directory-1/file-1.js'].name) - .to.equal('lib/directory-1/file-1.js'); + // lib directory + expect(unzippedFileData['lib/file-1.js'].name).to.equal('lib/file-1.js'); + expect(unzippedFileData['lib/directory-1/file-1.js'].name).to.equal( + 'lib/directory-1/file-1.js' + ); - // node_modules directory - expect(unzippedFileData['node_modules/directory-2/file-1'].name) - .to.equal('node_modules/directory-2/file-1'); - expect(unzippedFileData['node_modules/directory-2/file-2'].name) - .to.equal('node_modules/directory-2/file-2'); - }); + // node_modules directory + expect(unzippedFileData['node_modules/directory-2/file-1'].name).to.equal( + 'node_modules/directory-2/file-1' + ); + expect(unzippedFileData['node_modules/directory-2/file-2'].name).to.equal( + 'node_modules/directory-2/file-2' + ); + }); }); it('should re-include files using include config', () => { params.zipFileName = getTestArtifactFileName('re-include-with-include'); - params.exclude = [ - 'event.json', - 'lib/**', - 'node_modules/directory-1/**', - ]; - params.include = [ - 'event.json', - 'lib/**', - ]; + params.exclude = ['event.json', 'lib/**', 'node_modules/directory-1/**']; + params.include = ['event.json', 'lib/**']; - return expect(packagePlugin.zip(params)).to.eventually.be - .equal(path.join(serverless.config.servicePath, '.serverless', params.zipFileName)) - .then(artifact => { - const data = fs.readFileSync(artifact); - return expect(zip.loadAsync(data)).to.be.fulfilled; - }).then(unzippedData => { - const unzippedFileData = unzippedData.files; + return expect(packagePlugin.zip(params)) + .to.eventually.be.equal( + path.join(serverless.config.servicePath, '.serverless', params.zipFileName) + ) + .then(artifact => { + const data = fs.readFileSync(artifact); + return expect(zip.loadAsync(data)).to.be.fulfilled; + }) + .then(unzippedData => { + const unzippedFileData = unzippedData.files; - expect(Object.keys(unzippedFileData) - .filter(file => !unzippedFileData[file].dir)) - .to.be.lengthOf(10); + expect( + Object.keys(unzippedFileData).filter(file => !unzippedFileData[file].dir) + ).to.be.lengthOf(10); - // root directory - expect(unzippedFileData['event.json'].name) - .to.equal('event.json'); - expect(unzippedFileData['handler.js'].name) - .to.equal('handler.js'); - expect(unzippedFileData['file-1'].name) - .to.equal('file-1'); - expect(unzippedFileData['file-2'].name) - .to.equal('file-2'); + // root directory + expect(unzippedFileData['event.json'].name).to.equal('event.json'); + expect(unzippedFileData['handler.js'].name).to.equal('handler.js'); + expect(unzippedFileData['file-1'].name).to.equal('file-1'); + expect(unzippedFileData['file-2'].name).to.equal('file-2'); - // bin directory - expect(unzippedFileData['bin/binary-777'].name) - .to.equal('bin/binary-777'); - expect(unzippedFileData['bin/binary-444'].name) - .to.equal('bin/binary-444'); + // bin directory + expect(unzippedFileData['bin/binary-777'].name).to.equal('bin/binary-777'); + expect(unzippedFileData['bin/binary-444'].name).to.equal('bin/binary-444'); - // lib directory - expect(unzippedFileData['lib/file-1.js'].name) - .to.equal('lib/file-1.js'); - expect(unzippedFileData['lib/directory-1/file-1.js'].name) - .to.equal('lib/directory-1/file-1.js'); + // lib directory + expect(unzippedFileData['lib/file-1.js'].name).to.equal('lib/file-1.js'); + expect(unzippedFileData['lib/directory-1/file-1.js'].name).to.equal( + 'lib/directory-1/file-1.js' + ); - // node_modules directory - expect(unzippedFileData['node_modules/directory-2/file-1'].name) - .to.equal('node_modules/directory-2/file-1'); - expect(unzippedFileData['node_modules/directory-2/file-2'].name) - .to.equal('node_modules/directory-2/file-2'); - }); + // node_modules directory + expect(unzippedFileData['node_modules/directory-2/file-1'].name).to.equal( + 'node_modules/directory-2/file-1' + ); + expect(unzippedFileData['node_modules/directory-2/file-2'].name).to.equal( + 'node_modules/directory-2/file-2' + ); + }); }); it('should include files even if outside working dir', () => { params.zipFileName = getTestArtifactFileName('include-outside-working-dir'); serverless.config.servicePath = path.join(serverless.config.servicePath, 'lib'); - params.exclude = [ - './**', - ]; - params.include = [ - '../bin/binary-**', - ]; + params.exclude = ['./**']; + params.include = ['../bin/binary-**']; - return expect(packagePlugin.zip(params)).to.eventually.be - .equal(path.join(serverless.config.servicePath, '.serverless', params.zipFileName)) + return expect(packagePlugin.zip(params)) + .to.eventually.be.equal( + path.join(serverless.config.servicePath, '.serverless', params.zipFileName) + ) .then(artifact => { const data = fs.readFileSync(artifact); return expect(zip.loadAsync(data)).to.be.fulfilled; - }).then(unzippedData => { + }) + .then(unzippedData => { + const unzippedFileData = unzippedData.files; + expect(Object.keys(unzippedFileData).sort()).to.deep.equal([ + 'bin/binary-444', + 'bin/binary-777', + ]); + }); + }); + + it('should include files only once', () => { + params.zipFileName = getTestArtifactFileName('include-outside-working-dir'); + serverless.config.servicePath = path.join(serverless.config.servicePath, 'lib'); + params.exclude = ['./**']; + params.include = ['.././bin/**']; + + return expect(packagePlugin.zip(params)) + .to.eventually.be.equal( + path.join(serverless.config.servicePath, '.serverless', params.zipFileName) + ) + .then(artifact => { + const data = fs.readFileSync(artifact); + return expect(zip.loadAsync(data)).to.be.fulfilled; + }) + .then(unzippedData => { const unzippedFileData = unzippedData.files; expect(Object.keys(unzippedFileData).sort()).to.deep.equal([ 'bin/binary-444', @@ -1041,15 +1018,18 @@ describe('zipService', () => { params.include = []; params.zipFileName = getTestArtifactFileName('empty'); - return expect(packagePlugin.zip(params)).to.be - .rejectedWith(Error, 'file matches include / exclude'); + return expect(packagePlugin.zip(params)).to.be.rejectedWith( + Error, + 'file matches include / exclude' + ); }); }); describe('#zipFiles()', () => { it('should throw an error if no files are provided', () => - expect(packagePlugin.zipFiles([], path.resolve(__dirname, 'tmp.zip'))).to.be - .rejectedWith(Error, 'No files to package') - ); + expect(packagePlugin.zipFiles([], path.resolve(__dirname, 'tmp.zip'))).to.be.rejectedWith( + Error, + 'No files to package' + )); }); }); diff --git a/lib/plugins/package/package.js b/lib/plugins/package/package.js index 7e8f89ad7..5a2430c8b 100644 --- a/lib/plugins/package/package.js +++ b/lib/plugins/package/package.js @@ -10,15 +10,12 @@ class Package { this.serverless = serverless; this.options = options; this.servicePath = this.serverless.config.servicePath || ''; - this.packagePath = this.options.package || + this.packagePath = + this.options.package || this.serverless.service.package.path || path.join(this.servicePath || '.', '.serverless'); - Object.assign( - this, - packageService, - zipService - ); + Object.assign(this, packageService, zipService); this.commands = { package: { @@ -51,17 +48,14 @@ class Package { commands: { function: { type: 'entrypoint', - lifecycleEvents: [ - 'package', - ], + lifecycleEvents: ['package'], }, }, }, }; this.hooks = { - 'package:createDeploymentArtifacts': () => BbPromise.bind(this) - .then(this.packageService), + 'package:createDeploymentArtifacts': () => BbPromise.bind(this).then(this.packageService), 'package:function:package': () => { if (this.options.function) { diff --git a/lib/plugins/package/package.test.js b/lib/plugins/package/package.test.js index 8b2b2a0b2..175354eb5 100644 --- a/lib/plugins/package/package.test.js +++ b/lib/plugins/package/package.test.js @@ -17,12 +17,13 @@ describe('Package', () => { beforeEach(() => { serverless = new Serverless(); - serverless.init(); - options = { - stage: 'dev', - region: 'us-east-1', - }; - pkg = new Package(serverless, options); + return serverless.init().then(() => { + options = { + stage: 'dev', + region: 'us-east-1', + }; + pkg = new Package(serverless, options); + }); }); describe('#constructor()', () => { @@ -53,25 +54,26 @@ describe('Package', () => { pkg.packageFunction.restore(); }); - it('should implement the package:createDeploymentArtifacts event', - () => expect(pkg.hooks).to.have.property('package:createDeploymentArtifacts')); + it('should implement the package:createDeploymentArtifacts event', () => + expect(pkg.hooks).to.have.property('package:createDeploymentArtifacts')); - it('should implement the package:function:package event', - () => expect(pkg.hooks).to.have.property('package:function:package')); + it('should implement the package:function:package event', () => + expect(pkg.hooks).to.have.property('package:function:package')); describe('package:createDeploymentArtifacts', () => { it('should call packageService', () => - expect(pkg.hooks['package:createDeploymentArtifacts']()).to.be.fulfilled - .then(() => expect(packageServiceStub).to.be.calledOnce) - ); + expect(pkg.hooks['package:createDeploymentArtifacts']()).to.be.fulfilled.then( + () => expect(packageServiceStub).to.be.calledOnce + )); }); describe('package:function:package', () => { it('should call packageFunction', () => { pkg.options.function = 'myFunction'; - return expect(pkg.hooks['package:function:package']()).to.be.fulfilled - .then(() => expect(packageFunctionStub).to.be.calledOnce); + return expect(pkg.hooks['package:function:package']()).to.be.fulfilled.then( + () => expect(packageFunctionStub).to.be.calledOnce + ); }); it('should fail without function option', () => { @@ -79,7 +81,7 @@ describe('Package', () => { return expect(pkg.hooks['package:function:package']()) .to.be.rejectedWith('Function name must be set') - .then(() => expect(packageFunctionStub).to.be.not.called); + .then(() => expect(packageFunctionStub).to.be.not.called); }); }); }); diff --git a/lib/plugins/plugin/install/install.js b/lib/plugins/plugin/install/install.js index 91714d089..c2df8ffa7 100644 --- a/lib/plugins/plugin/install/install.js +++ b/lib/plugins/plugin/install/install.js @@ -15,19 +15,14 @@ class PluginInstall { this.serverless = serverless; this.options = options; - Object.assign( - this, - pluginUtils - ); + Object.assign(this, pluginUtils); this.commands = { plugin: { commands: { install: { usage: 'Install and add a plugin to your service', - lifecycleEvents: [ - 'install', - ], + lifecycleEvents: ['install'], options: { name: { usage: 'The plugin name', @@ -40,9 +35,10 @@ class PluginInstall { }, }; this.hooks = { - 'plugin:install:install': () => BbPromise.bind(this) - .then(this.install) - .then(this.trackPluginInstall), + 'plugin:install:install': () => + BbPromise.bind(this) + .then(this.install) + .then(this.trackPluginInstall), }; } @@ -60,20 +56,20 @@ class PluginInstall { return BbPromise.bind(this) .then(this.validate) .then(this.getPlugins) - .then((plugins) => { - const plugin = plugins.find((item) => item.name === this.options.pluginName); + .then(plugins => { + const plugin = plugins.find(item => item.name === this.options.pluginName); if (plugin) { return BbPromise.bind(this) - .then(this.pluginInstall) - .then(this.addPluginToServerlessFile) - .then(this.installPeerDependencies) - .then(() => { - const message = [ - 'Successfully installed', - ` "${this.options.pluginName}@${this.options.pluginVersion}"`, - ].join(''); - this.serverless.cli.log(message); - }); + .then(this.pluginInstall) + .then(this.addPluginToServerlessFile) + .then(this.installPeerDependencies) + .then(() => { + const message = [ + 'Successfully installed', + ` "${this.options.pluginName}@${this.options.pluginVersion}"`, + ].join(''); + this.serverless.cli.log(message); + }); } const message = `Plugin "${this.options.pluginName}" not found. Did you spell it correct?`; throw new this.serverless.classes.Error(message); @@ -84,33 +80,33 @@ class PluginInstall { const servicePath = this.serverless.config.servicePath; const packageJsonFilePath = path.join(servicePath, 'package.json'); - return fileExists(packageJsonFilePath).then(exists => { - // check if package.json is already present. Otherwise create one - if (!exists) { - this.serverless.cli - .log('Creating an empty package.json file in your service directory'); + return fileExists(packageJsonFilePath) + .then(exists => { + // check if package.json is already present. Otherwise create one + if (!exists) { + this.serverless.cli.log('Creating an empty package.json file in your service directory'); - const packageJsonFileContent = { - name: this.serverless.service.service, - description: '', - version: '0.1.0', - dependencies: {}, - devDependencies: {}, - }; - return fse.writeJsonAsync(packageJsonFilePath, packageJsonFileContent); - } - return BbPromise.resolve(); - }) - .then(() => { - // install the package through npm - const pluginFullName = `${this.options.pluginName}@${this.options.pluginVersion}`; - const message = [ - `Installing plugin "${pluginFullName}"`, - ' (this might take a few seconds...)', - ].join(''); - this.serverless.cli.log(message); - return this.npmInstall(pluginFullName); - }); + const packageJsonFileContent = { + name: this.serverless.service.service, + description: '', + version: '0.1.0', + dependencies: {}, + devDependencies: {}, + }; + return fse.writeJsonAsync(packageJsonFilePath, packageJsonFileContent); + } + return BbPromise.resolve(); + }) + .then(() => { + // install the package through npm + const pluginFullName = `${this.options.pluginName}@${this.options.pluginVersion}`; + const message = [ + `Installing plugin "${pluginFullName}"`, + ' (this might take a few seconds...)', + ].join(''); + this.serverless.cli.log(message); + return this.npmInstall(pluginFullName); + }); } addPluginToServerlessFile() { @@ -123,7 +119,7 @@ class PluginInstall { return BbPromise.resolve(); } - const checkIsArrayPluginsObject = (pluginsObject) => + const checkIsArrayPluginsObject = pluginsObject => _.isNil(pluginsObject) || _.isArray(pluginsObject); // pluginsObject type determined based on the value loaded during the serverless init. if (_.last(_.split(serverlessFilePath, '.')) === 'json') { @@ -131,8 +127,9 @@ class PluginInstall { const newServerlessFileObj = serverlessFileObj; const isArrayPluginsObject = checkIsArrayPluginsObject(newServerlessFileObj.plugins); // null modules property is not supported - let plugins = isArrayPluginsObject ? newServerlessFileObj.plugins || [] : - newServerlessFileObj.plugins.modules; + let plugins = isArrayPluginsObject + ? newServerlessFileObj.plugins || [] + : newServerlessFileObj.plugins.modules; if (_.isNil(plugins)) { throw new Error('plugins modules property must be present'); @@ -153,16 +150,23 @@ class PluginInstall { return this.serverless.yamlParser .parse(serverlessFilePath) - .then((serverlessFileObj) => - yamlAstParser.addNewArrayItem(serverlessFilePath, - checkIsArrayPluginsObject(serverlessFileObj.plugins) ? - 'plugins' : 'plugins.modules', this.options.pluginName)); + .then(serverlessFileObj => + yamlAstParser.addNewArrayItem( + serverlessFilePath, + checkIsArrayPluginsObject(serverlessFileObj.plugins) ? 'plugins' : 'plugins.modules', + this.options.pluginName + ) + ); }); } installPeerDependencies() { - const pluginPackageJsonFilePath = path.join(this.serverless.config.servicePath, - 'node_modules', this.options.pluginName, 'package.json'); + const pluginPackageJsonFilePath = path.join( + this.serverless.config.servicePath, + 'node_modules', + this.options.pluginName, + 'package.json' + ); return fse.readJsonAsync(pluginPackageJsonFilePath).then(pluginPackageJson => { if (pluginPackageJson.peerDependencies) { const pluginsArray = []; @@ -176,10 +180,9 @@ class PluginInstall { } npmInstall(name) { - return childProcess - .execAsync(`npm install --save-dev ${name}`, { - stdio: 'ignore', - }); + return childProcess.execAsync(`npm install --save-dev ${name}`, { + stdio: 'ignore', + }); } trackPluginInstall() { diff --git a/lib/plugins/plugin/install/install.test.js b/lib/plugins/plugin/install/install.test.js index d6a543dd1..d9666cc83 100644 --- a/lib/plugins/plugin/install/install.test.js +++ b/lib/plugins/plugin/install/install.test.js @@ -11,10 +11,12 @@ const fse = require('fs-extra'); const PluginInstall = require('./install'); const Serverless = require('../../../Serverless'); const CLI = require('../../../classes/CLI'); -const testUtils = require('../../../../tests/utils'); -chai.use(require('chai-as-promised')); -const expect = require('chai').expect; const _ = require('lodash'); +const userStats = require('../../../utils/userStats'); +const { expect } = require('chai'); +const { getTmpDirPath } = require('../../../../tests/utils/fs'); + +chai.use(require('chai-as-promised')); describe('PluginInstall', () => { let pluginInstall; @@ -62,12 +64,13 @@ describe('PluginInstall', () => { let installStub; beforeEach(() => { - installStub = sinon - .stub(pluginInstall, 'install').returns(BbPromise.resolve()); + installStub = sinon.stub(pluginInstall, 'install').returns(BbPromise.resolve()); + sinon.stub(userStats, 'track').resolves(); }); afterEach(() => { pluginInstall.install.restore(); + userStats.track.restore(); }); it('should have the sub-command "install"', () => { @@ -89,11 +92,10 @@ describe('PluginInstall', () => { expect(pluginInstall.hooks['plugin:install:install']).to.not.equal(undefined); }); - it('should run promise chain in order for "plugin:install:install" hook', - () => pluginInstall.hooks['plugin:install:install']().then(() => { + it('should run promise chain in order for "plugin:install:install" hook', () => + pluginInstall.hooks['plugin:install:install']().then(() => { expect(installStub.calledOnce).to.equal(true); - }) - ); + })); }); describe('#install()', () => { @@ -107,25 +109,19 @@ describe('PluginInstall', () => { let installPeerDependenciesStub; beforeEach(() => { - servicePath = testUtils.getTmpDirPath(); + servicePath = getTmpDirPath(); pluginInstall.serverless.config.servicePath = servicePath; fse.ensureDirSync(servicePath); serverlessYmlFilePath = path.join(servicePath, 'serverless.yml'); - validateStub = sinon - .stub(pluginInstall, 'validate') - .returns(BbPromise.resolve()); - pluginInstallStub = sinon - .stub(pluginInstall, 'pluginInstall') - .returns(BbPromise.resolve()); + validateStub = sinon.stub(pluginInstall, 'validate').returns(BbPromise.resolve()); + pluginInstallStub = sinon.stub(pluginInstall, 'pluginInstall').returns(BbPromise.resolve()); addPluginToServerlessFileStub = sinon .stub(pluginInstall, 'addPluginToServerlessFile') .returns(BbPromise.resolve()); installPeerDependenciesStub = sinon .stub(pluginInstall, 'installPeerDependencies') .returns(BbPromise.resolve()); - getPluginsStub = sinon - .stub(pluginInstall, 'getPlugins') - .returns(BbPromise.resolve(plugins)); + getPluginsStub = sinon.stub(pluginInstall, 'getPlugins').returns(BbPromise.resolve(plugins)); // save the cwd so that we can restore it later savedCwd = process.cwd(); process.chdir(servicePath); @@ -146,8 +142,7 @@ describe('PluginInstall', () => { service: 'plugin-service', provider: 'aws', }; - serverless.utils - .writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml)); + serverless.utils.writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml)); pluginInstall.options.name = 'serverless-plugin-1'; @@ -168,8 +163,7 @@ describe('PluginInstall', () => { service: 'plugin-service', provider: 'aws', }; - serverless.utils - .writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml)); + serverless.utils.writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml)); pluginInstall.options.name = '@scope/serverless-plugin-1'; @@ -190,8 +184,7 @@ describe('PluginInstall', () => { service: 'plugin-service', provider: 'aws', }; - serverless.utils - .writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml)); + serverless.utils.writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml)); pluginInstall.options.name = 'serverless-not-available-plugin'; return expect(pluginInstall.install()).to.be.rejected.then(() => { @@ -210,8 +203,7 @@ describe('PluginInstall', () => { service: 'plugin-service', provider: 'aws', }; - serverless.utils - .writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml)); + serverless.utils.writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml)); pluginInstall.options.name = 'serverless-plugin-1'; return expect(pluginInstall.install()).to.be.fulfilled.then(() => { expect(pluginInstall.options.pluginName).to.be.equal('serverless-plugin-1'); @@ -219,28 +211,29 @@ describe('PluginInstall', () => { }); }); - it('should apply the latest version if you can not get the ' + - 'version from name option even if scoped', () => { - const serverlessYml = { - service: 'plugin-service', - provider: 'aws', - }; - serverless.utils - .writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml)); - pluginInstall.options.name = '@scope/serverless-plugin-1'; - return expect(pluginInstall.install()).to.be.fulfilled.then(() => { - expect(pluginInstall.options.pluginName).to.be.equal('@scope/serverless-plugin-1'); - expect(pluginInstall.options.pluginVersion).to.be.equal('latest'); - }); - }); + it( + 'should apply the latest version if you can not get the ' + + 'version from name option even if scoped', + () => { + const serverlessYml = { + service: 'plugin-service', + provider: 'aws', + }; + serverless.utils.writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml)); + pluginInstall.options.name = '@scope/serverless-plugin-1'; + return expect(pluginInstall.install()).to.be.fulfilled.then(() => { + expect(pluginInstall.options.pluginName).to.be.equal('@scope/serverless-plugin-1'); + expect(pluginInstall.options.pluginVersion).to.be.equal('latest'); + }); + } + ); it('should apply the specified version if you can get the version from name option', () => { const serverlessYml = { service: 'plugin-service', provider: 'aws', }; - serverless.utils - .writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml)); + serverless.utils.writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml)); pluginInstall.options.name = 'serverless-plugin-1@1.0.0'; return expect(pluginInstall.install()).to.be.fulfilled.then(() => { expect(pluginInstall.options.pluginName).to.be.equal('serverless-plugin-1'); @@ -258,18 +251,16 @@ describe('PluginInstall', () => { beforeEach(() => { pluginInstall.options.pluginName = 'serverless-plugin-1'; pluginInstall.options.pluginVersion = 'latest'; - servicePath = testUtils.getTmpDirPath(); + servicePath = getTmpDirPath(); pluginInstall.serverless.config.servicePath = servicePath; fse.ensureDirSync(servicePath); packageJsonFilePath = path.join(servicePath, 'package.json'); - npmInstallStub = sinon.stub(childProcess, 'execAsync', () => { - const packageJson = - serverless.utils.readFileSync(packageJsonFilePath, 'utf8'); + npmInstallStub = sinon.stub(childProcess, 'execAsync').callsFake(() => { + const packageJson = serverless.utils.readFileSync(packageJsonFilePath, 'utf8'); packageJson.devDependencies = { 'serverless-plugin-1': 'latest', }; - serverless.utils - .writeFileSync(packageJsonFilePath, packageJson); + serverless.utils.writeFileSync(packageJsonFilePath, packageJson); return BbPromise.resolve(); }); @@ -292,31 +283,31 @@ describe('PluginInstall', () => { devDependencies: {}, }; - serverless.utils - .writeFileSync(packageJsonFilePath, packageJson); + serverless.utils.writeFileSync(packageJsonFilePath, packageJson); return expect(pluginInstall.pluginInstall()).to.be.fulfilled.then(() => Promise.all([ expect(consoleLogStub.called).to.equal(true), - expect(npmInstallStub.calledWithExactly( - 'npm install --save-dev serverless-plugin-1@latest', - { stdio: 'ignore' } - )).to.equal(true), + expect( + npmInstallStub.calledWithExactly('npm install --save-dev serverless-plugin-1@latest', { + stdio: 'ignore', + }) + ).to.equal(true), expect(serverlessErrorStub.calledOnce).to.equal(false), ]) ); }); - it('should generate a package.json file in the service directory if not present', - () => expect(pluginInstall.pluginInstall()).to.be.fulfilled.then(() => { + it('should generate a package.json file in the service directory if not present', () => + expect(pluginInstall.pluginInstall()).to.be.fulfilled.then(() => { expect(consoleLogStub.called).to.equal(true); - expect(npmInstallStub.calledWithExactly( - 'npm install --save-dev serverless-plugin-1@latest', - { stdio: 'ignore' } - )).to.equal(true); + expect( + npmInstallStub.calledWithExactly('npm install --save-dev serverless-plugin-1@latest', { + stdio: 'ignore', + }) + ).to.equal(true); expect(fs.existsSync(packageJsonFilePath)).to.equal(true); - }) - ); + })); }); describe('#addPluginToServerlessFile()', () => { @@ -324,7 +315,7 @@ describe('PluginInstall', () => { let serverlessYmlFilePath; beforeEach(() => { - servicePath = testUtils.getTmpDirPath(); + servicePath = getTmpDirPath(); pluginInstall.serverless.config.servicePath = servicePath; serverlessYmlFilePath = path.join(servicePath, 'serverless.yml'); }); @@ -336,16 +327,16 @@ describe('PluginInstall', () => { provider: 'aws', // no plugins array here }; - serverless.utils - .writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml)); + serverless.utils.writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml)); pluginInstall.options.pluginName = 'serverless-plugin-1'; return expect(pluginInstall.addPluginToServerlessFile()).to.be.fulfilled.then(() => { - expect(serverless.utils.readFileSync(serverlessYmlFilePath, 'utf8')) - .to.deep.equal(_.assign({}, serverlessYml, { + expect(serverless.utils.readFileSync(serverlessYmlFilePath, 'utf8')).to.deep.equal( + _.assign({}, serverlessYml, { plugins: ['serverless-plugin-1'], - })); + }) + ); }); }); @@ -356,16 +347,16 @@ describe('PluginInstall', () => { provider: 'aws', plugins: ['serverless-existing-plugin'], // one plugin was already added }; - serverless.utils - .writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml)); + serverless.utils.writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml)); pluginInstall.options.pluginName = 'serverless-plugin-1'; return expect(pluginInstall.addPluginToServerlessFile()).to.be.fulfilled.then(() => { - expect(serverless.utils.readFileSync(serverlessYmlFilePath, 'utf8')) - .to.deep.equal(_.assign({}, serverlessYml, { + expect(serverless.utils.readFileSync(serverlessYmlFilePath, 'utf8')).to.deep.equal( + _.assign({}, serverlessYml, { plugins: ['serverless-existing-plugin', 'serverless-plugin-1'], - })); + }) + ); }); }); @@ -375,12 +366,12 @@ describe('PluginInstall', () => { service: 'plugin-service', provider: 'aws', }; - serverless.utils - .writeFileSync(serverlessYamlFilePath, YAML.dump(serverlessYml)); + serverless.utils.writeFileSync(serverlessYamlFilePath, YAML.dump(serverlessYml)); pluginInstall.options.pluginName = 'serverless-plugin-1'; return expect(pluginInstall.addPluginToServerlessFile()).to.be.fulfilled.then(() => { - expect(serverless.utils.readFileSync(serverlessYamlFilePath, 'utf8')) - .to.deep.equal(_.assign({}, serverlessYml, { plugins: ['serverless-plugin-1'] })); + expect(serverless.utils.readFileSync(serverlessYamlFilePath, 'utf8')).to.deep.equal( + _.assign({}, serverlessYml, { plugins: ['serverless-plugin-1'] }) + ); }); }); @@ -390,21 +381,22 @@ describe('PluginInstall', () => { service: 'plugin-service', provider: 'aws', }; - serverless.utils - .writeFileSync(serverlessJsonFilePath, serverlessJson); + serverless.utils.writeFileSync(serverlessJsonFilePath, serverlessJson); pluginInstall.options.pluginName = 'serverless-plugin-1'; - return expect(pluginInstall.addPluginToServerlessFile()).to.be.fulfilled.then(() => { - expect(serverless.utils.readFileSync(serverlessJsonFilePath, 'utf8')) - .to.deep.equal(_.assign({}, serverlessJson, { plugins: ['serverless-plugin-1'] })); - }) + return expect(pluginInstall.addPluginToServerlessFile()) + .to.be.fulfilled.then(() => { + expect(serverless.utils.readFileSync(serverlessJsonFilePath, 'utf8')).to.deep.equal( + _.assign({}, serverlessJson, { plugins: ['serverless-plugin-1'] }) + ); + }) .then(() => { pluginInstall.options.pluginName = 'serverless-plugin-2'; return expect(pluginInstall.addPluginToServerlessFile()).to.be.fulfilled.then(() => { - expect(serverless.utils.readFileSync(serverlessJsonFilePath, 'utf8')) - .to.deep.equal(_.assign({}, serverlessJson, - { - plugins: ['serverless-plugin-1', 'serverless-plugin-2'], - })); + expect(serverless.utils.readFileSync(serverlessJsonFilePath, 'utf8')).to.deep.equal( + _.assign({}, serverlessJson, { + plugins: ['serverless-plugin-1', 'serverless-plugin-2'], + }) + ); }); }); }); @@ -416,14 +408,15 @@ describe('PluginInstall', () => { provider: 'aws', plugins: [], }; - serverless.utils - .writeFileSync(serverlessJsFilePath, `module.exports = ${JSON.stringify(serverlessJson)};`); + serverless.utils.writeFileSync( + serverlessJsFilePath, + `module.exports = ${JSON.stringify(serverlessJson)};` + ); pluginInstall.options.pluginName = 'serverless-plugin-1'; return expect(pluginInstall.addPluginToServerlessFile()).to.be.fulfilled.then(() => { // use require to load serverless.js // eslint-disable-next-line global-require - expect(require(serverlessJsFilePath).plugins) - .to.be.deep.equal([]); + expect(require(serverlessJsFilePath).plugins).to.be.deep.equal([]); }); }); @@ -438,19 +431,19 @@ describe('PluginInstall', () => { modules: [], }, }; - serverless.utils - .writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml)); + serverless.utils.writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml)); pluginInstall.options.pluginName = 'serverless-plugin-1'; return expect(pluginInstall.addPluginToServerlessFile()).to.be.fulfilled.then(() => { - expect(serverless.utils.readFileSync(serverlessYmlFilePath, 'utf8')) - .to.deep.equal(_.assign({}, serverlessYml, { + expect(serverless.utils.readFileSync(serverlessYmlFilePath, 'utf8')).to.deep.equal( + _.assign({}, serverlessYml, { plugins: { localPath: 'test', modules: [pluginInstall.options.pluginName], }, - })); + }) + ); }); }); @@ -464,28 +457,30 @@ describe('PluginInstall', () => { modules: [], }, }; - serverless.utils - .writeFileSync(serverlessJsonFilePath, serverlessJson); + serverless.utils.writeFileSync(serverlessJsonFilePath, serverlessJson); pluginInstall.options.pluginName = 'serverless-plugin-1'; - return expect(pluginInstall.addPluginToServerlessFile()).to.be.fulfilled.then(() => { - expect(serverless.utils.readFileSync(serverlessJsonFilePath, 'utf8')) - .to.deep.equal(_.assign({}, serverlessJson, { - plugins: { - localPath: 'test', - modules: [pluginInstall.options.pluginName], - }, - })); - }) + return expect(pluginInstall.addPluginToServerlessFile()) + .to.be.fulfilled.then(() => { + expect(serverless.utils.readFileSync(serverlessJsonFilePath, 'utf8')).to.deep.equal( + _.assign({}, serverlessJson, { + plugins: { + localPath: 'test', + modules: [pluginInstall.options.pluginName], + }, + }) + ); + }) .then(() => { pluginInstall.options.pluginName = 'serverless-plugin-2'; return expect(pluginInstall.addPluginToServerlessFile()).to.be.fulfilled.then(() => { - expect(serverless.utils.readFileSync(serverlessJsonFilePath, 'utf8')) - .to.deep.equal(_.assign({}, serverlessJson, { + expect(serverless.utils.readFileSync(serverlessJsonFilePath, 'utf8')).to.deep.equal( + _.assign({}, serverlessJson, { plugins: { localPath: 'test', modules: ['serverless-plugin-1', 'serverless-plugin-2'], }, - })); + }) + ); }); }); }); @@ -504,20 +499,17 @@ describe('PluginInstall', () => { beforeEach(() => { pluginName = 'some-plugin'; pluginInstall.options.pluginName = pluginName; - servicePath = testUtils.getTmpDirPath(); + servicePath = getTmpDirPath(); fse.ensureDirSync(servicePath); pluginInstall.serverless.config.servicePath = servicePath; servicePackageJsonFilePath = path.join(servicePath, 'package.json'); fse.writeJsonSync(servicePackageJsonFilePath, { devDependencies: {}, }); - pluginPath = path.join( - servicePath, 'node_modules', pluginName); + pluginPath = path.join(servicePath, 'node_modules', pluginName); fse.ensureDirSync(pluginPath); pluginPackageJsonFilePath = path.join(pluginPath, 'package.json'); - npmInstallStub = sinon - .stub(childProcess, 'execAsync') - .returns(BbPromise.resolve()); + npmInstallStub = sinon.stub(childProcess, 'execAsync').returns(BbPromise.resolve()); savedCwd = process.cwd(); process.chdir(servicePath); }); @@ -534,22 +526,23 @@ describe('PluginInstall', () => { }, }); return expect(pluginInstall.installPeerDependencies()).to.be.fulfilled.then(() => { - expect(npmInstallStub.calledWithExactly( - 'npm install --save-dev some-package@"*"', - { stdio: 'ignore' } - )).to.equal(true); + expect( + npmInstallStub.calledWithExactly('npm install --save-dev some-package@"*"', { + stdio: 'ignore', + }) + ).to.equal(true); }); }); it('should not install peerDependencies if an installed plugin does not have ones', () => { fse.writeJsonSync(pluginPackageJsonFilePath, {}); return expect(pluginInstall.installPeerDependencies()).to.be.fulfilled.then(() => { - expect(fse.readJsonSync(servicePackageJsonFilePath)) - .to.be.deep.equal({ devDependencies: {} }); - expect(npmInstallStub.calledWithExactly( - 'npm install', - { stdio: 'ignore' } - )).to.equal(false); + expect(fse.readJsonSync(servicePackageJsonFilePath)).to.be.deep.equal({ + devDependencies: {}, + }); + expect(npmInstallStub.calledWithExactly('npm install', { stdio: 'ignore' })).to.equal( + false + ); }); }); }); diff --git a/lib/plugins/plugin/lib/utils.js b/lib/plugins/plugin/lib/utils.js index 0f13b4573..f4eeffe3d 100644 --- a/lib/plugins/plugin/lib/utils.js +++ b/lib/plugins/plugin/lib/utils.js @@ -12,8 +12,9 @@ const fileExists = require('../../../utils/fs/fileExists'); module.exports = { validate() { if (!this.serverless.config.servicePath) { - throw new this.serverless.classes - .Error('This command can only be run inside a service directory'); + throw new this.serverless.classes.Error( + 'This command can only be run inside a service directory' + ); } return BbPromise.resolve(); @@ -31,7 +32,7 @@ module.exports = { yml: fileExists(ymlFilePath), yaml: fileExists(yamlFilePath), js: fileExists(jsFilePath), - }).then((exists) => { + }).then(exists => { if (exists.yml) { return ymlFilePath; } else if (exists.yaml) { @@ -42,10 +43,8 @@ module.exports = { return jsFilePath; } return BbPromise.reject( - new this.serverless.classes.Error( - 'Could not find any serverless service definition file.' - ) - ); + new this.serverless.classes.Error('Could not find any serverless service definition file.') + ); }); }, @@ -53,18 +52,21 @@ module.exports = { const endpoint = 'https://raw.githubusercontent.com/serverless/plugins/master/plugins.json'; // Use HTTPS Proxy (Optional) - const proxy = process.env.proxy - || process.env.HTTP_PROXY - || process.env.http_proxy - || process.env.HTTPS_PROXY - || process.env.https_proxy; + const proxy = + process.env.proxy || + process.env.HTTP_PROXY || + process.env.http_proxy || + process.env.HTTPS_PROXY || + process.env.https_proxy; const options = {}; if (proxy) { options.agent = new HttpsProxyAgent(url.parse(proxy)); } - return fetch(endpoint, options).then((result) => result.json()).then((json) => json); + return fetch(endpoint, options) + .then(result => result.json()) + .then(json => json); }, display(plugins) { @@ -72,7 +74,7 @@ module.exports = { if (plugins && plugins.length) { // order plugins by name const orderedPlugins = _.orderBy(plugins, ['name'], ['asc']); - orderedPlugins.forEach((plugin) => { + orderedPlugins.forEach(plugin => { message += `${chalk.yellow.underline(plugin.name)} - ${plugin.description}\n`; }); // remove last two newlines for a prettier output diff --git a/lib/plugins/plugin/lib/utils.test.js b/lib/plugins/plugin/lib/utils.test.js index 018883a2f..1a797062f 100644 --- a/lib/plugins/plugin/lib/utils.test.js +++ b/lib/plugins/plugin/lib/utils.test.js @@ -10,9 +10,10 @@ const chalk = require('chalk'); const PluginInstall = require('./../install/install'); const Serverless = require('../../../Serverless'); const CLI = require('../../../classes/CLI'); -const testUtils = require('../../../../tests/utils'); +const { expect } = require('chai'); +const { getTmpDirPath } = require('../../../../tests/utils/fs'); + chai.use(require('chai-as-promised')); -const expect = require('chai').expect; describe('PluginUtils', () => { let pluginUtils; @@ -52,10 +53,12 @@ describe('PluginUtils', () => { it('should throw an error if the the cwd is not a Serverless service', () => { pluginUtils.serverless.config.servicePath = false; - expect(() => { pluginUtils.validate(); }).to.throw(Error); + expect(() => { + pluginUtils.validate(); + }).to.throw(Error); }); - it('should resolve if the cwd is a Serverless service', (done) => { + it('should resolve if the cwd is a Serverless service', done => { pluginUtils.serverless.config.servicePath = true; pluginUtils.validate().then(() => done()); @@ -66,7 +69,7 @@ describe('PluginUtils', () => { let servicePath; beforeEach(() => { - servicePath = testUtils.getTmpDirPath(); + servicePath = getTmpDirPath(); pluginUtils.serverless.config.servicePath = servicePath; }); @@ -74,45 +77,50 @@ describe('PluginUtils', () => { const serverlessYmlFilePath = path.join(servicePath, 'serverless.yml'); fse.ensureFileSync(serverlessYmlFilePath); - return expect(pluginUtils.getServerlessFilePath()).to.be.fulfilled - .then(serverlessFilePath => { - expect(serverlessFilePath).to.equal(serverlessYmlFilePath); - }); + return expect(pluginUtils.getServerlessFilePath()).to.be.fulfilled.then( + serverlessFilePath => { + expect(serverlessFilePath).to.equal(serverlessYmlFilePath); + } + ); }); it('should return the correct serverless file path for a .yaml file', () => { const serverlessYamlFilePath = path.join(servicePath, 'serverless.yaml'); fse.ensureFileSync(serverlessYamlFilePath); - return expect(pluginUtils.getServerlessFilePath()).to.be.fulfilled - .then(serverlessFilePath => { - expect(serverlessFilePath).to.equal(serverlessYamlFilePath); - }); + return expect(pluginUtils.getServerlessFilePath()).to.be.fulfilled.then( + serverlessFilePath => { + expect(serverlessFilePath).to.equal(serverlessYamlFilePath); + } + ); }); it('should return the correct serverless file path for a .json file', () => { const serverlessJsonFilePath = path.join(servicePath, 'serverless.json'); fse.ensureFileSync(serverlessJsonFilePath); - return expect(pluginUtils.getServerlessFilePath()).to.be.fulfilled - .then(serverlessFilePath => { - expect(serverlessFilePath).to.equal(serverlessJsonFilePath); - }); + return expect(pluginUtils.getServerlessFilePath()).to.be.fulfilled.then( + serverlessFilePath => { + expect(serverlessFilePath).to.equal(serverlessJsonFilePath); + } + ); }); it('should return the correct serverless file path for a .js file', () => { const serverlessJsFilePath = path.join(servicePath, 'serverless.js'); fse.ensureFileSync(serverlessJsFilePath); - return expect(pluginUtils.getServerlessFilePath()).to.be.fulfilled - .then(serverlessFilePath => { - expect(serverlessFilePath).to.equal(serverlessJsFilePath); - }); + return expect(pluginUtils.getServerlessFilePath()).to.be.fulfilled.then( + serverlessFilePath => { + expect(serverlessFilePath).to.equal(serverlessJsFilePath); + } + ); }); it('should reject if no configuration file exists', () => - expect(pluginUtils.getServerlessFilePath()) - .to.be.rejectedWith('Could not find any serverless service definition file.')); + expect(pluginUtils.getServerlessFilePath()).to.be.rejectedWith( + 'Could not find any serverless service definition file.' + )); }); describe('#getPlugins()', () => { @@ -133,7 +141,7 @@ describe('PluginUtils', () => { it('should fetch and return the plugins from the plugins repository', () => { const endpoint = 'https://raw.githubusercontent.com/serverless/plugins/master/plugins.json'; - return pluginWithFetchStub.getPlugins().then((result) => { + return pluginWithFetchStub.getPlugins().then(result => { expect(fetchStub.calledOnce).to.equal(true); expect(fetchStub.args[0][0]).to.equal(endpoint); expect(result).to.deep.equal(plugins); @@ -151,7 +159,7 @@ describe('PluginUtils', () => { expectedMessage += `${chalk.yellow.underline('serverless-plugin-2')}`; expectedMessage += ' - Serverless Plugin 2\n'; expectedMessage = expectedMessage.slice(0, -2); - return expect(pluginUtils.display(plugins)).to.be.fulfilled.then((message) => { + return expect(pluginUtils.display(plugins)).to.be.fulfilled.then(message => { expect(consoleLogStub.calledTwice).to.equal(true); expect(message).to.equal(expectedMessage); }); @@ -160,7 +168,7 @@ describe('PluginUtils', () => { it('should print a message when no plugins are available to display', () => { const expectedMessage = 'There are no plugins available to display'; - return pluginUtils.display([]).then((message) => { + return pluginUtils.display([]).then(message => { expect(consoleLogStub.calledOnce).to.equal(true); expect(message).to.equal(expectedMessage); }); diff --git a/lib/plugins/plugin/list/list.js b/lib/plugins/plugin/list/list.js index 458f2300a..250324174 100644 --- a/lib/plugins/plugin/list/list.js +++ b/lib/plugins/plugin/list/list.js @@ -9,35 +9,31 @@ class PluginList { this.serverless = serverless; this.options = options; - Object.assign( - this, - pluginUtils - ); + Object.assign(this, pluginUtils); this.commands = { plugin: { commands: { list: { usage: 'Lists all available plugins', - lifecycleEvents: [ - 'list', - ], + lifecycleEvents: ['list'], }, }, }, }; this.hooks = { - 'plugin:list:list': () => BbPromise.bind(this) - .then(this.list) - .then(this.trackPluginList), + 'plugin:list:list': () => + BbPromise.bind(this) + .then(this.list) + .then(this.trackPluginList), }; } list() { return BbPromise.bind(this) .then(this.getPlugins) - .then((plugins) => this.display(plugins)); + .then(plugins => this.display(plugins)); } trackPluginList() { diff --git a/lib/plugins/plugin/list/list.test.js b/lib/plugins/plugin/list/list.test.js index 0585127ca..49091d355 100644 --- a/lib/plugins/plugin/list/list.test.js +++ b/lib/plugins/plugin/list/list.test.js @@ -6,6 +6,7 @@ const BbPromise = require('bluebird'); const PluginList = require('./list'); const Serverless = require('../../../Serverless'); const CLI = require('../../../classes/CLI'); +const userStats = require('../../../utils/userStats'); chai.use(require('chai-as-promised')); const expect = require('chai').expect; @@ -24,12 +25,13 @@ describe('PluginList', () => { let listStub; beforeEach(() => { - listStub = sinon - .stub(pluginList, 'list').returns(BbPromise.resolve()); + listStub = sinon.stub(pluginList, 'list').returns(BbPromise.resolve()); + sinon.stub(userStats, 'track').resolves(); }); afterEach(() => { pluginList.list.restore(); + userStats.track.restore(); }); it('should have the sub-command "list"', () => { @@ -37,9 +39,7 @@ describe('PluginList', () => { }); it('should have the lifecycle event "list" for the "list" sub-command', () => { - expect(pluginList.commands.plugin.commands.list.lifecycleEvents).to.deep.equal([ - 'list', - ]); + expect(pluginList.commands.plugin.commands.list.lifecycleEvents).to.deep.equal(['list']); }); it('should have no option for the "list" sub-command', () => { @@ -51,11 +51,10 @@ describe('PluginList', () => { expect(pluginList.hooks['plugin:list:list']).to.not.equal(undefined); }); - it('should run promise chain in order for "plugin:list:list" hook', - () => expect(pluginList.hooks['plugin:list:list']()).to.be.fulfilled.then(() => { + it('should run promise chain in order for "plugin:list:list" hook', () => + expect(pluginList.hooks['plugin:list:list']()).to.be.fulfilled.then(() => { expect(listStub.calledOnce).to.equal(true); - }) - ); + })); }); describe('#list()', () => { @@ -63,10 +62,8 @@ describe('PluginList', () => { let displayStub; beforeEach(() => { - getPluginsStub = sinon - .stub(pluginList, 'getPlugins').returns(BbPromise.resolve()); - displayStub = sinon - .stub(pluginList, 'display').returns(BbPromise.resolve()); + getPluginsStub = sinon.stub(pluginList, 'getPlugins').returns(BbPromise.resolve()); + displayStub = sinon.stub(pluginList, 'display').returns(BbPromise.resolve()); }); afterEach(() => { @@ -78,7 +75,6 @@ describe('PluginList', () => { pluginList.list().then(() => { expect(getPluginsStub.calledOnce).to.equal(true); expect(displayStub.calledOnce).to.equal(true); - }) - ); + })); }); }); diff --git a/lib/plugins/plugin/plugin.js b/lib/plugins/plugin/plugin.js index f6abc0461..56880843c 100644 --- a/lib/plugins/plugin/plugin.js +++ b/lib/plugins/plugin/plugin.js @@ -10,9 +10,7 @@ class Plugin { this.commands = { plugin: { usage: 'Plugin management for Serverless', - lifecycleEvents: [ - 'plugin', - ], + lifecycleEvents: ['plugin'], }, }; this.hooks = { diff --git a/lib/plugins/plugin/plugin.test.js b/lib/plugins/plugin/plugin.test.js index d99f59078..6afa304b8 100644 --- a/lib/plugins/plugin/plugin.test.js +++ b/lib/plugins/plugin/plugin.test.js @@ -25,7 +25,8 @@ describe('Plugin', () => { beforeEach(() => { generateCommandsHelpStub = sinon - .stub(plugin.serverless.cli, 'generateCommandsHelp').returns(BbPromise.resolve()); + .stub(plugin.serverless.cli, 'generateCommandsHelp') + .returns(BbPromise.resolve()); }); afterEach(() => { @@ -37,9 +38,7 @@ describe('Plugin', () => { }); it('should have the lifecycle event "plugin" for the "plugin" command', () => { - expect(plugin.commands.plugin.lifecycleEvents).to.deep.equal([ - 'plugin', - ]); + expect(plugin.commands.plugin.lifecycleEvents).to.deep.equal(['plugin']); }); it('should have no option for the "plugin" command', () => { @@ -51,10 +50,9 @@ describe('Plugin', () => { expect(plugin.hooks['plugin:plugin']).to.not.equal(undefined); }); - it('should run promise chain in order for "plugin:plugin" hook', - () => expect(plugin.hooks['plugin:plugin']()).to.be.fulfilled.then(() => { + it('should run promise chain in order for "plugin:plugin" hook', () => + expect(plugin.hooks['plugin:plugin']()).to.be.fulfilled.then(() => { expect(generateCommandsHelpStub.calledOnce).to.equal(true); - }) - ); + })); }); }); diff --git a/lib/plugins/plugin/search/search.js b/lib/plugins/plugin/search/search.js index f46fd364e..06a58dc26 100644 --- a/lib/plugins/plugin/search/search.js +++ b/lib/plugins/plugin/search/search.js @@ -10,19 +10,14 @@ class PluginSearch { this.serverless = serverless; this.options = options; - Object.assign( - this, - pluginUtils - ); + Object.assign(this, pluginUtils); this.commands = { plugin: { commands: { search: { usage: 'Search for plugins', - lifecycleEvents: [ - 'search', - ], + lifecycleEvents: ['search'], options: { query: { usage: 'Search query', @@ -36,21 +31,22 @@ class PluginSearch { }; this.hooks = { - 'plugin:search:search': () => BbPromise.bind(this) - .then(this.search) - .then(this.trackPluginSearch), + 'plugin:search:search': () => + BbPromise.bind(this) + .then(this.search) + .then(this.trackPluginSearch), }; } search() { return BbPromise.bind(this) .then(this.getPlugins) - .then((plugins) => { + .then(plugins => { // filter out plugins which match the query const regex = new RegExp(this.options.query); - const filteredPlugins = plugins.filter((plugin) => - (plugin.name.match(regex) || plugin.description.match(regex)) + const filteredPlugins = plugins.filter( + plugin => plugin.name.match(regex) || plugin.description.match(regex) ); // print a message with the search result @@ -61,7 +57,7 @@ class PluginSearch { return filteredPlugins; }) - .then((plugins) => { + .then(plugins => { this.display(plugins); }); } diff --git a/lib/plugins/plugin/search/search.test.js b/lib/plugins/plugin/search/search.test.js index bdb81dcc6..f01ce0773 100644 --- a/lib/plugins/plugin/search/search.test.js +++ b/lib/plugins/plugin/search/search.test.js @@ -6,6 +6,7 @@ const BbPromise = require('bluebird'); const PluginSearch = require('./search'); const Serverless = require('../../../Serverless'); const CLI = require('../../../classes/CLI'); +const userStats = require('../../../utils/userStats'); chai.use(require('chai-as-promised')); const expect = require('chai').expect; @@ -48,12 +49,13 @@ describe('PluginSearch', () => { let searchStub; beforeEach(() => { - searchStub = sinon - .stub(pluginSearch, 'search').returns(BbPromise.resolve()); + searchStub = sinon.stub(pluginSearch, 'search').returns(BbPromise.resolve()); + sinon.stub(userStats, 'track').resolves(); }); afterEach(() => { pluginSearch.search.restore(); + userStats.track.restore(); }); it('should have the sub-command "search"', () => { @@ -75,11 +77,10 @@ describe('PluginSearch', () => { expect(pluginSearch.hooks['plugin:search:search']).to.not.equal(undefined); }); - it('should run promise chain in order for "plugin:search:search" hook', - () => expect(pluginSearch.hooks['plugin:search:search']()).to.be.fulfilled.then(() => { + it('should run promise chain in order for "plugin:search:search" hook', () => + expect(pluginSearch.hooks['plugin:search:search']()).to.be.fulfilled.then(() => { expect(searchStub.calledOnce).to.equal(true); - }) - ); + })); }); describe('#search()', () => { diff --git a/lib/plugins/plugin/uninstall/uninstall.js b/lib/plugins/plugin/uninstall/uninstall.js index 4d241d2e9..5d539c66a 100644 --- a/lib/plugins/plugin/uninstall/uninstall.js +++ b/lib/plugins/plugin/uninstall/uninstall.js @@ -14,19 +14,14 @@ class PluginUninstall { this.serverless = serverless; this.options = options; - Object.assign( - this, - pluginUtils - ); + Object.assign(this, pluginUtils); this.commands = { plugin: { commands: { uninstall: { usage: 'Uninstall and remove a plugin from your service', - lifecycleEvents: [ - 'uninstall', - ], + lifecycleEvents: ['uninstall'], options: { name: { usage: 'The plugin name', @@ -40,9 +35,10 @@ class PluginUninstall { }; this.hooks = { - 'plugin:uninstall:uninstall': () => BbPromise.bind(this) - .then(this.uninstall) - .then(this.trackPluginUninstall), + 'plugin:uninstall:uninstall': () => + BbPromise.bind(this) + .then(this.uninstall) + .then(this.trackPluginUninstall), }; } @@ -54,16 +50,16 @@ class PluginUninstall { .then(this.validate) .then(this.getPlugins) .then(plugins => { - const plugin = plugins.find((item) => item.name === this.options.pluginName); + const plugin = plugins.find(item => item.name === this.options.pluginName); if (plugin) { return BbPromise.bind(this) - .then(this.uninstallPeerDependencies) - .then(this.pluginUninstall) - .then(this.removePluginFromServerlessFile) - .then(() => { - this.serverless.cli.log(`Successfully uninstalled "${this.options.pluginName}"`); - return BbPromise.resolve(); - }); + .then(this.uninstallPeerDependencies) + .then(this.pluginUninstall) + .then(this.removePluginFromServerlessFile) + .then(() => { + this.serverless.cli.log(`Successfully uninstalled "${this.options.pluginName}"`); + return BbPromise.resolve(); + }); } const message = `Plugin "${this.options.pluginName}" not found. Did you spell it correct?`; throw new this.serverless.classes.Error(message); @@ -71,8 +67,9 @@ class PluginUninstall { } pluginUninstall() { - this.serverless.cli - .log(`Uninstalling plugin "${this.options.pluginName}" (this might take a few seconds...)`); + this.serverless.cli.log( + `Uninstalling plugin "${this.options.pluginName}" (this might take a few seconds...)` + ); return this.npmUninstall(this.options.pluginName); } @@ -89,9 +86,9 @@ class PluginUninstall { if (_.last(_.split(serverlessFilePath, '.')) === 'json') { return fse.readJsonAsync(serverlessFilePath).then(serverlessFileObj => { const isArrayPluginsObject = _.isArray(serverlessFileObj.plugins); - const plugins = isArrayPluginsObject ? - serverlessFileObj.plugins : - serverlessFileObj.plugins && serverlessFileObj.plugins.modules; + const plugins = isArrayPluginsObject + ? serverlessFileObj.plugins + : serverlessFileObj.plugins && serverlessFileObj.plugins.modules; if (plugins) { _.pull(plugins, this.options.pluginName); @@ -109,34 +106,43 @@ class PluginUninstall { } return this.serverless.yamlParser - .parse(serverlessFilePath) - .then((serverlessFileObj) => - yamlAstParser.removeExistingArrayItem( - serverlessFilePath, _.isArray(serverlessFileObj.plugins) ? - 'plugins' : 'plugins.modules', this.options.pluginName)); + .parse(serverlessFilePath) + .then(serverlessFileObj => + yamlAstParser.removeExistingArrayItem( + serverlessFilePath, + _.isArray(serverlessFileObj.plugins) ? 'plugins' : 'plugins.modules', + this.options.pluginName + ) + ); }); } uninstallPeerDependencies() { - const pluginPackageJsonFilePath = path.join(this.serverless.config.servicePath, - 'node_modules', this.options.pluginName, 'package.json'); - return fse.readJsonAsync(pluginPackageJsonFilePath).then(pluginPackageJson => { - if (pluginPackageJson.peerDependencies) { - const pluginsArray = []; - _.forEach(pluginPackageJson.peerDependencies, (v, k) => { - pluginsArray.push(k); - }); - return BbPromise.map(pluginsArray, this.npmUninstall); - } - return BbPromise.resolve(); - }).catch(() => BbPromise.resolve()); + const pluginPackageJsonFilePath = path.join( + this.serverless.config.servicePath, + 'node_modules', + this.options.pluginName, + 'package.json' + ); + return fse + .readJsonAsync(pluginPackageJsonFilePath) + .then(pluginPackageJson => { + if (pluginPackageJson.peerDependencies) { + const pluginsArray = []; + _.forEach(pluginPackageJson.peerDependencies, (v, k) => { + pluginsArray.push(k); + }); + return BbPromise.map(pluginsArray, this.npmUninstall); + } + return BbPromise.resolve(); + }) + .catch(() => BbPromise.resolve()); } npmUninstall(name) { - return childProcess - .execAsync(`npm uninstall --save-dev ${name}`, { - stdio: 'ignore', - }); + return childProcess.execAsync(`npm uninstall --save-dev ${name}`, { + stdio: 'ignore', + }); } trackPluginUninstall() { diff --git a/lib/plugins/plugin/uninstall/uninstall.test.js b/lib/plugins/plugin/uninstall/uninstall.test.js index 8f75cd91c..2c84c7c9b 100644 --- a/lib/plugins/plugin/uninstall/uninstall.test.js +++ b/lib/plugins/plugin/uninstall/uninstall.test.js @@ -10,9 +10,11 @@ const fse = require('fs-extra'); const PluginUninstall = require('./uninstall'); const Serverless = require('../../../Serverless'); const CLI = require('../../../classes/CLI'); -const testUtils = require('../../../../tests/utils'); +const userStats = require('../../../utils/userStats'); +const { expect } = require('chai'); +const { getTmpDirPath } = require('../../../../tests/utils/fs'); + chai.use(require('chai-as-promised')); -const expect = require('chai').expect; describe('PluginUninstall', () => { let pluginUninstall; @@ -56,12 +58,13 @@ describe('PluginUninstall', () => { let uninstallStub; beforeEach(() => { - uninstallStub = sinon - .stub(pluginUninstall, 'uninstall').returns(BbPromise.resolve()); + uninstallStub = sinon.stub(pluginUninstall, 'uninstall').returns(BbPromise.resolve()); + sinon.stub(userStats, 'track').resolves(); }); afterEach(() => { pluginUninstall.uninstall.restore(); + userStats.track.restore(); }); it('should have the sub-command "uninstall"', () => { @@ -83,12 +86,10 @@ describe('PluginUninstall', () => { expect(pluginUninstall.hooks['plugin:uninstall:uninstall']).to.not.equal(undefined); }); - it('should run promise chain in order for "plugin:uninstall:uninstall" hook', - () => expect(pluginUninstall.hooks['plugin:uninstall:uninstall']()) - .to.be.fulfilled.then(() => { - expect(uninstallStub.calledOnce).to.equal(true); - }) - ); + it('should run promise chain in order for "plugin:uninstall:uninstall" hook', () => + expect(pluginUninstall.hooks['plugin:uninstall:uninstall']()).to.be.fulfilled.then(() => { + expect(uninstallStub.calledOnce).to.equal(true); + })); }); describe('#uninstall()', () => { @@ -102,13 +103,11 @@ describe('PluginUninstall', () => { let uninstallPeerDependenciesStub; beforeEach(() => { - servicePath = testUtils.getTmpDirPath(); + servicePath = getTmpDirPath(); pluginUninstall.serverless.config.servicePath = servicePath; fse.ensureDirSync(servicePath); serverlessYmlFilePath = path.join(servicePath, 'serverless.yml'); - validateStub = sinon - .stub(pluginUninstall, 'validate') - .returns(BbPromise.resolve()); + validateStub = sinon.stub(pluginUninstall, 'validate').returns(BbPromise.resolve()); pluginUninstallStub = sinon .stub(pluginUninstall, 'pluginUninstall') .returns(BbPromise.resolve()); @@ -141,8 +140,7 @@ describe('PluginUninstall', () => { service: 'plugin-service', provider: 'aws', }; - serverless.utils - .writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml)); + serverless.utils.writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml)); pluginUninstall.options.name = 'serverless-plugin-1'; @@ -163,8 +161,7 @@ describe('PluginUninstall', () => { service: 'plugin-service', provider: 'aws', }; - serverless.utils - .writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml)); + serverless.utils.writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml)); pluginUninstall.options.name = 'serverless-not-available-plugin'; return expect(pluginUninstall.uninstall()).to.be.rejected.then(() => { @@ -183,8 +180,7 @@ describe('PluginUninstall', () => { service: 'plugin-service', provider: 'aws', }; - serverless.utils - .writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml)); + serverless.utils.writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml)); pluginUninstall.options.name = 'serverless-plugin-1@1.0'; return expect(pluginUninstall.uninstall()).to.be.fulfilled.then(() => { expect(pluginUninstall.options.pluginName).to.be.equal('serverless-plugin-1'); @@ -200,12 +196,11 @@ describe('PluginUninstall', () => { beforeEach(() => { pluginUninstall.options.pluginName = 'serverless-plugin-1'; - servicePath = testUtils.getTmpDirPath(); + servicePath = getTmpDirPath(); pluginUninstall.serverless.config.servicePath = servicePath; fse.ensureDirSync(servicePath); packageJsonFilePath = path.join(servicePath, 'package.json'); - npmUninstallStub = sinon.stub(childProcess, 'execAsync') - .returns(BbPromise.resolve()); + npmUninstallStub = sinon.stub(childProcess, 'execAsync').returns(BbPromise.resolve()); // save the cwd so that we can restore it later savedCwd = process.cwd(); process.chdir(servicePath); @@ -227,15 +222,15 @@ describe('PluginUninstall', () => { }, }; - serverless.utils - .writeFileSync(packageJsonFilePath, packageJson); + serverless.utils.writeFileSync(packageJsonFilePath, packageJson); return expect(pluginUninstall.pluginUninstall()).to.be.fulfilled.then(() => { expect(consoleLogStub.called).to.equal(true); - expect(npmUninstallStub.calledWithExactly( - 'npm uninstall --save-dev serverless-plugin-1', - { stdio: 'ignore' } - )).to.equal(true); + expect( + npmUninstallStub.calledWithExactly('npm uninstall --save-dev serverless-plugin-1', { + stdio: 'ignore', + }) + ).to.equal(true); expect(serverlessErrorStub.calledOnce).to.equal(false); }); }); @@ -246,7 +241,7 @@ describe('PluginUninstall', () => { let serverlessYmlFilePath; beforeEach(() => { - servicePath = testUtils.getTmpDirPath(); + servicePath = getTmpDirPath(); pluginUninstall.serverless.config.servicePath = servicePath; serverlessYmlFilePath = path.join(servicePath, 'serverless.yml'); }); @@ -256,19 +251,16 @@ describe('PluginUninstall', () => { const serverlessYml = { service: 'plugin-service', provider: 'aws', - plugins: [ - 'serverless-existing-plugin', - 'serverless-plugin-1', - ], + plugins: ['serverless-existing-plugin', 'serverless-plugin-1'], }; - serverless.utils - .writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml)); + serverless.utils.writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml)); pluginUninstall.options.pluginName = 'serverless-plugin-1'; return expect(pluginUninstall.removePluginFromServerlessFile()).to.be.fulfilled.then(() => { - expect(serverless.utils.readFileSync(serverlessYmlFilePath, 'utf8').plugins) - .to.deep.equal(['serverless-existing-plugin']); + expect(serverless.utils.readFileSync(serverlessYmlFilePath, 'utf8').plugins).to.deep.equal([ + 'serverless-existing-plugin', + ]); }); }); @@ -277,18 +269,16 @@ describe('PluginUninstall', () => { const serverlessYml = { service: 'plugin-service', provider: 'aws', - plugins: [ - 'serverless-plugin-1', - ], + plugins: ['serverless-plugin-1'], }; - serverless.utils - .writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml)); + serverless.utils.writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml)); pluginUninstall.options.pluginName = 'serverless-plugin-1'; return expect(pluginUninstall.removePluginFromServerlessFile()).to.be.fulfilled.then(() => { - expect(serverless.utils.readFileSync(serverlessYmlFilePath, 'utf8')) - .to.not.have.property('plugins'); + expect(serverless.utils.readFileSync(serverlessYmlFilePath, 'utf8')).to.not.have.property( + 'plugins' + ); }); }); @@ -297,16 +287,14 @@ describe('PluginUninstall', () => { const serverlessYml = { service: 'plugin-service', provider: 'aws', - plugins: [ - 'serverless-plugin-1', - ], + plugins: ['serverless-plugin-1'], }; - serverless.utils - .writeFileSync(serverlessYamlFilePath, YAML.dump(serverlessYml)); + serverless.utils.writeFileSync(serverlessYamlFilePath, YAML.dump(serverlessYml)); pluginUninstall.options.pluginName = 'serverless-plugin-1'; return expect(pluginUninstall.removePluginFromServerlessFile()).to.be.fulfilled.then(() => { - expect(serverless.utils.readFileSync(serverlessYamlFilePath, 'utf8')) - .to.not.have.property('plugins'); + expect(serverless.utils.readFileSync(serverlessYamlFilePath, 'utf8')).to.not.have.property( + 'plugins' + ); }); }); @@ -315,29 +303,31 @@ describe('PluginUninstall', () => { const serverlessJson = { service: 'plugin-service', provider: 'aws', - plugins: [ - 'serverless-plugin-1', - 'serverless-plugin-2', - ], + plugins: ['serverless-plugin-1', 'serverless-plugin-2'], }; - serverless.utils - .writeFileSync(serverlessJsonFilePath, serverlessJson); + serverless.utils.writeFileSync(serverlessJsonFilePath, serverlessJson); pluginUninstall.options.pluginName = 'serverless-plugin-1'; - return expect(pluginUninstall.removePluginFromServerlessFile()).to.be.fulfilled.then(() => { - expect(serverless.utils.readFileSync(serverlessJsonFilePath, 'utf8').plugins) - .to.deep.equal(['serverless-plugin-2']); - pluginUninstall.options.pluginName = 'serverless-plugin-2'; - }) - .then(() => expect(pluginUninstall.removePluginFromServerlessFile()).to.be.fulfilled.then( - () => { - expect(serverless.utils.readFileSync(serverlessJsonFilePath, 'utf8')) - .to.not.have.property('plugins'); - })) - .then(() => expect(pluginUninstall.removePluginFromServerlessFile()).to.be.fulfilled.then( - () => { - expect(serverless.utils.readFileSync(serverlessJsonFilePath, 'utf8')) - .to.not.have.property('plugins'); - })); + return expect(pluginUninstall.removePluginFromServerlessFile()) + .to.be.fulfilled.then(() => { + expect( + serverless.utils.readFileSync(serverlessJsonFilePath, 'utf8').plugins + ).to.deep.equal(['serverless-plugin-2']); + pluginUninstall.options.pluginName = 'serverless-plugin-2'; + }) + .then(() => + expect(pluginUninstall.removePluginFromServerlessFile()).to.be.fulfilled.then(() => { + expect( + serverless.utils.readFileSync(serverlessJsonFilePath, 'utf8') + ).to.not.have.property('plugins'); + }) + ) + .then(() => + expect(pluginUninstall.removePluginFromServerlessFile()).to.be.fulfilled.then(() => { + expect( + serverless.utils.readFileSync(serverlessJsonFilePath, 'utf8') + ).to.not.have.property('plugins'); + }) + ); }); it('should not modify serverless .js file', () => { @@ -346,10 +336,7 @@ describe('PluginUninstall', () => { const serverlessJson = { service: 'plugin-service', provider: 'aws', - plugins: [ - 'serverless-plugin-1', - 'serverless-plugin-2', - ], + plugins: ['serverless-plugin-1', 'serverless-plugin-2'], }; serverless.utils.writeFileSync( serverlessJsFilePath, @@ -359,8 +346,7 @@ describe('PluginUninstall', () => { return expect(pluginUninstall.removePluginFromServerlessFile()).to.be.fulfilled.then(() => { // use require to load serverless.js // eslint-disable-next-line global-require - expect(require(serverlessJsFilePath).plugins) - .to.be.deep.equal(serverlessJson.plugins); + expect(require(serverlessJsFilePath).plugins).to.be.deep.equal(serverlessJson.plugins); }); }); }); @@ -373,25 +359,21 @@ describe('PluginUninstall', () => { provider: 'aws', plugins: { localPath: 'test', - modules: [ - 'serverless-existing-plugin', - 'serverless-plugin-1', - ], + modules: ['serverless-existing-plugin', 'serverless-plugin-1'], }, }; - serverless.utils - .writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml)); + serverless.utils.writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml)); pluginUninstall.options.pluginName = 'serverless-plugin-1'; - return expect(pluginUninstall.removePluginFromServerlessFile()) - .to.be.fulfilled.then(() => { - expect(serverless.utils.readFileSync(serverlessYmlFilePath, 'utf8').plugins) - .to.deep.equal({ - localPath: 'test', - modules: ['serverless-existing-plugin'], - }); + return expect(pluginUninstall.removePluginFromServerlessFile()).to.be.fulfilled.then(() => { + expect( + serverless.utils.readFileSync(serverlessYmlFilePath, 'utf8').plugins + ).to.deep.equal({ + localPath: 'test', + modules: ['serverless-existing-plugin'], }); + }); }); it('should remove the plugin from the service if it is the only one', () => { @@ -401,21 +383,19 @@ describe('PluginUninstall', () => { provider: 'aws', plugins: { localPath: 'test', - modules: [ - 'serverless-plugin-1', - ], + modules: ['serverless-plugin-1'], }, }; - serverless.utils - .writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml)); + serverless.utils.writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml)); pluginUninstall.options.pluginName = 'serverless-plugin-1'; return expect(pluginUninstall.removePluginFromServerlessFile()).to.be.fulfilled.then(() => { - expect(serverless.utils.readFileSync(serverlessYmlFilePath, 'utf8').plugins) - .to.deep.equal({ - localPath: 'test', - }); + expect( + serverless.utils.readFileSync(serverlessYmlFilePath, 'utf8').plugins + ).to.deep.equal({ + localPath: 'test', + }); }); }); @@ -426,39 +406,39 @@ describe('PluginUninstall', () => { provider: 'aws', plugins: { localPath: 'test', - modules: [ - 'serverless-plugin-1', - 'serverless-plugin-2', - ], + modules: ['serverless-plugin-1', 'serverless-plugin-2'], }, }; - serverless.utils - .writeFileSync(serverlessJsonFilePath, serverlessJson); + serverless.utils.writeFileSync(serverlessJsonFilePath, serverlessJson); pluginUninstall.options.pluginName = 'serverless-plugin-1'; - return expect(pluginUninstall.removePluginFromServerlessFile()).to.be.fulfilled.then(() => { - expect(serverless.utils.readFileSync(serverlessJsonFilePath, 'utf8').plugins) - .to.deep.equal({ + return expect(pluginUninstall.removePluginFromServerlessFile()) + .to.be.fulfilled.then(() => { + expect( + serverless.utils.readFileSync(serverlessJsonFilePath, 'utf8').plugins + ).to.deep.equal({ localPath: 'test', - modules: [ - 'serverless-plugin-2', - ], + modules: ['serverless-plugin-2'], }); - pluginUninstall.options.pluginName = 'serverless-plugin-2'; - }) - .then(() => expect(pluginUninstall.removePluginFromServerlessFile()).to.be.fulfilled.then( - () => { - expect(serverless.utils.readFileSync(serverlessJsonFilePath, 'utf8').plugins) - .to.deep.equal({ - localPath: 'test', - }); - })) - .then(() => expect(pluginUninstall.removePluginFromServerlessFile()).to.be.fulfilled.then( - () => { - expect(serverless.utils.readFileSync(serverlessJsonFilePath, 'utf8').plugins) - .to.deep.equal({ - localPath: 'test', - }); - })); + pluginUninstall.options.pluginName = 'serverless-plugin-2'; + }) + .then(() => + expect(pluginUninstall.removePluginFromServerlessFile()).to.be.fulfilled.then(() => { + expect( + serverless.utils.readFileSync(serverlessJsonFilePath, 'utf8').plugins + ).to.deep.equal({ + localPath: 'test', + }); + }) + ) + .then(() => + expect(pluginUninstall.removePluginFromServerlessFile()).to.be.fulfilled.then(() => { + expect( + serverless.utils.readFileSync(serverlessJsonFilePath, 'utf8').plugins + ).to.deep.equal({ + localPath: 'test', + }); + }) + ); }); }); }); @@ -474,15 +454,12 @@ describe('PluginUninstall', () => { beforeEach(() => { pluginName = 'some-plugin'; pluginUninstall.options.pluginName = pluginName; - servicePath = testUtils.getTmpDirPath(); + servicePath = getTmpDirPath(); pluginUninstall.serverless.config.servicePath = servicePath; - pluginPath = path.join( - servicePath, 'node_modules', pluginName); + pluginPath = path.join(servicePath, 'node_modules', pluginName); fse.ensureDirSync(pluginPath); pluginPackageJsonFilePath = path.join(pluginPath, 'package.json'); - npmUninstallStub = sinon - .stub(childProcess, 'execAsync') - .returns(BbPromise.resolve()); + npmUninstallStub = sinon.stub(childProcess, 'execAsync').returns(BbPromise.resolve()); savedCwd = process.cwd(); process.chdir(servicePath); }); @@ -499,31 +476,32 @@ describe('PluginUninstall', () => { }, }); return expect(pluginUninstall.uninstallPeerDependencies()).to.be.fulfilled.then(() => { - expect(npmUninstallStub.calledWithExactly( - `npm uninstall --save-dev ${pluginName}`, - { stdio: 'ignore' } - )).to.equal(true); + expect( + npmUninstallStub.calledWithExactly(`npm uninstall --save-dev ${pluginName}`, { + stdio: 'ignore', + }) + ).to.equal(true); }); }); it('should not uninstall peerDependencies if an installed plugin does not have ones', () => { fse.writeJsonSync(pluginPackageJsonFilePath, {}); return expect(pluginUninstall.uninstallPeerDependencies()).to.be.fulfilled.then(() => { - expect(npmUninstallStub.calledWithExactly( - `npm uninstall --save-dev ${pluginName}`, - { stdio: 'ignore' } - )).to.equal(false); + expect( + npmUninstallStub.calledWithExactly(`npm uninstall --save-dev ${pluginName}`, { + stdio: 'ignore', + }) + ).to.equal(false); }); }); - it('should do nothing if an uninstalled plugin does not have package.json', - () => expect(pluginUninstall.uninstallPeerDependencies()).to.be.fulfilled.then( - () => { - expect(npmUninstallStub.calledWithExactly( - `npm uninstall --save-dev ${pluginName}`, - { stdio: 'ignore' } - )).to.equal(false); - }) - ); + it('should do nothing if an uninstalled plugin does not have package.json', () => + expect(pluginUninstall.uninstallPeerDependencies()).to.be.fulfilled.then(() => { + expect( + npmUninstallStub.calledWithExactly(`npm uninstall --save-dev ${pluginName}`, { + stdio: 'ignore', + }) + ).to.equal(false); + })); }); }); diff --git a/lib/plugins/print/print.js b/lib/plugins/print/print.js index aea70be26..650d52588 100644 --- a/lib/plugins/print/print.js +++ b/lib/plugins/print/print.js @@ -18,9 +18,7 @@ class Print { print: { usage: 'Print your compiled and resolved config file', configDependent: true, - lifecycleEvents: [ - 'print', - ], + lifecycleEvents: ['print'], options: { format: { usage: 'Print configuration in given format ("yaml", "json", "text"). Default: yaml', @@ -35,8 +33,7 @@ class Print { }, }; this.hooks = { - 'print:print': () => BbPromise.bind(this) - .then(this.print), + 'print:print': () => BbPromise.bind(this).then(this.print), }; } @@ -53,11 +50,14 @@ class Print { if (_.isString(this.cache.serviceProvider)) { service.provider = { name: this.cache.serviceProvider }; } - service.provider = _.merge({ - stage: 'dev', - region: 'us-east-1', - variableSyntax: '\\${([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*]+?)}', - }, service.provider); + service.provider = _.merge( + { + stage: 'dev', + region: 'us-east-1', + variableSyntax: '\\${([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*]+?)}', + }, + service.provider + ); } strip(svc) { const service = svc; @@ -67,7 +67,8 @@ class Print { } if (_.isString(this.cache.serviceProvider)) { service.provider = this.cache.serviceProvider; - } else { // is object + } else { + // is object if (!this.cache.serviceProvider.stage) { delete service.provider.stage; } @@ -96,76 +97,73 @@ class Print { // the codebase. Avoiding that, this method must read the serverless.yml file itself, adorn it // as the Service class would and then populate it, reversing the adornments thereafter in // preparation for printing the service for the user. - return getServerlessConfigFile(this.serverless.config.servicePath) - .then((svc) => { - const service = svc; - this.adorn(service); - // Need to delete variableSyntax to avoid self-matching errors - this.serverless.variables.loadVariableSyntax(); - delete service.provider.variableSyntax; // cached by adorn, restored by strip - this.serverless.variables.service = service; - return this.serverless.variables.populateObject(service) - .then((populated) => { - let conf = populated; - this.strip(conf); + return getServerlessConfigFile(this.serverless).then(svc => { + const service = svc; + this.adorn(service); + // Need to delete variableSyntax to avoid self-matching errors + this.serverless.variables.loadVariableSyntax(); + delete service.provider.variableSyntax; // cached by adorn, restored by strip + this.serverless.variables.service = service; + return this.serverless.variables.populateObject(service).then(populated => { + let conf = populated; + this.strip(conf); - // dig into the object - if (this.options.path) { - const steps = this.options.path.split('.'); - for (const step of steps) { - conf = conf[step]; + // dig into the object + if (this.options.path) { + const steps = this.options.path.split('.'); + for (const step of steps) { + conf = conf[step]; - if (conf === undefined) { - return BbPromise.reject( - new this.serverless.classes.Error(`Path "${this.options.path}" not found`) - ); - } - } - } - - // apply an optional filter - if (this.options.transform) { - if (this.options.transform === 'keys') { - conf = Object.keys(conf); - } else { - return BbPromise.reject( - new this.serverless.classes.Error('Transform can only be "keys"') - ); - } - } - - // print configuration in the specified format - const format = this.options.format || 'yaml'; - let out; - - if (format === 'text') { - if (_.isArray(conf)) { - out = conf.join(os.EOL); - } else { - if (_.isObject(conf)) { - return BbPromise.reject( - new this.serverless.classes.Error('Cannot print an object as "text"') - ); - } - - out = String(conf); - } - } else if (format === 'json') { - out = jc.stringify(conf, null, ' '); - } else if (format === 'yaml') { - out = YAML.dump(JSON.parse(jc.stringify(conf)), { noRefs: true }); - } else { + if (conf === undefined) { return BbPromise.reject( - new this.serverless.classes.Error('Format must be "yaml", "json" or "text"') + new this.serverless.classes.Error(`Path "${this.options.path}" not found`) + ); + } + } + } + + // apply an optional filter + if (this.options.transform) { + if (this.options.transform === 'keys') { + conf = Object.keys(conf); + } else { + return BbPromise.reject( + new this.serverless.classes.Error('Transform can only be "keys"') + ); + } + } + + // print configuration in the specified format + const format = this.options.format || 'yaml'; + let out; + + if (format === 'text') { + if (_.isArray(conf)) { + out = conf.join(os.EOL); + } else { + if (_.isObject(conf)) { + return BbPromise.reject( + new this.serverless.classes.Error('Cannot print an object as "text"') ); } - this.serverless.cli.consoleLog(out); - return BbPromise.resolve(); - }); - }); - } + out = String(conf); + } + } else if (format === 'json') { + out = jc.stringify(conf, null, ' '); + } else if (format === 'yaml') { + out = YAML.dump(JSON.parse(jc.stringify(conf)), { noRefs: true }); + } else { + return BbPromise.reject( + new this.serverless.classes.Error('Format must be "yaml", "json" or "text"') + ); + } + this.serverless.cli.consoleLog(out); + return BbPromise.resolve(); + }); + }); + } } module.exports = Print; diff --git a/lib/plugins/print/print.test.js b/lib/plugins/print/print.test.js index 25e01f5f9..979e16704 100644 --- a/lib/plugins/print/print.test.js +++ b/lib/plugins/print/print.test.js @@ -1,13 +1,16 @@ 'use strict'; const os = require('os'); -const expect = require('chai').expect; +const chai = require('chai'); const sinon = require('sinon'); const proxyquire = require('proxyquire'); const Serverless = require('../../Serverless'); const CLI = require('../../classes/CLI'); const YAML = require('js-yaml'); +chai.use(require('chai-as-promised')); + +const expect = chai.expect; describe('Print', () => { let print; @@ -27,6 +30,7 @@ describe('Print', () => { region: 'us-east-1', }; serverless.cli = new CLI(serverless); + serverless.processedInput = { options: {} }; print = new PrintPlugin(serverless); print.serverless.cli = { consoleLog: sinon.spy(), @@ -258,10 +262,10 @@ describe('Print', () => { provider: { name: 'aws', stage: '${{opt:stage}}', - variableSyntax: "\\${{([ ~:a-zA-Z0-9._@\\'\",\\-\\/\\(\\)*]+?)}}", + variableSyntax: '\\${{([ ~:a-zA-Z0-9._@\\\'",\\-\\/\\(\\)*]+?)}}', }, }; - serverless.service.provider.variableSyntax = "\\${{([ ~:a-zA-Z0-9._@\\'\",\\-\\/\\(\\)*]+?)}}"; + serverless.service.provider.variableSyntax = '\\${{([ ~:a-zA-Z0-9._@\\\'",\\-\\/\\(\\)*]+?)}}'; getServerlessConfigFileStub.resolves(conf); serverless.processedInput = { @@ -274,7 +278,7 @@ describe('Print', () => { provider: { name: 'aws', stage: 'dev', - variableSyntax: "\\${{([ ~:a-zA-Z0-9._@\\'\",\\-\\/\\(\\)*]+?)}}", + variableSyntax: '\\${{([ ~:a-zA-Z0-9._@\\\'",\\-\\/\\(\\)*]+?)}}', }, }; diff --git a/lib/plugins/remove/remove.js b/lib/plugins/remove/remove.js index 41b70f8ea..937e49760 100644 --- a/lib/plugins/remove/remove.js +++ b/lib/plugins/remove/remove.js @@ -11,9 +11,7 @@ class Remove { remove: { usage: 'Remove Serverless service and all resources', configDependent: true, - lifecycleEvents: [ - 'remove', - ], + lifecycleEvents: ['remove'], options: { stage: { usage: 'Stage of the service', diff --git a/lib/plugins/remove/remove.test.js b/lib/plugins/remove/remove.test.js index 89871dbe5..9881ac61d 100644 --- a/lib/plugins/remove/remove.test.js +++ b/lib/plugins/remove/remove.test.js @@ -1,10 +1,15 @@ 'use strict'; -const expect = require('chai').expect; +const chai = require('chai'); const Remove = require('./remove'); const Serverless = require('../../Serverless'); const sinon = require('sinon'); +chai.use(require('chai-as-promised')); +chai.use(require('sinon-chai')); + +const expect = chai.expect; + describe('Remove', () => { let remove; let serverless; @@ -33,8 +38,9 @@ describe('Remove', () => { remove.track.restore(); }); - it('should track the execution', () => expect(remove.hooks['after:remove:remove']()) - .to.be.fulfilled.then(() => expect(trackStub).to.be.called) - ); + it('should track the execution', () => + expect(remove.hooks['after:remove:remove']()).to.be.fulfilled.then( + () => expect(trackStub).to.be.called + )); }); }); diff --git a/lib/plugins/rollback/index.js b/lib/plugins/rollback/index.js index a16b1e54e..93c402093 100644 --- a/lib/plugins/rollback/index.js +++ b/lib/plugins/rollback/index.js @@ -11,10 +11,7 @@ class Rollback { rollback: { usage: 'Rollback the Serverless service to a specific deployment', configDependent: true, - lifecycleEvents: [ - 'initialize', - 'rollback', - ], + lifecycleEvents: ['initialize', 'rollback'], options: { timestamp: { usage: 'Timestamp of the deployment (list deployments with `serverless deploy list`)', @@ -29,9 +26,7 @@ class Rollback { commands: { function: { usage: 'Rollback the function to the previous version', - lifecycleEvents: [ - 'rollback', - ], + lifecycleEvents: ['rollback'], options: { function: { usage: 'Name of the function', diff --git a/lib/plugins/slstats/slstats.js b/lib/plugins/slstats/slstats.js index 127432905..56d6c15e2 100644 --- a/lib/plugins/slstats/slstats.js +++ b/lib/plugins/slstats/slstats.js @@ -13,9 +13,7 @@ class SlStats { slstats: { usage: 'Enable or disable stats', configDependent: true, - lifecycleEvents: [ - 'slstats', - ], + lifecycleEvents: ['slstats'], options: { enable: { usage: 'Enable stats ("--enable")', @@ -39,24 +37,26 @@ class SlStats { const disabledStats = this.options.disable && !this.options.enable; const data = { force: true }; if (enableStats) { - return userStats.track('user_enabledTracking', data) + return userStats + .track('user_enabledTracking', data) .then(() => { // set .serverlessrc config config.set('trackingDisabled', false); this.serverless.cli.log('Stats successfully enabled'); }) - .catch((error) => { + .catch(error => { const message = error; return BbPromise.reject(`Enabling / Disabling of statistics failed: ${message}`); }); } else if (disabledStats) { - return userStats.track('user_disabledTracking', data) + return userStats + .track('user_disabledTracking', data) .then(() => { // set .serverlessrc config config.set('trackingDisabled', true); this.serverless.cli.log('Stats successfully disabled'); }) - .catch((error) => { + .catch(error => { const message = error; return BbPromise.reject(`Enabling / Disabling of statistics failed: ${message}`); }); diff --git a/lib/plugins/slstats/slstats.test.js b/lib/plugins/slstats/slstats.test.js index 6bb76ce49..03314a8ab 100644 --- a/lib/plugins/slstats/slstats.test.js +++ b/lib/plugins/slstats/slstats.test.js @@ -13,8 +13,9 @@ describe('SlStats', () => { beforeEach(() => { serverless = new Serverless(); - serverless.init(); - slStats = new SlStats(serverless); + return serverless.init().then(() => { + slStats = new SlStats(serverless); + }); }); describe('#constructor()', () => { @@ -48,16 +49,12 @@ describe('SlStats', () => { return slStats.toggleStats().then(() => { expect(trackStub.calledOnce).to.equal(true); - expect(trackStub.calledWithExactly( - 'user_disabledTracking', - { force: true } - )).to.equal(true); + expect(trackStub.calledWithExactly('user_disabledTracking', { force: true })).to.equal( + true + ); expect(setStub.calledOnce).to.equal(true); - expect(setStub.calledWithExactly( - 'trackingDisabled', - true - )).to.equal(true); + expect(setStub.calledWithExactly('trackingDisabled', true)).to.equal(true); }); }); @@ -68,16 +65,10 @@ describe('SlStats', () => { return slStats.toggleStats().then(() => { expect(trackStub.calledOnce).to.equal(true); - expect(trackStub.calledWithExactly( - 'user_enabledTracking', - { force: true } - )).to.equal(true); + expect(trackStub.calledWithExactly('user_enabledTracking', { force: true })).to.equal(true); expect(setStub.calledOnce).to.equal(true); - expect(setStub.calledWithExactly( - 'trackingDisabled', - false - )).to.equal(true); + expect(setStub.calledWithExactly('trackingDisabled', false)).to.equal(true); }); }); @@ -98,7 +89,7 @@ describe('SlStats', () => { trackStub.rejects('error while tracking'); slStats.options = { enable: true }; - return slStats.toggleStats().catch((error) => { + return slStats.toggleStats().catch(error => { expect(trackStub.calledOnce).to.equal(true); expect(setStub.calledOnce).to.equal(false); expect(error).to.match(/of statistics failed/); @@ -111,7 +102,7 @@ describe('SlStats', () => { trackStub.resolves(); slStats.options = { disable: true }; - return slStats.toggleStats().catch((error) => { + return slStats.toggleStats().catch(error => { expect(trackStub.calledOnce).to.equal(true); expect(setStub.calledOnce).to.equal(true); expect(error).to.match(/of statistics failed/); diff --git a/lib/utils/autocomplete.js b/lib/utils/autocomplete.js index 84cec0b94..47c8319a6 100644 --- a/lib/utils/autocomplete.js +++ b/lib/utils/autocomplete.js @@ -12,7 +12,7 @@ const name = path.basename(process.argv[0]); const tab = require('tabtab')({ name }); -const getSuggestions = (commands) => { +const getSuggestions = commands => { tab.on(name, (data, done) => { if (data.words === 1) { done(null, Object.keys(commands)); @@ -31,8 +31,10 @@ const getSuggestions = (commands) => { }; const cacheFileValid = (serverlessConfigFile, validationHash) => { - const serverlessConfigFileHash = crypto.createHash('sha256') - .update(JSON.stringify(serverlessConfigFile)).digest('hex'); + const serverlessConfigFileHash = crypto + .createHash('sha256') + .update(JSON.stringify(serverlessConfigFile)) + .digest('hex'); if (validationHash === serverlessConfigFileHash) { return true; } @@ -41,21 +43,23 @@ const cacheFileValid = (serverlessConfigFile, validationHash) => { const autocomplete = () => { const servicePath = process.cwd(); - return getServerlessConfigFile(servicePath) - .then((serverlessConfigFile) => getCacheFile(servicePath) - .then((cacheFile) => { - if (!cacheFile || !cacheFileValid(serverlessConfigFile, cacheFile.validationHash)) { - const serverless = new Serverless(); - return serverless.init().then(() => getCacheFile(servicePath)); - } - return cacheFile; - }) - .then((cacheFile) => { - if (!cacheFile || !cacheFileValid(serverlessConfigFile, cacheFile.validationHash)) { - return; - } - return getSuggestions(cacheFile.commands); // eslint-disable-line consistent-return - })); + return getServerlessConfigFile({ processedInput: { options: {} }, config: { servicePath } }).then( + serverlessConfigFile => + getCacheFile(servicePath) + .then(cacheFile => { + if (!cacheFile || !cacheFileValid(serverlessConfigFile, cacheFile.validationHash)) { + const serverless = new Serverless(); + return serverless.init().then(() => getCacheFile(servicePath)); + } + return cacheFile; + }) + .then(cacheFile => { + if (!cacheFile || !cacheFileValid(serverlessConfigFile, cacheFile.validationHash)) { + return; + } + return getSuggestions(cacheFile.commands); // eslint-disable-line consistent-return + }) + ); }; module.exports = autocomplete; diff --git a/lib/utils/config/config.test.js b/lib/utils/config/config.test.js index 5ab1365b2..1b69c0175 100644 --- a/lib/utils/config/config.test.js +++ b/lib/utils/config/config.test.js @@ -31,17 +31,17 @@ describe('Config', () => { describe('When using config.getConfig', () => { it('should have userId key', () => { const conf = config.getConfig(); - expect(conf).to.have.deep.property('userId'); + expect(conf).to.have.nested.property('userId'); }); it('should have frameworkId key', () => { const conf = config.getConfig(); - expect(conf).to.have.deep.property('frameworkId'); + expect(conf).to.have.nested.property('frameworkId'); }); it('should have trackingDisabled key', () => { const conf = config.getConfig(); - expect(conf).to.have.deep.property('trackingDisabled'); + expect(conf).to.have.nested.property('trackingDisabled'); }); }); diff --git a/lib/utils/config/index.js b/lib/utils/config/index.js index 33753dcf9..0d256b851 100644 --- a/lib/utils/config/index.js +++ b/lib/utils/config/index.js @@ -4,6 +4,7 @@ const p = require('path'); const os = require('os'); const _ = require('lodash'); +const rc = require('rc'); const writeFileAtomic = require('write-file-atomic'); const fileExistsSync = require('../fs/fileExistsSync'); const readFileSync = require('../fs/readFileSync'); @@ -23,9 +24,10 @@ function createConfig() { userId: null, // currentUserId frameworkId: initialSetup.generateFrameworkId(), trackingDisabled: initialSetup.configureTrack(), // default false + enterpriseDisabled: false, meta: { created_at: Math.round(+new Date() / 1000), // config file creation date - updated_at: null, // config file updated date + updated_at: null, // config file updated date }, }; @@ -51,7 +53,7 @@ function getConfig() { } // then return config merged via rc module - return require('rc')(rcFileBase); // eslint-disable-line + return rc(rcFileBase, null, /* Ensure to not read options from CLI */ {}); } function getGlobalConfig() { diff --git a/lib/utils/createFromTemplate.js b/lib/utils/createFromTemplate.js new file mode 100644 index 000000000..c0976b34a --- /dev/null +++ b/lib/utils/createFromTemplate.js @@ -0,0 +1,30 @@ +'use strict'; + +const { join } = require('path'); +const BbPromise = require('bluebird'); +const { copy, exists, rename } = require('fs-extra'); + +const serverlessPath = join(__dirname, '../../'); + +module.exports = (templateName, destPath) => + new BbPromise((resolve, reject) => { + const templateSrcDir = join(serverlessPath, 'lib/plugins/create/templates', templateName); + + copy(templateSrcDir, destPath, copyError => { + if (copyError) { + reject(copyError); + return; + } + const gitignorePath = join(destPath, 'gitignore'); + exists(gitignorePath, hasGitignore => { + if (!hasGitignore) { + resolve(); + return; + } + rename(gitignorePath, join(destPath, '.gitignore'), renameError => { + if (renameError) reject(renameError); + else resolve(); + }); + }); + }); + }); diff --git a/lib/utils/createFromTemplate.test.js b/lib/utils/createFromTemplate.test.js new file mode 100644 index 000000000..d7b5e03c0 --- /dev/null +++ b/lib/utils/createFromTemplate.test.js @@ -0,0 +1,26 @@ +'use strict'; + +const path = require('path'); +const chai = require('chai'); +const { existsSync } = require('fs-extra'); +const installTemplate = require('./createFromTemplate'); +const { getTmpDirPath } = require('../../tests/utils/fs'); + +chai.use(require('chai-as-promised')); + +const expect = chai.expect; + +describe('#createFromTemplate()', () => { + let tmpDirPath; + + beforeEach(() => { + tmpDirPath = getTmpDirPath(); + return installTemplate('aws-nodejs', tmpDirPath); + }); + + it('should create from template', () => + expect(existsSync(path.join(tmpDirPath, 'serverless.yml'))).to.be.true); + + it('should handle .gitignore', () => + expect(existsSync(path.join(tmpDirPath, '.gitignore'))).to.be.true); +}); diff --git a/lib/utils/downloadTemplateFromRepo.js b/lib/utils/downloadTemplateFromRepo.js index accd32314..1bbd0424b 100644 --- a/lib/utils/downloadTemplateFromRepo.js +++ b/lib/utils/downloadTemplateFromRepo.js @@ -38,13 +38,9 @@ function getPathDirectory(length, parts) { * @param {String} repo */ function validateUrl(url, hostname, service, owner, repo) { - // validate if given url is a valid url + // validate if given url is a valid url if (url.hostname !== hostname || !owner || !repo) { - const errorMessage = `The URL must be a valid ${ - service - } URL in the following format: https://${ - hostname - }/serverless/serverless`; + const errorMessage = `The URL must be a valid ${service} URL in the following format: https://${hostname}/serverless/serverless`; throw new ServerlessError(errorMessage); } } @@ -60,11 +56,16 @@ function parseGitHubURL(url) { const owner = parts[1]; const repo = parts[2]; const branch = isSubdirectory ? parts[pathLength] : 'master'; + const isGitHubEnterprise = url.hostname !== 'github.com'; - // validate if given url is a valid GitHub url - validateUrl(url, 'github.com', 'GitHub', owner, repo); + if (!isGitHubEnterprise) { + // validate if given url is a valid GitHub url + validateUrl(url, 'github.com', 'GitHub', owner, repo); + } - const downloadUrl = `https://${url.auth ? `${url.auth}@` : ''}github.com/${owner}/${repo}/archive/${branch}.zip`; + const downloadUrl = `https://${ + isGitHubEnterprise ? url.hostname : 'github.com' + }/${owner}/${repo}/archive/${branch}.zip`; return { owner, @@ -73,6 +74,7 @@ function parseGitHubURL(url) { downloadUrl, isSubdirectory, pathToDirectory: getPathDirectory(pathLength + 1, parts), + auth: url.auth || '', }; } @@ -102,6 +104,7 @@ function parseBitbucketURL(url) { downloadUrl, isSubdirectory, pathToDirectory: getPathDirectory(pathLength + 1, parts), + auth: '', }; } @@ -130,6 +133,7 @@ function parseGitlabURL(url) { downloadUrl, isSubdirectory, pathToDirectory: getPathDirectory(pathLength + 1, parts), + auth: '', }; } @@ -152,22 +156,17 @@ function parseRepoURL(inputUrl) { 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); - } - case 'gitlab.com': { - return parseGitlabURL(url); - } - default: { - const msg = - 'The URL you passed is not one of the valid providers: "GitHub", "Bitbucket", or "GitLab".'; - throw new ServerlessError(msg); - } + if (url.hostname === 'github.com' || url.hostname.indexOf('github.') !== -1) { + return parseGitHubURL(url); + } else if (url.hostname === 'bitbucket.org') { + return parseBitbucketURL(url); + } else if (url.hostname === 'gitlab.com') { + return parseGitlabURL(url); } + + const msg = + 'The URL you passed is not one of the valid providers: "GitHub", "GitHub Entreprise", "Bitbucket", or "GitLab".'; + throw new ServerlessError(msg); } /** @@ -182,6 +181,7 @@ function downloadTemplateFromRepo(inputUrl, templateName, downloadPath) { let serviceName; let dirName; let downloadServicePath; + const { auth } = repoInformation; if (repoInformation.isSubdirectory) { const folderName = repoInformation.pathToDirectory.split('/').splice(-1)[0]; @@ -204,23 +204,28 @@ function downloadTemplateFromRepo(inputUrl, templateName, downloadPath) { log(`Downloading and installing "${serviceName}"...`); + const downloadOptions = { + timeout: 30000, + extract: true, + strip: 1, + mode: '755', + auth, + }; // 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) renameService(dirName, servicePath); + return download(repoInformation.downloadUrl, downloadServicePath, downloadOptions) + .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) renameService(dirName, servicePath); - return BbPromise.resolve(serviceName); - }); + return BbPromise.resolve(serviceName); + }); } module.exports = { diff --git a/lib/utils/downloadTemplateFromRepo.test.js b/lib/utils/downloadTemplateFromRepo.test.js index dcafe3a39..eee3f4c1a 100644 --- a/lib/utils/downloadTemplateFromRepo.test.js +++ b/lib/utils/downloadTemplateFromRepo.test.js @@ -1,19 +1,19 @@ '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 { expect } = require('chai'); +const { getTmpDirPath } = require('../../tests/utils/fs'); const writeFileSync = require('./fs/writeFileSync'); const readFileSync = require('./fs/readFileSync'); const remove = BbPromise.promisify(fse.remove); -const parseRepoURL = require('./downloadTemplateFromRepo').parseRepoURL; +const { parseRepoURL } = require('./downloadTemplateFromRepo'); describe('downloadTemplateFromRepo', () => { let downloadTemplateFromRepo; @@ -24,7 +24,7 @@ describe('downloadTemplateFromRepo', () => { let newServicePath; beforeEach(() => { - const tmpDir = testUtils.getTmpDirPath(); + const tmpDir = getTmpDirPath(); cwd = process.cwd(); fse.mkdirsSync(tmpDir); @@ -51,14 +51,16 @@ describe('downloadTemplateFromRepo', () => { }); 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); + expect(() => downloadTemplateFromRepo('http://no-git-hub-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); + expect(() => + downloadTemplateFromRepo('https://github.com/johndoe/existing-service') + ).to.throw(Error); }); it('should download the service based on the GitHub URL', () => { @@ -83,16 +85,14 @@ describe('downloadTemplateFromRepo', () => { 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'); - })); + 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((serviceName) => { + return downloadTemplateFromRepo(url, name).then(serviceName => { 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`); @@ -107,17 +107,14 @@ describe('downloadTemplateFromRepo', () => { 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'); - })); + 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((serviceName) => { + return downloadTemplateFromRepo(url, name).then(serviceName => { expect(downloadStub.calledOnce).to.equal(true); const yml = readFileSync(path.join(newServicePath, 'serverless.yml')); expect(yml.service).to.equal(name); @@ -157,6 +154,7 @@ describe('downloadTemplateFromRepo', () => { downloadUrl: 'https://github.com/serverless/serverless/archive/master.zip', isSubdirectory: false, pathToDirectory: '', + auth: '', }); }); @@ -170,6 +168,51 @@ describe('downloadTemplateFromRepo', () => { downloadUrl: 'https://github.com/serverless/serverless/archive/master.zip', isSubdirectory: true, pathToDirectory: 'assets', + auth: '', + }); + }); + + it('should parse a valid GitHub Entreprise URL', () => { + const output = parseRepoURL('https://github.mydomain.com/serverless/serverless'); + + expect(output).to.deep.eq({ + owner: 'serverless', + repo: 'serverless', + branch: 'master', + downloadUrl: 'https://github.mydomain.com/serverless/serverless/archive/master.zip', + isSubdirectory: false, + pathToDirectory: '', + auth: '', + }); + }); + + it('should parse a valid GitHub Entreprise with subdirectory', () => { + const output = parseRepoURL( + 'https://github.mydomain.com/serverless/serverless/tree/master/assets' + ); + + expect(output).to.deep.eq({ + owner: 'serverless', + repo: 'serverless', + branch: 'master', + downloadUrl: 'https://github.mydomain.com/serverless/serverless/archive/master.zip', + isSubdirectory: true, + pathToDirectory: 'assets', + auth: '', + }); + }); + + it('should parse a valid GitHub Entreprise URL with authentication', () => { + const output = parseRepoURL('https://username:password@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, + auth: 'username:password', + pathToDirectory: '', }); }); @@ -183,11 +226,14 @@ describe('downloadTemplateFromRepo', () => { downloadUrl: 'https://bitbucket.org/atlassian/localstack/get/master.zip', isSubdirectory: false, pathToDirectory: '', + auth: '', }); }); it('should parse a valid BitBucket URL with subdirectory', () => { - const output = parseRepoURL('https://bitbucket.org/atlassian/localstack/src/85870856fd6941ae75c0fa946a51cf756ff2f53a/localstack/dashboard/?at=mvn'); + const output = parseRepoURL( + 'https://bitbucket.org/atlassian/localstack/src/85870856fd6941ae75c0fa946a51cf756ff2f53a/localstack/dashboard/?at=mvn' + ); expect(output).to.deep.eq({ owner: 'atlassian', @@ -196,6 +242,7 @@ describe('downloadTemplateFromRepo', () => { downloadUrl: 'https://bitbucket.org/atlassian/localstack/get/mvn.zip', isSubdirectory: true, pathToDirectory: `localstack${path.sep}dashboard`, + auth: '', }); }); @@ -206,9 +253,11 @@ describe('downloadTemplateFromRepo', () => { owner: 'serverless', repo: 'serverless', branch: 'master', - downloadUrl: 'https://gitlab.com/serverless/serverless/-/archive/master/serverless-master.zip', + downloadUrl: + 'https://gitlab.com/serverless/serverless/-/archive/master/serverless-master.zip', isSubdirectory: false, pathToDirectory: '', + auth: '', }); }); @@ -222,6 +271,7 @@ describe('downloadTemplateFromRepo', () => { downloadUrl: 'https://gitlab.com/serverless/serverless/-/archive/dev/serverless-dev.zip', isSubdirectory: true, pathToDirectory: 'subdir', + auth: '', }); }); }); diff --git a/lib/utils/fs/createZipFile.js b/lib/utils/fs/createZipFile.js new file mode 100644 index 000000000..5207313ec --- /dev/null +++ b/lib/utils/fs/createZipFile.js @@ -0,0 +1,39 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const archiver = require('archiver'); +const BbPromise = require('bluebird'); +const walkDirSync = require('../fs/walkDirSync'); + +function createZipFile(srcDirPath, outputFilePath) { + const files = walkDirSync(srcDirPath).map(file => ({ + input: file, + output: file.replace(path.join(srcDirPath, path.sep), ''), + })); + + return new BbPromise((resolve, reject) => { + const output = fs.createWriteStream(outputFilePath); + const archive = archiver('zip', { + zlib: { level: 9 }, + }); + + output.on('open', () => { + archive.pipe(output); + + files.forEach(file => { + // TODO: update since this is REALLY slow + if (fs.lstatSync(file.input).isFile()) { + archive.append(fs.createReadStream(file.input), { name: file.output }); + } + }); + + archive.finalize(); + }); + + archive.on('error', err => reject(err)); + output.on('close', () => resolve(outputFilePath)); + }); +} + +module.exports = createZipFile; diff --git a/lib/utils/fs/createZipFile.test.js b/lib/utils/fs/createZipFile.test.js new file mode 100644 index 000000000..1e609c6a9 --- /dev/null +++ b/lib/utils/fs/createZipFile.test.js @@ -0,0 +1,27 @@ +'use strict'; + +const path = require('path'); +const chai = require('chai'); +const createZipFile = require('./createZipFile'); +const { createTmpFile, listZipFiles } = require('../../../tests/utils/fs'); + +// Configure chai +chai.use(require('chai-as-promised')); +chai.use(require('sinon-chai')); +const expect = require('chai').expect; + +describe('#createZipFile()', () => { + it('should create a zip file with the source directory content', () => { + const toZipFilePath = createTmpFile('foo.json'); + const zipFilePath = createTmpFile('package.zip'); + + const srcDirPath = toZipFilePath + .split(path.sep) + .slice(0, -1) + .join(path.sep); + + return createZipFile(srcDirPath, zipFilePath) + .then(listZipFiles) + .then(files => expect(files).to.deep.equal(['foo.json'])); + }); +}); diff --git a/lib/utils/fs/fileExists.js b/lib/utils/fs/fileExists.js index 7edf920e9..81a829ad5 100644 --- a/lib/utils/fs/fileExists.js +++ b/lib/utils/fs/fileExists.js @@ -3,8 +3,9 @@ const fse = require('./fse'); function fileExists(filePath) { - return fse.lstatAsync(filePath) - .then((stats) => stats.isFile()) + return fse + .lstatAsync(filePath) + .then(stats => stats.isFile()) .catch(() => false); } diff --git a/lib/utils/fs/fileExists.test.js b/lib/utils/fs/fileExists.test.js index 71bf001d1..159a78db6 100644 --- a/lib/utils/fs/fileExists.test.js +++ b/lib/utils/fs/fileExists.test.js @@ -11,10 +11,10 @@ const expect = require('chai').expect; describe('#fileExists()', () => { describe('When reading a file', () => { - it('should detect if a file exists', () => expect(fileExists(__filename)) - .to.eventually.equal(true)); + it('should detect if a file exists', () => + expect(fileExists(__filename)).to.eventually.equal(true)); - it('should detect if a file doesn\'t exist', () => expect(fileExists(path - .join(__dirname, 'XYZ.json'))).to.eventually.equal(false)); + it("should detect if a file doesn't exist", () => + expect(fileExists(path.join(__dirname, 'XYZ.json'))).to.eventually.equal(false)); }); }); diff --git a/lib/utils/fs/fileExistsSync.test.js b/lib/utils/fs/fileExistsSync.test.js index 34ab7fefe..e6e3b6e85 100644 --- a/lib/utils/fs/fileExistsSync.test.js +++ b/lib/utils/fs/fileExistsSync.test.js @@ -11,7 +11,7 @@ describe('#fileExistsSync()', () => { expect(file).to.equal(true); }); - it('should detect if a file doesn\'t exist', () => { + it("should detect if a file doesn't exist", () => { const noFile = fileExistsSync(path.join(__dirname, 'XYZ.json')); expect(noFile).to.equal(false); }); diff --git a/lib/utils/fs/parse.test.js b/lib/utils/fs/parse.test.js index 8473e925e..c0c0f72bd 100644 --- a/lib/utils/fs/parse.test.js +++ b/lib/utils/fs/parse.test.js @@ -112,7 +112,6 @@ describe('#parse()', () => { }); }); - it('should parse YAML without shorthand syntax', () => { const tmpFilePath = 'anything.yml'; const fileContents = 'Item:\n Fn::Join:\n - ""\n - - "arn:aws:s3::"\n - !Ref MyBucket'; diff --git a/lib/utils/fs/readFile.js b/lib/utils/fs/readFile.js index b494baff9..81f571ffb 100644 --- a/lib/utils/fs/readFile.js +++ b/lib/utils/fs/readFile.js @@ -4,8 +4,7 @@ const fse = require('./fse'); const parse = require('./parse'); function readFile(filePath) { - return fse.readFileAsync(filePath, 'utf8') - .then((contents) => parse(filePath, contents)); + return fse.readFileAsync(filePath, 'utf8').then(contents => parse(filePath, contents)); } module.exports = readFile; diff --git a/lib/utils/fs/readFile.test.js b/lib/utils/fs/readFile.test.js index 87625df4d..d2383b20d 100644 --- a/lib/utils/fs/readFile.test.js +++ b/lib/utils/fs/readFile.test.js @@ -1,9 +1,9 @@ 'use strict'; -const testUtils = require('../../../tests/utils'); const chai = require('chai'); const writeFile = require('./writeFile'); const readFile = require('./readFile'); +const { getTmpFilePath } = require('../../../tests/utils/fs'); // Configure chai chai.use(require('chai-as-promised')); @@ -12,29 +12,33 @@ const expect = require('chai').expect; describe('#readFile()', () => { it('should read a file asynchronously', () => { - const tmpFilePath = testUtils.getTmpFilePath('anything.json'); + const tmpFilePath = getTmpFilePath('anything.json'); - return writeFile(tmpFilePath, { foo: 'bar' }) - .then(() => expect(readFile(tmpFilePath)).to.eventually.deep.equal({ foo: 'bar' })); + return writeFile(tmpFilePath, { foo: 'bar' }).then(() => + expect(readFile(tmpFilePath)).to.eventually.deep.equal({ foo: 'bar' }) + ); }); it('should read a filename extension .yml', () => { - const tmpFilePath = testUtils.getTmpFilePath('anything.yml'); + const tmpFilePath = getTmpFilePath('anything.yml'); - return writeFile(tmpFilePath, { foo: 'bar' }) - .then(() => expect(readFile(tmpFilePath)).to.eventually.deep.equal({ foo: 'bar' })); + return writeFile(tmpFilePath, { foo: 'bar' }).then(() => + expect(readFile(tmpFilePath)).to.eventually.deep.equal({ foo: 'bar' }) + ); }); it('should read a filename extension .yaml', () => { - const tmpFilePath = testUtils.getTmpFilePath('anything.yaml'); + const tmpFilePath = getTmpFilePath('anything.yaml'); - return writeFile(tmpFilePath, { foo: 'bar' }) - .then(() => expect(readFile(tmpFilePath)).to.eventually.deep.equal({ foo: 'bar' })); + return writeFile(tmpFilePath, { foo: 'bar' }).then(() => + expect(readFile(tmpFilePath)).to.eventually.deep.equal({ foo: 'bar' }) + ); }); it('should throw YAMLException with filename if yml file is invalid format', () => { - const tmpFilePath = testUtils.getTmpFilePath('invalid.yml'); - return writeFile(tmpFilePath, ': a').then(() => readFile(tmpFilePath)) + const tmpFilePath = getTmpFilePath('invalid.yml'); + return writeFile(tmpFilePath, ': a') + .then(() => readFile(tmpFilePath)) .catch(e => { expect(e.name).to.equal('YAMLException'); expect(e.message).to.match(new RegExp('.*invalid.yml')); diff --git a/lib/utils/fs/readFileIfExists.js b/lib/utils/fs/readFileIfExists.js index 4876033db..4446a45ca 100644 --- a/lib/utils/fs/readFileIfExists.js +++ b/lib/utils/fs/readFileIfExists.js @@ -1,15 +1,16 @@ +'use strict'; + const fileExists = require('./fileExists'); const readFile = require('./readFile'); const BbPromise = require('bluebird'); -const readFileIfExists = function (filePath) { - return fileExists(filePath) - .then((exists) => { - if (!exists) { - return BbPromise.resolve(false); - } - return readFile(filePath); - }); +const readFileIfExists = function(filePath) { + return fileExists(filePath).then(exists => { + if (!exists) { + return BbPromise.resolve(false); + } + return readFile(filePath); + }); }; module.exports = readFileIfExists; diff --git a/lib/utils/fs/readFileIfExists.test.js b/lib/utils/fs/readFileIfExists.test.js index 71d89e870..46a0c2d02 100644 --- a/lib/utils/fs/readFileIfExists.test.js +++ b/lib/utils/fs/readFileIfExists.test.js @@ -10,13 +10,13 @@ chai.use(require('sinon-chai')); const expect = require('chai').expect; describe('#readFileIfExists()', () => { - it('should resolve with file content if file exists', () => readFileIfExists(__filename) - .then(content => { + it('should resolve with file content if file exists', () => + readFileIfExists(__filename).then(content => { expect(content).to.not.equal(false); expect(content).to.not.equal(undefined); expect(typeof content).to.equal('string'); })); - it('should resolve with false if file does not exist', () => expect(readFileIfExists(path - .join(__dirname, 'XYZ.json'))).to.eventually.equal(false)); + it('should resolve with false if file does not exist', () => + expect(readFileIfExists(path.join(__dirname, 'XYZ.json'))).to.eventually.equal(false)); }); diff --git a/lib/utils/fs/readFileSync.test.js b/lib/utils/fs/readFileSync.test.js index 652432040..f02a98921 100644 --- a/lib/utils/fs/readFileSync.test.js +++ b/lib/utils/fs/readFileSync.test.js @@ -1,13 +1,13 @@ 'use strict'; -const testUtils = require('../../../tests/utils'); const expect = require('chai').expect; const writeFileSync = require('./writeFileSync'); const readFileSync = require('./readFileSync'); +const { getTmpFilePath } = require('../../../tests/utils/fs'); describe('#readFileSync()', () => { it('should read a file synchronously', () => { - const tmpFilePath = testUtils.getTmpFilePath('anything.json'); + const tmpFilePath = getTmpFilePath('anything.json'); writeFileSync(tmpFilePath, { foo: 'bar' }); const obj = readFileSync(tmpFilePath); @@ -16,7 +16,7 @@ describe('#readFileSync()', () => { }); it('should read a filename extension .yml', () => { - const tmpFilePath = testUtils.getTmpFilePath('anything.yml'); + const tmpFilePath = getTmpFilePath('anything.yml'); writeFileSync(tmpFilePath, { foo: 'bar' }); const obj = readFileSync(tmpFilePath); @@ -25,7 +25,7 @@ describe('#readFileSync()', () => { }); it('should read a filename extension .yaml', () => { - const tmpFilePath = testUtils.getTmpFilePath('anything.yaml'); + const tmpFilePath = getTmpFilePath('anything.yaml'); writeFileSync(tmpFilePath, { foo: 'bar' }); const obj = readFileSync(tmpFilePath); @@ -34,12 +34,12 @@ describe('#readFileSync()', () => { }); it('should throw YAMLException with filename if yml file is invalid format', () => { - const tmpFilePath = testUtils.getTmpFilePath('invalid.yml'); + const tmpFilePath = getTmpFilePath('invalid.yml'); writeFileSync(tmpFilePath, ': a'); expect(() => { readFileSync(tmpFilePath); - }).to.throw(new RegExp('YAMLException:.*invalid.yml')); + }).to.throw(/.*invalid.yml/); }); }); diff --git a/lib/utils/fs/walkDirSync.js b/lib/utils/fs/walkDirSync.js index 150e856b0..d0038cedd 100644 --- a/lib/utils/fs/walkDirSync.js +++ b/lib/utils/fs/walkDirSync.js @@ -4,12 +4,15 @@ const path = require('path'); const fs = require('fs'); function walkDirSync(dirPath, opts) { - const options = Object.assign({ - noLinks: false, - }, opts); + const options = Object.assign( + { + noLinks: false, + }, + opts + ); let filePaths = []; const list = fs.readdirSync(dirPath); - list.forEach((filePathParam) => { + list.forEach(filePathParam => { let filePath = filePathParam; filePath = path.join(dirPath, filePath); const stat = options.noLinks ? fs.lstatSync(filePath) : fs.statSync(filePath); diff --git a/lib/utils/fs/walkDirSync.test.js b/lib/utils/fs/walkDirSync.test.js index 2ef3a2600..04ffea2f0 100644 --- a/lib/utils/fs/walkDirSync.test.js +++ b/lib/utils/fs/walkDirSync.test.js @@ -2,14 +2,15 @@ const fs = require('fs'); const path = require('path'); -const expect = require('chai').expect; -const testUtils = require('../../../tests/utils'); const writeFileSync = require('./writeFileSync'); const walkDirSync = require('./walkDirSync'); +const { expect } = require('chai'); +const { getTmpDirPath } = require('../../../tests/utils/fs'); +const { skipOnWindowsDisabledSymlinks } = require('../../../tests/utils/misc'); describe('#walkDirSync()', () => { it('should return an array with corresponding paths to the found files', () => { - const tmpDirPath = testUtils.getTmpDirPath(); + const tmpDirPath = getTmpDirPath(); const nestedDir1 = path.join(tmpDirPath, 'foo'); const nestedDir2 = path.join(tmpDirPath, 'foo', 'bar'); @@ -30,14 +31,19 @@ describe('#walkDirSync()', () => { expect(filePaths).to.include(tmpFilePath3); }); - it('should check noLinks option', () => { - const tmpDirPath = testUtils.getTmpDirPath(); + it('should check noLinks option', function() { + const tmpDirPath = getTmpDirPath(); const realFile = path.join(tmpDirPath, 'real'); writeFileSync(realFile, 'content'); const symLink = path.join(tmpDirPath, 'sym'); - fs.symlinkSync(realFile, symLink); + try { + fs.symlinkSync(realFile, symLink); + } catch (error) { + skipOnWindowsDisabledSymlinks(error, this); + throw error; + } const filePaths = walkDirSync(tmpDirPath, { noLinks: true, diff --git a/lib/utils/fs/writeFile.js b/lib/utils/fs/writeFile.js index abeef07e0..42defb6a7 100644 --- a/lib/utils/fs/writeFile.js +++ b/lib/utils/fs/writeFile.js @@ -8,25 +8,24 @@ const YAML = require('js-yaml'); function writeFile(filePath, conts, cycles) { let contents = conts || ''; - return fse.mkdirsAsync(path.dirname(filePath)) - .then(() => { - if (filePath.indexOf('.json') !== -1 && typeof contents !== 'string') { - if (cycles) { - contents = jc.stringify(contents, null, 2); - } else { - contents = JSON.stringify(contents, null, 2); - } + return fse.mkdirsAsync(path.dirname(filePath)).then(() => { + if (filePath.indexOf('.json') !== -1 && typeof contents !== 'string') { + if (cycles) { + contents = jc.stringify(contents, null, 2); + } else { + contents = JSON.stringify(contents, null, 2); } + } - const yamlFileExists = (filePath.indexOf('.yaml') !== -1); - const ymlFileExists = (filePath.indexOf('.yml') !== -1); + const yamlFileExists = filePath.indexOf('.yaml') !== -1; + const ymlFileExists = filePath.indexOf('.yml') !== -1; - if ((yamlFileExists || ymlFileExists) && typeof contents !== 'string') { - contents = YAML.dump(contents); - } + if ((yamlFileExists || ymlFileExists) && typeof contents !== 'string') { + contents = YAML.dump(contents); + } - return fse.writeFileAsync(filePath, contents); - }); + return fse.writeFileAsync(filePath, contents); + }); } module.exports = writeFile; diff --git a/lib/utils/fs/writeFile.test.js b/lib/utils/fs/writeFile.test.js index 966c928d4..35aefabba 100644 --- a/lib/utils/fs/writeFile.test.js +++ b/lib/utils/fs/writeFile.test.js @@ -1,18 +1,18 @@ 'use strict'; const fse = require('./fse'); -const testUtils = require('../../../tests/utils'); const Serverless = require('../../../lib/Serverless'); const chai = require('chai'); const writeFile = require('./writeFile'); const readFile = require('./readFile'); +const { getTmpFilePath } = require('../../../tests/utils/fs'); // Configure chai chai.use(require('chai-as-promised')); chai.use(require('sinon-chai')); const expect = require('chai').expect; -describe('#writeFile()', function () { +describe('#writeFile()', function() { let serverless; this.timeout(0); @@ -21,37 +21,36 @@ describe('#writeFile()', function () { }); it('should write a .json file asynchronously', () => { - const tmpFilePath = testUtils.getTmpFilePath('anything.json'); - return writeFile(tmpFilePath, { foo: 'bar' }) - .then(() => expect(readFile(tmpFilePath)) - .to.eventually.deep.equal({ foo: 'bar' })); + const tmpFilePath = getTmpFilePath('anything.json'); + return writeFile(tmpFilePath, { foo: 'bar' }).then(() => + expect(readFile(tmpFilePath)).to.eventually.deep.equal({ foo: 'bar' }) + ); }); it('should write a .yml file synchronously', () => { - const tmpFilePath = testUtils.getTmpFilePath('anything.yml'); + const tmpFilePath = getTmpFilePath('anything.yml'); - return writeFile(tmpFilePath, { foo: 'bar' }) - .then(() => expect(serverless.yamlParser.parse(tmpFilePath)) - .to.eventually.deep.equal({ foo: 'bar' })); + return writeFile(tmpFilePath, { foo: 'bar' }).then(() => + expect(serverless.yamlParser.parse(tmpFilePath)).to.eventually.deep.equal({ foo: 'bar' }) + ); }); it('should write a .yaml file synchronously', () => { - const tmpFilePath = testUtils.getTmpFilePath('anything.yaml'); + const tmpFilePath = getTmpFilePath('anything.yaml'); - return writeFile(tmpFilePath, { foo: 'bar' }) - .then(() => expect(serverless.yamlParser.parse(tmpFilePath)) - .to.eventually.deep.equal({ foo: 'bar' })); + return writeFile(tmpFilePath, { foo: 'bar' }).then(() => + expect(serverless.yamlParser.parse(tmpFilePath)).to.eventually.deep.equal({ foo: 'bar' }) + ); }); it('should be able to write an object with circular references', () => { - const tmpFilePath = testUtils.getTmpFilePath('anything.json'); + const tmpFilePath = getTmpFilePath('anything.json'); const bar = {}; bar.foo = bar; const expected = '{\n "foo": {\n "$ref": "$"\n }\n}'; - return writeFile(tmpFilePath, bar, true) - .then(() => - expect(fse.readFileAsync(tmpFilePath, 'utf8')).to.eventually.equal(expected) - ); + return writeFile(tmpFilePath, bar, true).then(() => + expect(fse.readFileAsync(tmpFilePath, 'utf8')).to.eventually.equal(expected) + ); }); }); diff --git a/lib/utils/fs/writeFileSync.js b/lib/utils/fs/writeFileSync.js index 6e58263c4..2119a88a7 100644 --- a/lib/utils/fs/writeFileSync.js +++ b/lib/utils/fs/writeFileSync.js @@ -18,8 +18,8 @@ function writeFileSync(filePath, conts, cycles) { } } - const yamlFileExists = (filePath.indexOf('.yaml') !== -1); - const ymlFileExists = (filePath.indexOf('.yml') !== -1); + const yamlFileExists = filePath.indexOf('.yaml') !== -1; + const ymlFileExists = filePath.indexOf('.yml') !== -1; if ((yamlFileExists || ymlFileExists) && typeof contents !== 'string') { contents = YAML.dump(contents); diff --git a/lib/utils/fs/writeFileSync.test.js b/lib/utils/fs/writeFileSync.test.js index 87ec760a5..26a907a45 100644 --- a/lib/utils/fs/writeFileSync.test.js +++ b/lib/utils/fs/writeFileSync.test.js @@ -1,11 +1,11 @@ 'use strict'; const fse = require('./fse'); -const testUtils = require('../../../tests/utils'); const Serverless = require('../../../lib/Serverless'); -const expect = require('chai').expect; const writeFileSync = require('./writeFileSync'); const readFileSync = require('./readFileSync'); +const { expect } = require('chai'); +const { getTmpFilePath } = require('../../../tests/utils/fs'); describe('#writeFileSync()', () => { let serverless; @@ -15,7 +15,7 @@ describe('#writeFileSync()', () => { }); it('should write a .json file synchronously', () => { - const tmpFilePath = testUtils.getTmpFilePath('anything.json'); + const tmpFilePath = getTmpFilePath('anything.json'); writeFileSync(tmpFilePath, { foo: 'bar' }); const obj = readFileSync(tmpFilePath); @@ -24,38 +24,40 @@ describe('#writeFileSync()', () => { }); it('should write a .yml file synchronously', () => { - const tmpFilePath = testUtils.getTmpFilePath('anything.yml'); + const tmpFilePath = getTmpFilePath('anything.yml'); writeFileSync(tmpFilePath, { foo: 'bar' }); - return serverless.yamlParser.parse(tmpFilePath).then((obj) => { + return serverless.yamlParser.parse(tmpFilePath).then(obj => { expect(obj.foo).to.equal('bar'); }); }); it('should write a .yaml file synchronously', () => { - const tmpFilePath = testUtils.getTmpFilePath('anything.yaml'); + const tmpFilePath = getTmpFilePath('anything.yaml'); writeFileSync(tmpFilePath, { foo: 'bar' }); - return serverless.yamlParser.parse(tmpFilePath).then((obj) => { + return serverless.yamlParser.parse(tmpFilePath).then(obj => { expect(obj.foo).to.equal('bar'); }); }); it('should throw error if invalid path is provided', () => { - expect(() => { writeFileSync(null); }).to.throw(Error); + expect(() => { + writeFileSync(null); + }).to.throw(Error); }); it('should be able to write an object with circular references', () => { - const tmpFilePath = testUtils.getTmpFilePath('anything.json'); + const tmpFilePath = getTmpFilePath('anything.json'); const bar = {}; bar.foo = bar; const expected = '{\n "foo": {\n "$ref": "$"\n }\n}'; writeFileSync(tmpFilePath, bar, true); - return fse.readFileAsync(tmpFilePath, 'utf8').then((contents) => { + return fse.readFileAsync(tmpFilePath, 'utf8').then(contents => { expect(contents).to.equal(expected); }); }); diff --git a/lib/utils/getCacheFile.js b/lib/utils/getCacheFile.js index 8a51706b7..e44ffda69 100644 --- a/lib/utils/getCacheFile.js +++ b/lib/utils/getCacheFile.js @@ -4,15 +4,14 @@ const fileExists = require('./fs/fileExists'); const readFile = require('./fs/readFile'); const getCacheFilePath = require('./getCacheFilePath'); -const getCacheFile = function (servicePath) { +const getCacheFile = function(servicePath) { const cacheFilePath = getCacheFilePath(servicePath); - return fileExists(cacheFilePath) - .then((exists) => { - if (!exists) { - return false; - } - return readFile(cacheFilePath); - }); + return fileExists(cacheFilePath).then(exists => { + if (!exists) { + return false; + } + return readFile(cacheFilePath); + }); }; module.exports = getCacheFile; diff --git a/lib/utils/getCacheFilePath.js b/lib/utils/getCacheFilePath.js index adf6d5d25..72ec74097 100644 --- a/lib/utils/getCacheFilePath.js +++ b/lib/utils/getCacheFilePath.js @@ -4,9 +4,12 @@ const homedir = require('os').homedir(); const path = require('path'); const crypto = require('crypto'); -const getCacheFilePath = function (srvcPath) { +const getCacheFilePath = function(srvcPath) { const servicePath = srvcPath || process.cwd(); - const servicePathHash = crypto.createHash('sha256').update(servicePath).digest('hex'); + const servicePathHash = crypto + .createHash('sha256') + .update(servicePath) + .digest('hex'); return path.join(homedir, '.serverless', 'cache', servicePathHash, 'autocomplete.json'); }; diff --git a/lib/utils/getCommandSuggestion.js b/lib/utils/getCommandSuggestion.js index 3e4604ed8..85f22bf39 100644 --- a/lib/utils/getCommandSuggestion.js +++ b/lib/utils/getCommandSuggestion.js @@ -3,8 +3,8 @@ const _ = require('lodash'); const levenshtein = require('fast-levenshtein'); const getCollectCommandWords = (commandObject, commandWordsArray) => { - let wordsArray = _.isArray(commandWordsArray) - && !_.isEmpty(commandWordsArray) ? commandWordsArray : []; + let wordsArray = + _.isArray(commandWordsArray) && !_.isEmpty(commandWordsArray) ? commandWordsArray : []; _.forEach(commandObject, (commandChildObject, commandChildName) => { wordsArray.push(commandChildName); if (commandChildObject.commands) { diff --git a/lib/utils/getCommandSuggestion.test.js b/lib/utils/getCommandSuggestion.test.js index 56ea46337..499570f6b 100644 --- a/lib/utils/getCommandSuggestion.test.js +++ b/lib/utils/getCommandSuggestion.test.js @@ -4,10 +4,14 @@ const expect = require('chai').expect; const getCommandSuggestion = require('./getCommandSuggestion'); const Serverless = require('../../lib/Serverless'); -const serverless = new Serverless(); -serverless.init(); - describe('#getCommandSuggestion', () => { + let serverless; + + before(() => { + serverless = new Serverless(); + return serverless.init(); + }); + it('should return "package" as a suggested command if you input "pekage"', () => { const suggestedCommand = getCommandSuggestion('pekage', serverless.cli.loadedCommands); expect(suggestedCommand).to.be.equal('package'); diff --git a/lib/utils/getServerlessConfigFile.js b/lib/utils/getServerlessConfigFile.js index c0f07a6b2..23ddbb8c7 100644 --- a/lib/utils/getServerlessConfigFile.js +++ b/lib/utils/getServerlessConfigFile.js @@ -6,8 +6,12 @@ const path = require('path'); const fileExists = require('./fs/fileExists'); const readFile = require('./fs/readFile'); -const getServerlessConfigFilePath = (srvcPath) => { - const servicePath = srvcPath || process.cwd(); +const getConfigFilePath = (servicePath, options = {}) => { + if (options.config) { + const customPath = path.join(servicePath, options.config); + return fileExists(customPath).then(exists => (exists ? customPath : null)); + } + const jsonPath = path.join(servicePath, 'serverless.json'); const ymlPath = path.join(servicePath, 'serverless.yml'); const yamlPath = path.join(servicePath, 'serverless.yaml'); @@ -19,12 +23,12 @@ const getServerlessConfigFilePath = (srvcPath) => { yaml: fileExists(yamlPath), js: fileExists(jsPath), }).then(exists => { - if (exists.json) { - return jsonPath; + if (exists.yaml) { + return yamlPath; } else if (exists.yml) { return ymlPath; - } else if (exists.yaml) { - return yamlPath; + } else if (exists.json) { + return jsonPath; } else if (exists.js) { return jsPath; } @@ -33,33 +37,43 @@ const getServerlessConfigFilePath = (srvcPath) => { }); }; -const handleJsConfigFile = (jsConfigFile) => BbPromise.try(() => { - // use require to load serverless.js - // eslint-disable-next-line global-require - const configExport = require(jsConfigFile); - // In case of a promise result, first resolve it. - return configExport; -}).then(config => { - if (_.isPlainObject(config)) { - return config; - } - throw new Error('serverless.js must export plain object'); -}); +const getServerlessConfigFilePath = serverless => { + return getConfigFilePath( + serverless.config.servicePath || process.cwd(), + serverless.processedInput.options + ); +}; -const getServerlessConfigFile = _.memoize(srvcPath => getServerlessConfigFilePath(srvcPath) - .then(configFilePath => { - if (configFilePath !== null) { - const isJSConfigFile = _.last(_.split(configFilePath, '.')) === 'js'; +const handleJsConfigFile = jsConfigFile => + BbPromise.try(() => { + // use require to load serverless.js + // eslint-disable-next-line global-require + const configExport = require(jsConfigFile); + // In case of a promise result, first resolve it. + return configExport; + }).then(config => { + if (_.isPlainObject(config)) { + return config; + } + throw new Error('serverless.js must export plain object'); + }); - if (isJSConfigFile) { - return handleJsConfigFile(configFilePath); +const getServerlessConfigFile = _.memoize( + serverless => + getServerlessConfigFilePath(serverless).then(configFilePath => { + if (configFilePath !== null) { + const isJSConfigFile = _.last(_.split(configFilePath, '.')) === 'js'; + + if (isJSConfigFile) { + return handleJsConfigFile(configFilePath); + } + + return readFile(configFilePath); } - return readFile(configFilePath); - } - - return ''; - }) + return ''; + }), + serverless => `${serverless.processedInput.options.config} - ${serverless.config.servicePath}` ); -module.exports = { getServerlessConfigFile, getServerlessConfigFilePath }; +module.exports = { getConfigFilePath, getServerlessConfigFile, getServerlessConfigFilePath }; diff --git a/lib/utils/getServerlessConfigFile.test.js b/lib/utils/getServerlessConfigFile.test.js index 17318bb56..44be5b7b7 100644 --- a/lib/utils/getServerlessConfigFile.test.js +++ b/lib/utils/getServerlessConfigFile.test.js @@ -1,25 +1,34 @@ 'use strict'; const path = require('path'); -const expect = require('chai').expect; -const testUtils = require('../../tests/utils'); +const chai = require('chai'); const writeFileSync = require('./fs/writeFileSync'); const serverlessConfigFileUtils = require('./getServerlessConfigFile'); +const { getTmpDirPath } = require('../../tests/utils/fs'); const getServerlessConfigFile = serverlessConfigFileUtils.getServerlessConfigFile; +chai.use(require('chai-as-promised')); + +const expect = chai.expect; + describe('#getServerlessConfigFile()', () => { let tmpDirPath; beforeEach(() => { - tmpDirPath = testUtils.getTmpDirPath(); + tmpDirPath = getTmpDirPath(); }); it('should return an empty string if no serverless file is found', () => { const randomFilePath = path.join(tmpDirPath, 'not-a-serverless-file'); writeFileSync(randomFilePath, 'some content'); - return expect(getServerlessConfigFile(tmpDirPath)).to.be.fulfilled.then((result) => { + return expect( + getServerlessConfigFile({ + processedInput: { options: {} }, + config: { servicePath: tmpDirPath }, + }) + ).to.be.fulfilled.then(result => { expect(result).to.equal(''); }); }); @@ -28,7 +37,12 @@ describe('#getServerlessConfigFile()', () => { const serverlessFilePath = path.join(tmpDirPath, 'serverless.yml'); writeFileSync(serverlessFilePath, 'service: my-yml-service'); - return expect(getServerlessConfigFile(tmpDirPath)).to.be.fulfilled.then((result) => { + return expect( + getServerlessConfigFile({ + processedInput: { options: {} }, + config: { servicePath: tmpDirPath }, + }) + ).to.be.fulfilled.then(result => { expect(result).to.deep.equal({ service: 'my-yml-service' }); }); }); @@ -37,7 +51,26 @@ describe('#getServerlessConfigFile()', () => { const serverlessFilePath = path.join(tmpDirPath, 'serverless.yaml'); writeFileSync(serverlessFilePath, 'service: my-yaml-service'); - return expect(getServerlessConfigFile(tmpDirPath)).to.be.fulfilled.then((result) => { + return expect( + getServerlessConfigFile({ + processedInput: { options: {} }, + config: { servicePath: tmpDirPath }, + }) + ).to.be.fulfilled.then(result => { + expect(result).to.deep.equal({ service: 'my-yaml-service' }); + }); + }); + + it('should return the file content if a foobar.yaml file is specified & found', () => { + const serverlessFilePath = path.join(tmpDirPath, 'foobar.yaml'); + + writeFileSync(serverlessFilePath, 'service: my-yaml-service'); + return expect( + getServerlessConfigFile({ + processedInput: { options: { config: 'foobar.yaml' } }, + config: { servicePath: tmpDirPath }, + }) + ).to.be.fulfilled.then(result => { expect(result).to.deep.equal({ service: 'my-yaml-service' }); }); }); @@ -46,23 +79,28 @@ describe('#getServerlessConfigFile()', () => { const serverlessFilePath = path.join(tmpDirPath, 'serverless.json'); writeFileSync(serverlessFilePath, '{ "service": "my-json-service" }'); - return expect(getServerlessConfigFile(tmpDirPath)).to.be.fulfilled.then((result) => { + return expect( + getServerlessConfigFile({ + processedInput: { options: {} }, + config: { servicePath: tmpDirPath }, + }) + ).to.be.fulfilled.then(result => { expect(result).to.deep.equal({ service: 'my-json-service' }); }); }); it('should return the file content if a serverless.js file found', () => { const serverlessFilePath = path.join(tmpDirPath, 'serverless.js'); - writeFileSync( - serverlessFilePath, - 'module.exports = {"service": "my-json-service"};' - ); + writeFileSync(serverlessFilePath, 'module.exports = {"service": "my-json-service"};'); - return expect(getServerlessConfigFile(tmpDirPath)).to.be.fulfilled.then( - (result) => { - expect(result).to.deep.equal({ service: 'my-json-service' }); - } - ); + return expect( + getServerlessConfigFile({ + processedInput: { options: {} }, + config: { servicePath: tmpDirPath }, + }) + ).to.be.fulfilled.then(result => { + expect(result).to.deep.equal({ service: 'my-json-service' }); + }); }); it('should return the resolved value if a promise-using serverless.js file found', () => { @@ -72,22 +110,26 @@ describe('#getServerlessConfigFile()', () => { 'module.exports = new Promise(resolve => { resolve({"service": "my-json-service"}); });' ); - return expect(getServerlessConfigFile(tmpDirPath)).to.be.fulfilled.then( - (result) => { - expect(result).to.deep.equal({ service: 'my-json-service' }); - } - ); + return expect( + getServerlessConfigFile({ + processedInput: { options: {} }, + config: { servicePath: tmpDirPath }, + }) + ).to.be.fulfilled.then(result => { + expect(result).to.deep.equal({ service: 'my-json-service' }); + }); }); it('should throw an error, if serverless.js export not a plain object', () => { const serverlessFilePath = path.join(tmpDirPath, 'serverless.js'); - writeFileSync( - serverlessFilePath, - 'module.exports = function config() {};' - ); + writeFileSync(serverlessFilePath, 'module.exports = function config() {};'); - return expect(getServerlessConfigFile(tmpDirPath)) - .to.be.rejectedWith('serverless.js must export plain object'); + return expect( + getServerlessConfigFile({ + processedInput: { options: {} }, + config: { servicePath: tmpDirPath }, + }) + ).to.be.rejectedWith('serverless.js must export plain object'); }); it('should look in the current working directory if servicePath is undefined', () => { @@ -96,14 +138,20 @@ describe('#getServerlessConfigFile()', () => { writeFileSync(serverlessFilePath, 'service: my-yml-service'); const cwd = process.cwd(); process.chdir(tmpDirPath); - return expect(getServerlessConfigFile()).to.be.fulfilled - .then(result => result) - .catch((ex) => { - process.chdir(cwd); - throw ex; + return expect( + getServerlessConfigFile({ + processedInput: { options: {} }, + config: {}, }) - .then((result) => { + ).to.be.fulfilled.then( + result => { + process.chdir(cwd); expect(result).to.deep.equal({ service: 'my-yml-service' }); - }); + }, + error => { + process.chdir(cwd); + throw error; + } + ); }); }); diff --git a/lib/utils/getTrackingConfigFileName.js b/lib/utils/getTrackingConfigFileName.js index 5bc85aedd..2bb98cc55 100644 --- a/lib/utils/getTrackingConfigFileName.js +++ b/lib/utils/getTrackingConfigFileName.js @@ -1,6 +1,6 @@ 'use strict'; -const getTrackingConfigFileName = function () { +const getTrackingConfigFileName = function() { return 'tracking-config.json'; }; diff --git a/lib/utils/log/consoleLog.js b/lib/utils/log/consoleLog.js index 2ba6c7017..b256d8efb 100644 --- a/lib/utils/log/consoleLog.js +++ b/lib/utils/log/consoleLog.js @@ -1,7 +1,7 @@ 'use strict'; -const consoleLog = function () { - console.log(arguments); // eslint-disable-line no-console +const consoleLog = function(...args) { + console.log(args); // eslint-disable-line no-console }; module.exports = consoleLog; diff --git a/lib/utils/log/fileLog.js b/lib/utils/log/fileLog.js index b8dd3549f..f47e5bf72 100644 --- a/lib/utils/log/fileLog.js +++ b/lib/utils/log/fileLog.js @@ -4,11 +4,10 @@ const _ = require('lodash'); const fs = require('fs'); const path = require('path'); -const fileLog = function () { +const fileLog = function(...args) { // TODO BRN: This does not guarentee order, is not multi process safe, // TODO BRN: and is not guarenteed to complete before exit. - fs.appendFileSync(path.join(process.cwd(), 'sls.log'), - _.join(Array.prototype.slice.call(arguments)) + '\n'); // eslint-disable-line prefer-template + fs.appendFileSync(path.join(process.cwd(), 'sls.log'), `${_.join(args)}\n`); }; module.exports = fileLog; diff --git a/lib/utils/log/log.js b/lib/utils/log/log.js index aa59e55ce..adf689b0a 100644 --- a/lib/utils/log/log.js +++ b/lib/utils/log/log.js @@ -9,8 +9,8 @@ const loggers = [ fileLog, ]; -const log = function () { - _.each(loggers, (logger) => logger.apply(null, arguments)); // eslint-disable-line prefer-spread +const log = function(...args) { + _.each(loggers, logger => logger(...args)); }; module.exports = log; diff --git a/lib/utils/log/serverlessLog.js b/lib/utils/log/serverlessLog.js index 94ba61795..b1675b114 100644 --- a/lib/utils/log/serverlessLog.js +++ b/lib/utils/log/serverlessLog.js @@ -4,7 +4,7 @@ const chalk = require('chalk'); -const log = function (message) { +const log = function(message) { console.log(`Serverless: ${chalk.yellow(message)}`); }; diff --git a/lib/utils/renameService.js b/lib/utils/renameService.js index ecb853c07..6f4b67a78 100644 --- a/lib/utils/renameService.js +++ b/lib/utils/renameService.js @@ -1,3 +1,5 @@ +'use strict'; + const path = require('path'); const fse = require('fs-extra'); @@ -11,25 +13,22 @@ function renameService(name, servicePath) { const packageFile = path.join(servicePath, 'package.json'); if (!fileExistsSync(serviceFile)) { - const errorMessage = [ - 'serverless.yml not found in', - ` ${servicePath}`, - ].join(''); + 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(' #'); - }) - .replace(/service\s*:\n {2}name:.+/gi, (match) => { - const fractions = match.split('#'); - fractions[0] = `service:\n name: ${name}`; - return fractions.join(' #'); - }); + const serverlessYml = fse + .readFileSync(serviceFile, 'utf-8') + .replace(/service\s*:.+/gi, match => { + const fractions = match.split('#'); + fractions[0] = `service: ${name}`; + return fractions.join(' #'); + }) + .replace(/service\s*:\n {2}name:.+/gi, match => { + const fractions = match.split('#'); + fractions[0] = `service:\n name: ${name}`; + return fractions.join(' #'); + }); fse.writeFileSync(serviceFile, serverlessYml); diff --git a/lib/utils/renameService.test.js b/lib/utils/renameService.test.js index 7fb600f9b..cbfb52f2d 100644 --- a/lib/utils/renameService.test.js +++ b/lib/utils/renameService.test.js @@ -1,12 +1,12 @@ '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 { expect } = require('chai'); +const { getTmpDirPath } = require('../../tests/utils/fs'); -const renameService = require('./renameService').renameService; +const { renameService } = require('./renameService'); describe('renameService', () => { let serverless; @@ -15,7 +15,7 @@ describe('renameService', () => { let servicePath; beforeEach(() => { - const tmpDir = testUtils.getTmpDirPath(); + const tmpDir = getTmpDirPath(); cwd = process.cwd(); fse.mkdirsSync(tmpDir); @@ -24,7 +24,7 @@ describe('renameService', () => { servicePath = tmpDir; serverless = new Serverless(); - serverless.init(); + return serverless.init(); }); afterEach(() => { @@ -33,10 +33,8 @@ describe('renameService', () => { }); 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 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'; @@ -93,10 +91,8 @@ describe('renameService', () => { }); it('should set new name of service in serverless.yml and name in package.json', () => { - const defaultServiceYml = - 'service:\n name: service-name\n\nprovider:\n name: aws\n'; - const newServiceYml = - 'service:\n name: new-service-name\n\nprovider:\n name: aws\n'; + const defaultServiceYml = 'service:\n name: service-name\n\nprovider:\n name: aws\n'; + const newServiceYml = 'service:\n name: new-service-name\n\nprovider:\n name: aws\n'; const defaultServiceName = 'service-name'; const newServiceName = 'new-service-name'; diff --git a/lib/utils/segment.js b/lib/utils/segment.js index 89e51270e..1fea1a5e2 100644 --- a/lib/utils/segment.js +++ b/lib/utils/segment.js @@ -14,7 +14,7 @@ function request(url, payload) { timeout: 1000, body: JSON.stringify(payload), }) - .then((response) => response.json()) + .then(response => response.json()) .then(() => BbPromise.resolve()) .catch(() => BbPromise.resolve()); } diff --git a/lib/utils/sentry.js b/lib/utils/sentry.js index 651a9302f..1a01325dc 100644 --- a/lib/utils/sentry.js +++ b/lib/utils/sentry.js @@ -1,3 +1,5 @@ +'use strict'; + const raven = require('raven'); const ci = require('ci-info'); const configUtils = require('./config'); diff --git a/lib/utils/userStats.js b/lib/utils/userStats.js index 435d6a7f9..9d6f1876b 100644 --- a/lib/utils/userStats.js +++ b/lib/utils/userStats.js @@ -13,8 +13,8 @@ const TRACK_URL = 'https://serverless.com/api/framework/track'; const IDENTIFY_URL = 'https://serverless.com/api/framework/identify'; const DEBUG = false; -function debug() { - if (DEBUG) console.log(arguments); +function debug(...args) { + if (DEBUG) console.log(args); } /* note tracking swallows errors */ @@ -25,14 +25,14 @@ function request(url, payload) { timeout: 1000, body: JSON.stringify(payload), }) - .then((response) => { - if (response.status === 404) { - return BbPromise.reject('404 api not found'); - } - return response.json(); - }) - .then((res) => BbPromise.resolve(res)) - .catch((e) => BbPromise.resolve(e)); + .then(response => { + if (response.status === 404) { + return BbPromise.reject('404 api not found'); + } + return response.json(); + }) + .then(res => BbPromise.resolve(res)) + .catch(e => BbPromise.resolve(e)); } function track(eventName, payload) { diff --git a/lib/utils/userStatsValidation.js b/lib/utils/userStatsValidation.js index 16693b5bc..735814109 100644 --- a/lib/utils/userStatsValidation.js +++ b/lib/utils/userStatsValidation.js @@ -16,7 +16,8 @@ function containsSeparators(eventName) { if (underscores !== 1) { console.log(chalk.red('Tracking Error:')); console.log( - chalk.red(`Event name must have single underscore. "${eventName}" contains ${underscores}`)); + chalk.red(`Event name must have single underscore. "${eventName}" contains ${underscores}`) + ); return false; } if (colons !== 1) { diff --git a/lib/utils/yamlAstParser.js b/lib/utils/yamlAstParser.js index 17f719a07..c47b38cd6 100644 --- a/lib/utils/yamlAstParser.js +++ b/lib/utils/yamlAstParser.js @@ -8,11 +8,9 @@ const os = require('os'); const chalk = require('chalk'); const log = require('./log/serverlessLog'); -const findKeyChain = (astContent) => { +const findKeyChain = astContent => { let content = astContent; - const chain = [ - content.key.value, - ]; + const chain = [content.key.value]; while (content.parent) { content = content.parent; if (content.key) { @@ -25,7 +23,7 @@ const findKeyChain = (astContent) => { const parseAST = (ymlAstContent, astObject) => { let newAstObject = astObject || {}; if (ymlAstContent.mappings && _.isArray(ymlAstContent.mappings)) { - _.forEach(ymlAstContent.mappings, (v) => { + _.forEach(ymlAstContent.mappings, v => { if (!v.value) { const errorMessage = [ 'Serverless: ', @@ -58,7 +56,7 @@ const parseAST = (ymlAstContent, astObject) => { const constructPlainObject = (ymlAstContent, branchObject) => { const newbranchObject = branchObject || {}; if (ymlAstContent.mappings && _.isArray(ymlAstContent.mappings)) { - _.forEach(ymlAstContent.mappings, (v) => { + _.forEach(ymlAstContent.mappings, v => { if (!v.value) { // no need to log twice, parseAST will log errors return; @@ -70,7 +68,7 @@ const constructPlainObject = (ymlAstContent, branchObject) => { newbranchObject[v.key.value] = constructPlainObject(v.value, {}); } else if (v.key.kind === 0 && v.value.kind === 3) { const plainArray = []; - _.forEach(v.value.items, (c) => { + _.forEach(v.value.items, c => { plainArray.push(c.value); }); newbranchObject[v.key.value] = plainArray; @@ -82,91 +80,98 @@ const constructPlainObject = (ymlAstContent, branchObject) => { }; const addNewArrayItem = (ymlFile, pathInYml, newValue) => -fs.readFileAsync(ymlFile, 'utf8').then(yamlContent => { - const rawAstObject = yaml.load(yamlContent); - const astObject = parseAST(rawAstObject); - const plainObject = constructPlainObject(rawAstObject); - const pathInYmlArray = pathInYml.split('.'); - - let currentNode = plainObject; - for (let i = 0; i < pathInYmlArray.length - 1; i++) { - const propertyName = pathInYmlArray[i]; - const property = currentNode[propertyName]; - if (_.isUndefined(property) || _.isObject(property)) { - currentNode[propertyName] = property || {}; - currentNode = currentNode[propertyName]; - } else { - throw new Error(`${property} can only be undefined or an object!`); - } - } - - const arrayPropertyName = _.last(pathInYmlArray); - let arrayProperty = currentNode[arrayPropertyName]; - if (_.isUndefined(arrayProperty) || _.isArray(arrayProperty)) { - arrayProperty = arrayProperty || []; - } else { - throw new Error(`${arrayProperty} can only be undefined or an array!`); - } - currentNode[arrayPropertyName] = _.union(arrayProperty, [newValue]); - - const branchToReplaceName = _.head(pathInYmlArray); - const newObject = {}; - newObject[branchToReplaceName] = plainObject[branchToReplaceName]; - const newText = yaml.dump(newObject); - if (astObject[branchToReplaceName]) { - const beginning = yamlContent - .substring(0, astObject[branchToReplaceName].parent.key.startPosition); - const end = yamlContent - .substring(astObject[branchToReplaceName].endPosition, yamlContent.length); - return fs.writeFileAsync(ymlFile, `${beginning}${newText}${end}`); - } - return fs.writeFileAsync(ymlFile, `${yamlContent}${os.EOL}${newText}`); -}); - -const removeExistingArrayItem = (ymlFile, pathInYml, removeValue) => -fs.readFileAsync(ymlFile, 'utf8').then(yamlContent => { - const rawAstObject = yaml.load(yamlContent); - const astObject = parseAST(rawAstObject); - - if (astObject[pathInYml] && astObject[pathInYml].items) { + fs.readFileAsync(ymlFile, 'utf8').then(yamlContent => { + const rawAstObject = yaml.load(yamlContent); + const astObject = parseAST(rawAstObject); const plainObject = constructPlainObject(rawAstObject); const pathInYmlArray = pathInYml.split('.'); let currentNode = plainObject; - const pathInObjectTree = []; for (let i = 0; i < pathInYmlArray.length - 1; i++) { - pathInObjectTree.push(currentNode); - currentNode = currentNode[pathInYmlArray[i]]; - } - const arrayPropertyName = _.last(pathInYmlArray); - const arrayProperty = currentNode[arrayPropertyName]; - _.pull(arrayProperty, removeValue); - - if (_.isEmpty(arrayProperty)) { - _.unset(currentNode, arrayPropertyName); - pathInObjectTree.push(currentNode); - for (let i = pathInObjectTree.length - 1; i > 0; i--) { - if (_.keys(pathInObjectTree[i]).length > 0) { - break; - } - _.unset(pathInObjectTree[i - 1], pathInYmlArray[i - 1]); + const propertyName = pathInYmlArray[i]; + const property = currentNode[propertyName]; + if (_.isUndefined(property) || _.isObject(property)) { + currentNode[propertyName] = property || {}; + currentNode = currentNode[propertyName]; + } else { + throw new Error(`${property} can only be undefined or an object!`); } } - const headObjectPath = _.head(pathInYmlArray); - let newText = ''; - - if (plainObject[headObjectPath]) { - const newObject = {}; - newObject[headObjectPath] = plainObject[headObjectPath]; - newText = yaml.dump(newObject); + const arrayPropertyName = _.last(pathInYmlArray); + let arrayProperty = currentNode[arrayPropertyName]; + if (_.isUndefined(arrayProperty) || _.isArray(arrayProperty)) { + arrayProperty = arrayProperty || []; + } else { + throw new Error(`${arrayProperty} can only be undefined or an array!`); } - const beginning = yamlContent.substring(0, astObject[headObjectPath].parent.key.startPosition); - const end = yamlContent.substring(astObject[pathInYml].endPosition, yamlContent.length); - return fs.writeFileAsync(ymlFile, `${beginning}${newText}${end}`); - } - return BbPromise.resolve(); -}); + currentNode[arrayPropertyName] = _.union(arrayProperty, [newValue]); + + const branchToReplaceName = _.head(pathInYmlArray); + const newObject = {}; + newObject[branchToReplaceName] = plainObject[branchToReplaceName]; + const newText = yaml.dump(newObject); + if (astObject[branchToReplaceName]) { + const beginning = yamlContent.substring( + 0, + astObject[branchToReplaceName].parent.key.startPosition + ); + const end = yamlContent.substring( + astObject[branchToReplaceName].endPosition, + yamlContent.length + ); + return fs.writeFileAsync(ymlFile, `${beginning}${newText}${end}`); + } + return fs.writeFileAsync(ymlFile, `${yamlContent}${os.EOL}${newText}`); + }); + +const removeExistingArrayItem = (ymlFile, pathInYml, removeValue) => + fs.readFileAsync(ymlFile, 'utf8').then(yamlContent => { + const rawAstObject = yaml.load(yamlContent); + const astObject = parseAST(rawAstObject); + + if (astObject[pathInYml] && astObject[pathInYml].items) { + const plainObject = constructPlainObject(rawAstObject); + const pathInYmlArray = pathInYml.split('.'); + + let currentNode = plainObject; + const pathInObjectTree = []; + for (let i = 0; i < pathInYmlArray.length - 1; i++) { + pathInObjectTree.push(currentNode); + currentNode = currentNode[pathInYmlArray[i]]; + } + const arrayPropertyName = _.last(pathInYmlArray); + const arrayProperty = currentNode[arrayPropertyName]; + _.pull(arrayProperty, removeValue); + + if (_.isEmpty(arrayProperty)) { + _.unset(currentNode, arrayPropertyName); + pathInObjectTree.push(currentNode); + for (let i = pathInObjectTree.length - 1; i > 0; i--) { + if (_.keys(pathInObjectTree[i]).length > 0) { + break; + } + _.unset(pathInObjectTree[i - 1], pathInYmlArray[i - 1]); + } + } + + const headObjectPath = _.head(pathInYmlArray); + let newText = ''; + + if (plainObject[headObjectPath]) { + const newObject = {}; + newObject[headObjectPath] = plainObject[headObjectPath]; + newText = yaml.dump(newObject); + } + const beginning = yamlContent.substring( + 0, + astObject[headObjectPath].parent.key.startPosition + ); + const end = yamlContent.substring(astObject[pathInYml].endPosition, yamlContent.length); + return fs.writeFileAsync(ymlFile, `${beginning}${newText}${end}`); + } + return BbPromise.resolve(); + }); module.exports = { addNewArrayItem, diff --git a/lib/utils/yamlAstParser.test.js b/lib/utils/yamlAstParser.test.js index 3dbbb2361..641c859c8 100644 --- a/lib/utils/yamlAstParser.test.js +++ b/lib/utils/yamlAstParser.test.js @@ -2,12 +2,12 @@ const path = require('path'); const chai = require('chai'); -const testUtils = require('../../tests/utils'); const writeFileSync = require('./fs/writeFileSync'); const readFileSync = require('./fs/readFileSync'); const yamlAstParser = require('./yamlAstParser'); const _ = require('lodash'); const chaiAsPromised = require('chai-as-promised'); +const { getTmpDirPath } = require('../../tests/utils/fs'); chai.use(chaiAsPromised); const expect = require('chai').expect; @@ -16,18 +16,19 @@ describe('#yamlAstParser', () => { let tmpDirPath; beforeEach(() => { - tmpDirPath = testUtils.getTmpDirPath(); + tmpDirPath = getTmpDirPath(); }); describe('#addNewArrayItem()', () => { const addNewArrayItemAndVerifyResult = (yamlContent, pathInYaml, newItem, expectedResult) => { const yamlFilePath = path.join(tmpDirPath, 'test.yaml'); writeFileSync(yamlFilePath, yamlContent); - return expect(yamlAstParser.addNewArrayItem(yamlFilePath, pathInYaml, newItem)) - .to.be.fulfilled.then(() => { - const yaml = readFileSync(yamlFilePath, 'utf8'); - expect(yaml).to.be.deep.equal(expectedResult); - }); + return expect( + yamlAstParser.addNewArrayItem(yamlFilePath, pathInYaml, newItem) + ).to.be.fulfilled.then(() => { + const yaml = readFileSync(yamlFilePath, 'utf8'); + expect(yaml).to.be.deep.equal(expectedResult); + }); }; it('should add a top level object and item into the yaml file', () => { @@ -53,8 +54,12 @@ describe('#yamlAstParser', () => { }, }, }); - return addNewArrayItemAndVerifyResult(yamlContent, - 'toplevel.second.third', 'foo', expectedResult); + return addNewArrayItemAndVerifyResult( + yamlContent, + 'toplevel.second.third', + 'foo', + expectedResult + ); }); it('should add an item under the existing multiple level object which you specify', () => { @@ -72,16 +77,19 @@ describe('#yamlAstParser', () => { }, }, }; - return addNewArrayItemAndVerifyResult(yamlContent, - 'toplevel.second.third', 'bar', expectedResult); + return addNewArrayItemAndVerifyResult( + yamlContent, + 'toplevel.second.third', + 'bar', + expectedResult + ); }); it('should add an item under partially existing multiple level object', () => { const yamlContent = { toplevel: { first: 'foo', - second: { - }, + second: {}, }, }; const expectedResult = { @@ -92,8 +100,12 @@ describe('#yamlAstParser', () => { }, }, }; - return addNewArrayItemAndVerifyResult(yamlContent, - 'toplevel.second.third', 'bar', expectedResult); + return addNewArrayItemAndVerifyResult( + yamlContent, + 'toplevel.second.third', + 'bar', + expectedResult + ); }); it('should add an item in the middle branch', () => { @@ -112,16 +124,14 @@ describe('#yamlAstParser', () => { }, bottomlevel: 'bar', }; - return addNewArrayItemAndVerifyResult(yamlContent, - 'toplevel.second', 'bar', expectedResult); + return addNewArrayItemAndVerifyResult(yamlContent, 'toplevel.second', 'bar', expectedResult); }); it('should add an item with multiple top level entries', () => { const yamlContent = { toplevel: { first: 'foo', - second: { - }, + second: {}, }, nexttoplevel: { first: 'bar', @@ -138,8 +148,12 @@ describe('#yamlAstParser', () => { first: 'bar', }, }; - return addNewArrayItemAndVerifyResult(yamlContent, - 'toplevel.second.third', 'bar', expectedResult); + return addNewArrayItemAndVerifyResult( + yamlContent, + 'toplevel.second.third', + 'bar', + expectedResult + ); }); it('should do nothing when adding the existing item', () => { @@ -156,16 +170,21 @@ describe('#yamlAstParser', () => { }); describe('#removeExistingArrayItem()', () => { - const removeExistingArrayItemAndVerifyResult = - (yamlContent, pathInYaml, removeItem, expectedResult) => { - const yamlFilePath = path.join(tmpDirPath, 'test.yaml'); - writeFileSync(yamlFilePath, yamlContent); - return expect(yamlAstParser.removeExistingArrayItem(yamlFilePath, pathInYaml, removeItem)) - .to.be.fulfilled.then(() => { - const yaml = readFileSync(yamlFilePath, 'utf8'); - expect(yaml).to.be.deep.equal(expectedResult); - }); - }; + const removeExistingArrayItemAndVerifyResult = ( + yamlContent, + pathInYaml, + removeItem, + expectedResult + ) => { + const yamlFilePath = path.join(tmpDirPath, 'test.yaml'); + writeFileSync(yamlFilePath, yamlContent); + return expect( + yamlAstParser.removeExistingArrayItem(yamlFilePath, pathInYaml, removeItem) + ).to.be.fulfilled.then(() => { + const yaml = readFileSync(yamlFilePath, 'utf8'); + expect(yaml).to.be.deep.equal(expectedResult); + }); + }; it('should remove the existing top level object and item from the yaml file', () => { const yamlContent = { @@ -173,8 +192,7 @@ describe('#yamlAstParser', () => { toplevel: ['foo'], }; const expectedResult = { service: 'test-service' }; - return removeExistingArrayItemAndVerifyResult(yamlContent, 'toplevel', - 'foo', expectedResult); + return removeExistingArrayItemAndVerifyResult(yamlContent, 'toplevel', 'foo', expectedResult); }); it('should remove the existing item under the object which you specify', () => { @@ -186,8 +204,7 @@ describe('#yamlAstParser', () => { service: 'test-service', toplevel: ['foo'], }; - return removeExistingArrayItemAndVerifyResult(yamlContent, 'toplevel', - 'bar', expectedResult); + return removeExistingArrayItemAndVerifyResult(yamlContent, 'toplevel', 'bar', expectedResult); }); it('should remove the multiple level object and item from the yaml file', () => { @@ -200,8 +217,12 @@ describe('#yamlAstParser', () => { }, }; const expectedResult = { service: 'test-service' }; - return removeExistingArrayItemAndVerifyResult(yamlContent, 'toplevel.second.third', - 'foo', expectedResult); + return removeExistingArrayItemAndVerifyResult( + yamlContent, + 'toplevel.second.third', + 'foo', + expectedResult + ); }); it('should remove the existing item under the multiple level object which you specify', () => { @@ -221,8 +242,12 @@ describe('#yamlAstParser', () => { }, }, }; - return removeExistingArrayItemAndVerifyResult(yamlContent, 'toplevel.second.third', - 'bar', expectedResult); + return removeExistingArrayItemAndVerifyResult( + yamlContent, + 'toplevel.second.third', + 'bar', + expectedResult + ); }); it('should remove multilevel object from the middle branch', () => { @@ -239,8 +264,12 @@ describe('#yamlAstParser', () => { service: 'test-service', end: 'end', }; - return removeExistingArrayItemAndVerifyResult(yamlContent, 'toplevel.second.third', - 'foo', expectedResult); + return removeExistingArrayItemAndVerifyResult( + yamlContent, + 'toplevel.second.third', + 'foo', + expectedResult + ); }); it('should remove item from multilevel object from the middle branch', () => { @@ -262,8 +291,12 @@ describe('#yamlAstParser', () => { }, end: 'end', }; - return removeExistingArrayItemAndVerifyResult(yamlContent, 'toplevel.second.third', - 'foo', expectedResult); + return removeExistingArrayItemAndVerifyResult( + yamlContent, + 'toplevel.second.third', + 'foo', + expectedResult + ); }); it('should do nothing when you can not find the object which you specify', () => { @@ -272,21 +305,22 @@ describe('#yamlAstParser', () => { toplevel: ['foo', 'bar'], }; - return removeExistingArrayItemAndVerifyResult(yamlContent, 'toplevel', - 'foo2', yamlContent); + return removeExistingArrayItemAndVerifyResult(yamlContent, 'toplevel', 'foo2', yamlContent); }); it('should remove when with inline declaration of the array', () => { - const yamlContent = - 'toplevel:\n' + - ' second: ["foo2", "bar"]'; + const yamlContent = 'toplevel:\n second: ["foo2", "bar"]'; const expectedResult = { toplevel: { second: ['foo2'], }, }; - return removeExistingArrayItemAndVerifyResult(yamlContent, 'toplevel.second', - 'bar', expectedResult); + return removeExistingArrayItemAndVerifyResult( + yamlContent, + 'toplevel.second', + 'bar', + expectedResult + ); }); }); }); diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 35e47c634..000000000 --- a/package-lock.json +++ /dev/null @@ -1,8777 +0,0 @@ -{ - "name": "serverless", - "version": "1.42.3", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@babel/code-frame": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", - "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", - "dev": true, - "requires": { - "@babel/highlight": "^7.0.0" - } - }, - "@babel/core": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.4.4.tgz", - "integrity": "sha512-lQgGX3FPRgbz2SKmhMtYgJvVzGZrmjaF4apZ2bLwofAKiSjxU0drPh4S/VasyYXwaTs+A1gvQ45BN8SQJzHsQQ==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/generator": "^7.4.4", - "@babel/helpers": "^7.4.4", - "@babel/parser": "^7.4.4", - "@babel/template": "^7.4.4", - "@babel/traverse": "^7.4.4", - "@babel/types": "^7.4.4", - "convert-source-map": "^1.1.0", - "debug": "^4.1.0", - "json5": "^2.1.0", - "lodash": "^4.17.11", - "resolve": "^1.3.2", - "semver": "^5.4.1", - "source-map": "^0.5.0" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - } - } - }, - "@babel/generator": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.4.4.tgz", - "integrity": "sha512-53UOLK6TVNqKxf7RUh8NE851EHRxOOeVXKbK2bivdb+iziMyk03Sr4eaE9OELCbyZAAafAKPDwF2TPUES5QbxQ==", - "dev": true, - "requires": { - "@babel/types": "^7.4.4", - "jsesc": "^2.5.1", - "lodash": "^4.17.11", - "source-map": "^0.5.0", - "trim-right": "^1.0.1" - } - }, - "@babel/helper-function-name": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz", - "integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.0.0", - "@babel/template": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz", - "integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==", - "dev": true, - "requires": { - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz", - "integrity": "sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA==", - "dev": true - }, - "@babel/helper-split-export-declaration": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz", - "integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==", - "dev": true, - "requires": { - "@babel/types": "^7.4.4" - } - }, - "@babel/helpers": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.4.4.tgz", - "integrity": "sha512-igczbR/0SeuPR8RFfC7tGrbdTbFL3QTvH6D+Z6zNxnTe//GyqmtHmDkzrqDmyZ3eSwPqB/LhyKoU5DXsp+Vp2A==", - "dev": true, - "requires": { - "@babel/template": "^7.4.4", - "@babel/traverse": "^7.4.4", - "@babel/types": "^7.4.4" - } - }, - "@babel/highlight": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", - "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", - "dev": true, - "requires": { - "chalk": "^2.0.0", - "esutils": "^2.0.2", - "js-tokens": "^4.0.0" - }, - "dependencies": { - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - } - } - }, - "@babel/parser": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.4.4.tgz", - "integrity": "sha512-5pCS4mOsL+ANsFZGdvNLybx4wtqAZJ0MJjMHxvzI3bvIsz6sQvzW8XX92EYIkiPtIvcfG3Aj+Ir5VNyjnZhP7w==", - "dev": true - }, - "@babel/plugin-syntax-object-rest-spread": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.2.0.tgz", - "integrity": "sha512-t0JKGgqk2We+9may3t0xDdmneaXmyxq0xieYcKHxIsrJO64n1OiMWNUtc5gQK1PA0NpdCRrtZp4z+IUaKugrSA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.4.4.tgz", - "integrity": "sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.4.4", - "@babel/types": "^7.4.4" - } - }, - "@babel/traverse": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.4.4.tgz", - "integrity": "sha512-Gw6qqkw/e6AGzlyj9KnkabJX7VcubqPtkUQVAwkc0wUMldr3A/hezNB3Rc5eIvId95iSGkGIOe5hh1kMKf951A==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/generator": "^7.4.4", - "@babel/helper-function-name": "^7.1.0", - "@babel/helper-split-export-declaration": "^7.4.4", - "@babel/parser": "^7.4.4", - "@babel/types": "^7.4.4", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.11" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true - } - } - }, - "@babel/types": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.4.4.tgz", - "integrity": "sha512-dOllgYdnEFOebhkKCjzSVFqw/PmmB8pH6RGOWkY4GsboQNd47b1fBThBSwlHAq9alF9vc1M3+6oqR47R50L0tQ==", - "dev": true, - "requires": { - "esutils": "^2.0.2", - "lodash": "^4.17.11", - "to-fast-properties": "^2.0.0" - } - }, - "@cnakazawa/watch": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.3.tgz", - "integrity": "sha512-r5160ogAvGyHsal38Kux7YYtodEKOj89RGb28ht1jh3SJb08VwRwAKKJL0bGb04Zd/3r9FL3BFIc3bBidYffCA==", - "dev": true, - "requires": { - "exec-sh": "^0.3.2", - "minimist": "^1.2.0" - } - }, - "@jest/console": { - "version": "24.7.1", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-24.7.1.tgz", - "integrity": "sha512-iNhtIy2M8bXlAOULWVTUxmnelTLFneTNEkHCgPmgd+zNwy9zVddJ6oS5rZ9iwoscNdT5mMwUd0C51v/fSlzItg==", - "dev": true, - "requires": { - "@jest/source-map": "^24.3.0", - "chalk": "^2.0.1", - "slash": "^2.0.0" - }, - "dependencies": { - "slash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", - "dev": true - } - } - }, - "@jest/core": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-24.8.0.tgz", - "integrity": "sha512-R9rhAJwCBQzaRnrRgAdVfnglUuATXdwTRsYqs6NMdVcAl5euG8LtWDe+fVkN27YfKVBW61IojVsXKaOmSnqd/A==", - "dev": true, - "requires": { - "@jest/console": "^24.7.1", - "@jest/reporters": "^24.8.0", - "@jest/test-result": "^24.8.0", - "@jest/transform": "^24.8.0", - "@jest/types": "^24.8.0", - "ansi-escapes": "^3.0.0", - "chalk": "^2.0.1", - "exit": "^0.1.2", - "graceful-fs": "^4.1.15", - "jest-changed-files": "^24.8.0", - "jest-config": "^24.8.0", - "jest-haste-map": "^24.8.0", - "jest-message-util": "^24.8.0", - "jest-regex-util": "^24.3.0", - "jest-resolve-dependencies": "^24.8.0", - "jest-runner": "^24.8.0", - "jest-runtime": "^24.8.0", - "jest-snapshot": "^24.8.0", - "jest-util": "^24.8.0", - "jest-validate": "^24.8.0", - "jest-watcher": "^24.8.0", - "micromatch": "^3.1.10", - "p-each-series": "^1.0.0", - "pirates": "^4.0.1", - "realpath-native": "^1.1.0", - "rimraf": "^2.5.4", - "strip-ansi": "^5.0.0" - }, - "dependencies": { - "ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", - "dev": true - } - } - }, - "@jest/environment": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-24.8.0.tgz", - "integrity": "sha512-vlGt2HLg7qM+vtBrSkjDxk9K0YtRBi7HfRFaDxoRtyi+DyVChzhF20duvpdAnKVBV6W5tym8jm0U9EfXbDk1tw==", - "dev": true, - "requires": { - "@jest/fake-timers": "^24.8.0", - "@jest/transform": "^24.8.0", - "@jest/types": "^24.8.0", - "jest-mock": "^24.8.0" - } - }, - "@jest/fake-timers": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-24.8.0.tgz", - "integrity": "sha512-2M4d5MufVXwi6VzZhJ9f5S/wU4ud2ck0kxPof1Iz3zWx6Y+V2eJrES9jEktB6O3o/oEyk+il/uNu9PvASjWXQw==", - "dev": true, - "requires": { - "@jest/types": "^24.8.0", - "jest-message-util": "^24.8.0", - "jest-mock": "^24.8.0" - } - }, - "@jest/reporters": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-24.8.0.tgz", - "integrity": "sha512-eZ9TyUYpyIIXfYCrw0UHUWUvE35vx5I92HGMgS93Pv7du+GHIzl+/vh8Qj9MCWFK/4TqyttVBPakWMOfZRIfxw==", - "dev": true, - "requires": { - "@jest/environment": "^24.8.0", - "@jest/test-result": "^24.8.0", - "@jest/transform": "^24.8.0", - "@jest/types": "^24.8.0", - "chalk": "^2.0.1", - "exit": "^0.1.2", - "glob": "^7.1.2", - "istanbul-lib-coverage": "^2.0.2", - "istanbul-lib-instrument": "^3.0.1", - "istanbul-lib-report": "^2.0.4", - "istanbul-lib-source-maps": "^3.0.1", - "istanbul-reports": "^2.1.1", - "jest-haste-map": "^24.8.0", - "jest-resolve": "^24.8.0", - "jest-runtime": "^24.8.0", - "jest-util": "^24.8.0", - "jest-worker": "^24.6.0", - "node-notifier": "^5.2.1", - "slash": "^2.0.0", - "source-map": "^0.6.0", - "string-length": "^2.0.0" - }, - "dependencies": { - "slash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "@jest/source-map": { - "version": "24.3.0", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-24.3.0.tgz", - "integrity": "sha512-zALZt1t2ou8le/crCeeiRYzvdnTzaIlpOWaet45lNSqNJUnXbppUUFR4ZUAlzgDmKee4Q5P/tKXypI1RiHwgag==", - "dev": true, - "requires": { - "callsites": "^3.0.0", - "graceful-fs": "^4.1.15", - "source-map": "^0.6.0" - }, - "dependencies": { - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "@jest/test-result": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-24.8.0.tgz", - "integrity": "sha512-+YdLlxwizlfqkFDh7Mc7ONPQAhA4YylU1s529vVM1rsf67vGZH/2GGm5uO8QzPeVyaVMobCQ7FTxl38QrKRlng==", - "dev": true, - "requires": { - "@jest/console": "^24.7.1", - "@jest/types": "^24.8.0", - "@types/istanbul-lib-coverage": "^2.0.0" - } - }, - "@jest/test-sequencer": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-24.8.0.tgz", - "integrity": "sha512-OzL/2yHyPdCHXEzhoBuq37CE99nkme15eHkAzXRVqthreWZamEMA0WoetwstsQBCXABhczpK03JNbc4L01vvLg==", - "dev": true, - "requires": { - "@jest/test-result": "^24.8.0", - "jest-haste-map": "^24.8.0", - "jest-runner": "^24.8.0", - "jest-runtime": "^24.8.0" - } - }, - "@jest/transform": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-24.8.0.tgz", - "integrity": "sha512-xBMfFUP7TortCs0O+Xtez2W7Zu1PLH9bvJgtraN1CDST6LBM/eTOZ9SfwS/lvV8yOfcDpFmwf9bq5cYbXvqsvA==", - "dev": true, - "requires": { - "@babel/core": "^7.1.0", - "@jest/types": "^24.8.0", - "babel-plugin-istanbul": "^5.1.0", - "chalk": "^2.0.1", - "convert-source-map": "^1.4.0", - "fast-json-stable-stringify": "^2.0.0", - "graceful-fs": "^4.1.15", - "jest-haste-map": "^24.8.0", - "jest-regex-util": "^24.3.0", - "jest-util": "^24.8.0", - "micromatch": "^3.1.10", - "realpath-native": "^1.1.0", - "slash": "^2.0.0", - "source-map": "^0.6.1", - "write-file-atomic": "2.4.1" - }, - "dependencies": { - "slash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "write-file-atomic": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.1.tgz", - "integrity": "sha512-TGHFeZEZMnv+gBFRfjAcxL5bPHrsGKtnb4qsFAws7/vlh+QfwAaySIw4AXP9ZskTTh5GWu3FLuJhsWVdiJPGvg==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.2" - } - } - } - }, - "@jest/types": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.8.0.tgz", - "integrity": "sha512-g17UxVr2YfBtaMUxn9u/4+siG1ptg9IGYAYwvpwn61nBg779RXnjE/m7CxYcIzEt0AbHZZAHSEZNhkE2WxURVg==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^1.1.1", - "@types/yargs": "^12.0.9" - } - }, - "@types/babel__core": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.1.tgz", - "integrity": "sha512-+hjBtgcFPYyCTo0A15+nxrCVJL7aC6Acg87TXd5OW3QhHswdrOLoles+ldL2Uk8q++7yIfl4tURtztccdeeyOw==", - "dev": true, - "requires": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "@types/babel__generator": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.0.2.tgz", - "integrity": "sha512-NHcOfab3Zw4q5sEE2COkpfXjoE7o+PmqD9DQW4koUT3roNxwziUdXGnRndMat/LJNUtePwn1TlP4do3uoe3KZQ==", - "dev": true, - "requires": { - "@babel/types": "^7.0.0" - } - }, - "@types/babel__template": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.0.2.tgz", - "integrity": "sha512-/K6zCpeW7Imzgab2bLkLEbz0+1JlFSrUMdw7KoIIu+IUdu51GWaBZpd3y1VXGVXzynvGa4DaIaxNZHiON3GXUg==", - "dev": true, - "requires": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "@types/babel__traverse": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.0.6.tgz", - "integrity": "sha512-XYVgHF2sQ0YblLRMLNPB3CkFMewzFmlDsH/TneZFHUXDlABQgh88uOxuez7ZcXxayLFrqLwtDH1t+FmlFwNZxw==", - "dev": true, - "requires": { - "@babel/types": "^7.3.0" - } - }, - "@types/istanbul-lib-coverage": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz", - "integrity": "sha512-hRJD2ahnnpLgsj6KWMYSrmXkM3rm2Dl1qkx6IOFD5FnuNPXJIG5L0dhgKXCYTRMGzU4n0wImQ/xfmRc4POUFlg==", - "dev": true - }, - "@types/istanbul-lib-report": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-1.1.1.tgz", - "integrity": "sha512-3BUTyMzbZa2DtDI2BkERNC6jJw2Mr2Y0oGI7mRxYNBPxppbtEK1F66u3bKwU2g+wxwWI7PAoRpJnOY1grJqzHg==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "*" - } - }, - "@types/istanbul-reports": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.1.tgz", - "integrity": "sha512-UpYjBi8xefVChsCoBpKShdxTllC9pwISirfoZsUa2AAdQg/Jd2KQGtSbw+ya7GPo7x/wAPlH6JBhKhAsXUEZNA==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "*", - "@types/istanbul-lib-report": "*" - } - }, - "@types/stack-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", - "integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==", - "dev": true - }, - "@types/yargs": { - "version": "12.0.12", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-12.0.12.tgz", - "integrity": "sha512-SOhuU4wNBxhhTHxYaiG5NY4HBhDIDnJF60GU+2LqHAdKKer86//e4yg69aENCtQ04n0ovz+tq2YPME5t5yp4pw==", - "dev": true - }, - "abab": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.0.tgz", - "integrity": "sha512-sY5AXXVZv4Y1VACTtR11UJCPHHudgY5i26Qj5TypE6DKlIApbwb5uqhXcJ5UUGbvZNRh7EeIoW+LrJumBsKp7w==", - "dev": true - }, - "abbrev": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", - "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=", - "dev": true - }, - "acorn": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", - "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==", - "dev": true - }, - "acorn-globals": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.2.tgz", - "integrity": "sha512-BbzvZhVtZP+Bs1J1HcwrQe8ycfO0wStkSGxuul3He3GkHOIZ6eTqOkPuw9IP1X3+IkOo4wiJmwkobzXYz4wewQ==", - "dev": true, - "requires": { - "acorn": "^6.0.1", - "acorn-walk": "^6.0.1" - }, - "dependencies": { - "acorn": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.1.tgz", - "integrity": "sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA==", - "dev": true - } - } - }, - "acorn-jsx": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", - "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", - "dev": true, - "requires": { - "acorn": "^3.0.4" - }, - "dependencies": { - "acorn": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", - "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", - "dev": true - } - } - }, - "acorn-walk": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.1.1.tgz", - "integrity": "sha512-OtUw6JUTgxA2QoqqmrmQ7F2NYqiBPi/L2jqHyFtllhOUvXYQXf0Z1CYUinIfyT4bTCGmrA7gX9FvHA81uzCoVw==", - "dev": true - }, - "agent-base": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz", - "integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==", - "requires": { - "es6-promisify": "^5.0.0" - } - }, - "ajv": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", - "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", - "dev": true, - "requires": { - "fast-deep-equal": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "dependencies": { - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true - }, - "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - } - } - }, - "ajv-keywords": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-1.5.1.tgz", - "integrity": "sha1-MU3QpLM2j609/NxU7eYXG4htrzw=", - "dev": true - }, - "amdefine": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", - "dev": true, - "optional": true - }, - "ansi": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/ansi/-/ansi-0.3.1.tgz", - "integrity": "sha1-DELU+xcWDVqa8eSEus4cZpIsGyE=" - }, - "ansi-align": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz", - "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=", - "requires": { - "string-width": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "ansi-escapes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", - "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=" - }, - "ansi-red": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-red/-/ansi-red-0.1.1.tgz", - "integrity": "sha1-jGOPnRCAgAo1PJwoyKgcpHBdlGw=", - "dev": true, - "requires": { - "ansi-wrap": "0.1.0" - } - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "ansi-wrap": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz", - "integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=", - "dev": true - }, - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - } - }, - "archiver": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-1.3.0.tgz", - "integrity": "sha1-TyGU1tj5nfP1MeaIHxTxXVX6ryI=", - "requires": { - "archiver-utils": "^1.3.0", - "async": "^2.0.0", - "buffer-crc32": "^0.2.1", - "glob": "^7.0.0", - "lodash": "^4.8.0", - "readable-stream": "^2.0.0", - "tar-stream": "^1.5.0", - "walkdir": "^0.0.11", - "zip-stream": "^1.1.0" - }, - "dependencies": { - "async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.2.tgz", - "integrity": "sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg==", - "requires": { - "lodash": "^4.17.11" - } - } - } - }, - "archiver-utils": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-1.3.0.tgz", - "integrity": "sha1-5QtMCccL89aA4y/xt5lOn52JUXQ=", - "requires": { - "glob": "^7.0.0", - "graceful-fs": "^4.1.0", - "lazystream": "^1.0.0", - "lodash": "^4.8.0", - "normalize-path": "^2.0.0", - "readable-stream": "^2.0.0" - } - }, - "are-we-there-yet": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", - "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "dev": true - }, - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" - }, - "array-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", - "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", - "dev": true - }, - "array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", - "requires": { - "array-uniq": "^1.0.1" - } - }, - "array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=" - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" - }, - "array.prototype.find": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/array.prototype.find/-/array.prototype.find-2.0.4.tgz", - "integrity": "sha1-VWpcU2LAhkgyPdrrnenRS8GGTJA=", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.7.0" - } - }, - "asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", - "dev": true - }, - "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "dev": true, - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - }, - "assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true - }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" - }, - "astral-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", - "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", - "dev": true - }, - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" - }, - "async-limiter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", - "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==", - "dev": true - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" - }, - "atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" - }, - "autolinker": { - "version": "0.15.3", - "resolved": "https://registry.npmjs.org/autolinker/-/autolinker-0.15.3.tgz", - "integrity": "sha1-NCQX2PLzRhsUzwkIjV7fh5HcmDI=", - "dev": true - }, - "aws-sdk": { - "version": "2.454.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.454.0.tgz", - "integrity": "sha512-1vB9DNIwh+mqKD2IZspYTQapCD6f5VnMT5V2VPlXJ1CNcUdFSU8FFyxKmYApNs+S3re1h3fhWDjpwTreS+XLRQ==", - "requires": { - "buffer": "4.9.1", - "events": "1.1.1", - "ieee754": "1.1.8", - "jmespath": "0.15.0", - "querystring": "0.2.0", - "sax": "1.2.1", - "url": "0.10.3", - "uuid": "3.3.2", - "xml2js": "0.4.19" - }, - "dependencies": { - "buffer": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", - "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", - "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4", - "isarray": "^1.0.0" - } - }, - "ieee754": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", - "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=" - }, - "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" - } - } - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "dev": true - }, - "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", - "dev": true - }, - "babel-code-frame": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", - "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "esutils": "^2.0.2", - "js-tokens": "^3.0.2" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } - }, - "babel-jest": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-24.8.0.tgz", - "integrity": "sha512-+5/kaZt4I9efoXzPlZASyK/lN9qdRKmmUav9smVc0ruPQD7IsfucQ87gpOE8mn2jbDuS6M/YOW6n3v9ZoIfgnw==", - "dev": true, - "requires": { - "@jest/transform": "^24.8.0", - "@jest/types": "^24.8.0", - "@types/babel__core": "^7.1.0", - "babel-plugin-istanbul": "^5.1.0", - "babel-preset-jest": "^24.6.0", - "chalk": "^2.4.2", - "slash": "^2.0.0" - }, - "dependencies": { - "slash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", - "dev": true - } - } - }, - "babel-plugin-istanbul": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-5.1.4.tgz", - "integrity": "sha512-dySz4VJMH+dpndj0wjJ8JPs/7i1TdSPb1nRrn56/92pKOF9VKC1FMFJmMXjzlGGusnCAqujP6PBCiKq0sVA+YQ==", - "dev": true, - "requires": { - "find-up": "^3.0.0", - "istanbul-lib-instrument": "^3.3.0", - "test-exclude": "^5.2.3" - }, - "dependencies": { - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - } - } - }, - "babel-plugin-jest-hoist": { - "version": "24.6.0", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-24.6.0.tgz", - "integrity": "sha512-3pKNH6hMt9SbOv0F3WVmy5CWQ4uogS3k0GY5XLyQHJ9EGpAT9XWkFd2ZiXXtkwFHdAHa5j7w7kfxSP5lAIwu7w==", - "dev": true, - "requires": { - "@types/babel__traverse": "^7.0.6" - } - }, - "babel-preset-jest": { - "version": "24.6.0", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-24.6.0.tgz", - "integrity": "sha512-pdZqLEdmy1ZK5kyRUfvBb2IfTPb2BUvIJczlPspS8fWmBQslNNDBqVfh7BW5leOVJMDZKzjD8XEyABTk6gQ5yw==", - "dev": true, - "requires": { - "@babel/plugin-syntax-object-rest-spread": "^7.0.0", - "babel-plugin-jest-hoist": "^24.6.0" - } - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "requires": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "^1.0.0" - } - } - } - }, - "base64-js": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", - "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "dev": true, - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "bl": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", - "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", - "requires": { - "readable-stream": "^2.3.5", - "safe-buffer": "^5.1.1" - } - }, - "bluebird": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.4.tgz", - "integrity": "sha512-FG+nFEZChJrbQ9tIccIfZJBz3J7mLrAhxakAbnrJWn8d7aKOC+LWifa0G+p4ZqKp4y13T7juYvdhq9NzKdsrjw==" - }, - "boxen": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", - "integrity": "sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==", - "requires": { - "ansi-align": "^2.0.0", - "camelcase": "^4.0.0", - "chalk": "^2.0.1", - "cli-boxes": "^1.0.0", - "string-width": "^2.0.0", - "term-size": "^1.2.0", - "widest-line": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true - } - } - }, - "browser-process-hrtime": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz", - "integrity": "sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw==", - "dev": true - }, - "browser-resolve": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz", - "integrity": "sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==", - "dev": true, - "requires": { - "resolve": "1.1.7" - }, - "dependencies": { - "resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", - "dev": true - } - } - }, - "browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true - }, - "bser": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.0.0.tgz", - "integrity": "sha1-mseNPtXZFYBP2HrLFYvHlxR6Fxk=", - "dev": true, - "requires": { - "node-int64": "^0.4.0" - } - }, - "buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz", - "integrity": "sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==", - "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4" - } - }, - "buffer-alloc": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", - "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", - "requires": { - "buffer-alloc-unsafe": "^1.1.0", - "buffer-fill": "^1.0.0" - } - }, - "buffer-alloc-unsafe": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", - "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==" - }, - "buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" - }, - "buffer-fill": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", - "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=" - }, - "buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" - }, - "builtin-modules": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", - "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", - "dev": true - }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "requires": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - } - }, - "cachedir": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.2.0.tgz", - "integrity": "sha512-VvxA0xhNqIIfg0V9AmJkDg91DaJwryutH5rVEZAhcNi4iJFj9f+QxmAjgK1LT9I8OgToX27fypX6/MeCXVbBjQ==" - }, - "caller-id": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/caller-id/-/caller-id-0.1.0.tgz", - "integrity": "sha1-Wb2sCJPRLDhxQIJ5Ix+XRYNk8Hs=", - "dev": true, - "requires": { - "stack-trace": "~0.0.7" - } - }, - "caller-path": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", - "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", - "dev": true, - "requires": { - "callsites": "^0.2.0" - } - }, - "callsites": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", - "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", - "dev": true - }, - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" - }, - "capture-exit": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", - "integrity": "sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==", - "dev": true, - "requires": { - "rsvp": "^4.8.4" - } - }, - "capture-stack-trace": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz", - "integrity": "sha512-mYQLZnx5Qt1JgB1WEiMCf2647plpGeQ2NMR/5L0HNZzGQo4fuSPnK+wjfPnKZV0aiJDgzmWqqkV/g7JD+DW0qw==" - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", - "dev": true - }, - "caw": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/caw/-/caw-2.0.1.tgz", - "integrity": "sha512-Cg8/ZSBEa8ZVY9HspcGUYaK63d/bN7rqS3CYCzEGUxuYv6UlmcjzDUz2fCFFHyTvUW5Pk0I+3hkA3iXlIj6guA==", - "requires": { - "get-proxy": "^2.0.0", - "isurl": "^1.0.0-alpha5", - "tunnel-agent": "^0.6.0", - "url-to-options": "^1.0.1" - } - }, - "chai": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-3.5.0.tgz", - "integrity": "sha1-TQJjewZ/6Vi9v906QOxW/vc3Mkc=", - "dev": true, - "requires": { - "assertion-error": "^1.0.1", - "deep-eql": "^0.1.3", - "type-detect": "^1.0.0" - } - }, - "chai-as-promised": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-6.0.0.tgz", - "integrity": "sha1-GgKkM6byTa+sY7nJb6FoTbGqjaY=", - "dev": true, - "requires": { - "check-error": "^1.0.2" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", - "dev": true - }, - "ci-info": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", - "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==" - }, - "circular-json": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", - "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", - "dev": true - }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" - } - } - }, - "cli-boxes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", - "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=" - }, - "cli-cursor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", - "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", - "requires": { - "restore-cursor": "^1.0.1" - } - }, - "cli-width": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", - "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=" - }, - "cliui": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", - "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", - "dev": true, - "requires": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0", - "wrap-ansi": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", - "dev": true - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" - }, - "coffee-script": { - "version": "1.12.7", - "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.12.7.tgz", - "integrity": "sha512-fLeEhqwymYat/MpTPUjSKHVYYl0ec2mOyALEMLmzr5i1isuG+6jfI2j2d5oBO3VIzgUXgBVIcOT9uH1TFxBckw==", - "dev": true - }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "commander": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz", - "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=", - "requires": { - "graceful-readlink": ">= 1.0.0" - } - }, - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" - }, - "compress-commons": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-1.2.2.tgz", - "integrity": "sha1-UkqfEJA/OoEzibAiXSfEi7dRiQ8=", - "requires": { - "buffer-crc32": "^0.2.1", - "crc32-stream": "^2.0.0", - "normalize-path": "^2.0.0", - "readable-stream": "^2.0.0" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "config-chain": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz", - "integrity": "sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==", - "requires": { - "ini": "^1.3.4", - "proto-list": "~1.2.1" - } - }, - "configstore": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.2.tgz", - "integrity": "sha512-vtv5HtGjcYUgFrXc6Kx747B83MRRVS5R1VTEQoXvuP+kMI+if6uywV0nDGoiydJRy4yk7h9od5Og0kxx4zUXmw==", - "requires": { - "dot-prop": "^4.1.0", - "graceful-fs": "^4.1.2", - "make-dir": "^1.0.0", - "unique-string": "^1.0.0", - "write-file-atomic": "^2.0.0", - "xdg-basedir": "^3.0.0" - } - }, - "contains-path": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", - "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", - "dev": true - }, - "convert-source-map": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", - "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.1" - } - }, - "cookie": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", - "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" - }, - "cookiejar": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", - "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==" - }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "coveralls": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/coveralls/-/coveralls-3.0.3.tgz", - "integrity": "sha512-viNfeGlda2zJr8Gj1zqXpDMRjw9uM54p7wzZdvLRyOgnAfCe974Dq4veZkjJdxQXbmdppu6flEajFYseHYaUhg==", - "dev": true, - "requires": { - "growl": "~> 1.10.0", - "js-yaml": "^3.11.0", - "lcov-parse": "^0.0.10", - "log-driver": "^1.2.7", - "minimist": "^1.2.0", - "request": "^2.86.0" - } - }, - "crc": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", - "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", - "requires": { - "buffer": "^5.1.0" - } - }, - "crc32-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-2.0.0.tgz", - "integrity": "sha1-483TtN8xaN10494/u8t7KX/pCPQ=", - "requires": { - "crc": "^3.4.4", - "readable-stream": "^2.0.0" - } - }, - "create-error-class": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", - "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", - "requires": { - "capture-stack-trace": "^1.0.0" - } - }, - "cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", - "requires": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "crypto-random-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", - "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=" - }, - "cssom": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.6.tgz", - "integrity": "sha512-DtUeseGk9/GBW0hl0vVPpU22iHL6YB5BUX7ml1hB+GMpo0NX5G4voX3kdWiMSEguFtcW3Vh3djqNF4aIe6ne0A==", - "dev": true - }, - "cssstyle": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.2.2.tgz", - "integrity": "sha512-43wY3kl1CVQSvL7wUY1qXkxVGkStjpkDmVjiIKX8R97uhajy8Bybay78uOtqvh7Q5GK75dNPfW0geWjE6qQQow==", - "dev": true, - "requires": { - "cssom": "0.3.x" - } - }, - "d": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", - "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", - "dev": true, - "requires": { - "es5-ext": "^0.10.9" - } - }, - "damerau-levenshtein": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.5.tgz", - "integrity": "sha512-CBCRqFnpu715iPmw1KrdOrzRqbdFwQTwAWyyyYS42+iAgHCuXZ+/TdMgQkUENPomxEz9z1BEzuQU2Xw0kUuAgA==", - "dev": true - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "data-urls": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz", - "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==", - "dev": true, - "requires": { - "abab": "^2.0.0", - "whatwg-mimetype": "^2.2.0", - "whatwg-url": "^7.0.0" - }, - "dependencies": { - "whatwg-url": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.0.0.tgz", - "integrity": "sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ==", - "dev": true, - "requires": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - } - } - }, - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "requires": { - "ms": "^2.1.1" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true - }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" - }, - "decompress": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.0.tgz", - "integrity": "sha1-eu3YVCflqS2s/lVnSnxQXpbQH50=", - "requires": { - "decompress-tar": "^4.0.0", - "decompress-tarbz2": "^4.0.0", - "decompress-targz": "^4.0.0", - "decompress-unzip": "^4.0.1", - "graceful-fs": "^4.1.10", - "make-dir": "^1.0.0", - "pify": "^2.3.0", - "strip-dirs": "^2.0.0" - } - }, - "decompress-tar": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", - "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==", - "requires": { - "file-type": "^5.2.0", - "is-stream": "^1.1.0", - "tar-stream": "^1.5.2" - } - }, - "decompress-tarbz2": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", - "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", - "requires": { - "decompress-tar": "^4.1.0", - "file-type": "^6.1.0", - "is-stream": "^1.1.0", - "seek-bzip": "^1.0.5", - "unbzip2-stream": "^1.0.9" - }, - "dependencies": { - "file-type": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", - "integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==" - } - } - }, - "decompress-targz": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", - "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==", - "requires": { - "decompress-tar": "^4.1.1", - "file-type": "^5.2.0", - "is-stream": "^1.1.0" - } - }, - "decompress-unzip": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", - "integrity": "sha1-3qrM39FK6vhVePczroIQ+bSEj2k=", - "requires": { - "file-type": "^3.8.0", - "get-stream": "^2.2.0", - "pify": "^2.3.0", - "yauzl": "^2.4.2" - }, - "dependencies": { - "file-type": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", - "integrity": "sha1-JXoHg4TR24CHvESdEH1SpSZyuek=" - }, - "get-stream": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", - "integrity": "sha1-Xzj5PzRgCWZu4BUKBUFn+Rvdld4=", - "requires": { - "object-assign": "^4.0.1", - "pinkie-promise": "^2.0.0" - } - } - } - }, - "deep-eql": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", - "integrity": "sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=", - "dev": true, - "requires": { - "type-detect": "0.1.1" - }, - "dependencies": { - "type-detect": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz", - "integrity": "sha1-C6XsKohWQORw6k6FBZcZANrFiCI=", - "dev": true - } - } - }, - "deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" - }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", - "dev": true - }, - "deepmerge": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-1.5.2.tgz", - "integrity": "sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ==", - "dev": true - }, - "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, - "requires": { - "object-keys": "^1.0.12" - } - }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" - }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" - }, - "detect-newline": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", - "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=", - "dev": true - }, - "diacritics-map": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/diacritics-map/-/diacritics-map-0.1.0.tgz", - "integrity": "sha1-bfwP+dAQAKLt8oZTccrDFulJd68=", - "dev": true - }, - "diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", - "dev": true - }, - "diff-sequences": { - "version": "24.3.0", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-24.3.0.tgz", - "integrity": "sha512-xLqpez+Zj9GKSnPWS0WZw1igGocZ+uua8+y+5dDNTT934N3QuY1sp2LkHzwiaYQGz60hMq0pjAshdeXm5VUOEw==", - "dev": true - }, - "doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "domexception": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", - "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", - "dev": true, - "requires": { - "webidl-conversions": "^4.0.2" - } - }, - "dot-prop": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", - "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", - "requires": { - "is-obj": "^1.0.0" - } - }, - "download": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/download/-/download-5.0.3.tgz", - "integrity": "sha1-Y1N/l3+ZJmow64oqL70fILgAD3o=", - "requires": { - "caw": "^2.0.0", - "decompress": "^4.0.0", - "filenamify": "^2.0.0", - "get-stream": "^3.0.0", - "got": "^6.3.0", - "mkdirp": "^0.5.1", - "pify": "^2.3.0" - } - }, - "duplexer3": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", - "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=" - }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "dev": true, - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "encoding": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", - "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", - "requires": { - "iconv-lite": "~0.4.13" - } - }, - "end-of-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", - "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", - "requires": { - "once": "^1.4.0" - } - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "es-abstract": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz", - "integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==", - "dev": true, - "requires": { - "es-to-primitive": "^1.2.0", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "is-callable": "^1.1.4", - "is-regex": "^1.0.4", - "object-keys": "^1.0.12" - } - }, - "es-to-primitive": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", - "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", - "dev": true, - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } - }, - "es5-ext": { - "version": "0.10.50", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.50.tgz", - "integrity": "sha512-KMzZTPBkeQV/JcSQhI5/z6d9VWJ3EnQ194USTUwIYZ2ZbpN8+SGXQKt1h68EX44+qt+Fzr8DO17vnxrw7c3agw==", - "dev": true, - "requires": { - "es6-iterator": "~2.0.3", - "es6-symbol": "~3.1.1", - "next-tick": "^1.0.0" - } - }, - "es6-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" - } - }, - "es6-map": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz", - "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "~0.10.14", - "es6-iterator": "~2.0.1", - "es6-set": "~0.1.5", - "es6-symbol": "~3.1.1", - "event-emitter": "~0.3.5" - } - }, - "es6-promise": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.6.tgz", - "integrity": "sha512-aRVgGdnmW2OiySVPUC9e6m+plolMAJKjZnQlCwNSuK5yQ0JN61DZSO1X1Ufd1foqWRAlig0rhduTCHe7sVtK5Q==" - }, - "es6-promisify": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", - "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", - "requires": { - "es6-promise": "^4.0.3" - } - }, - "es6-set": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", - "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "~0.10.14", - "es6-iterator": "~2.0.1", - "es6-symbol": "3.1.1", - "event-emitter": "~0.3.5" - } - }, - "es6-symbol": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", - "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "~0.10.14" - } - }, - "es6-weak-map": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.2.tgz", - "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "^0.10.14", - "es6-iterator": "^2.0.1", - "es6-symbol": "^3.1.1" - } - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" - }, - "escodegen": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", - "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", - "dev": true, - "requires": { - "esprima": "^2.7.1", - "estraverse": "^1.9.1", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.2.0" - }, - "dependencies": { - "esprima": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", - "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", - "dev": true - }, - "estraverse": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", - "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=", - "dev": true - }, - "source-map": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", - "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", - "dev": true, - "optional": true, - "requires": { - "amdefine": ">=0.0.4" - } - } - } - }, - "escope": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz", - "integrity": "sha1-4Bl16BJ4GhY6ba392AOY3GTIicM=", - "dev": true, - "requires": { - "es6-map": "^0.1.3", - "es6-weak-map": "^2.0.1", - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - } - }, - "eslint": { - "version": "3.19.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-3.19.0.tgz", - "integrity": "sha1-yPxiAcf0DdCJQbh8CFdnOGpnmsw=", - "dev": true, - "requires": { - "babel-code-frame": "^6.16.0", - "chalk": "^1.1.3", - "concat-stream": "^1.5.2", - "debug": "^2.1.1", - "doctrine": "^2.0.0", - "escope": "^3.6.0", - "espree": "^3.4.0", - "esquery": "^1.0.0", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "file-entry-cache": "^2.0.0", - "glob": "^7.0.3", - "globals": "^9.14.0", - "ignore": "^3.2.0", - "imurmurhash": "^0.1.4", - "inquirer": "^0.12.0", - "is-my-json-valid": "^2.10.0", - "is-resolvable": "^1.0.0", - "js-yaml": "^3.5.1", - "json-stable-stringify": "^1.0.0", - "levn": "^0.3.0", - "lodash": "^4.0.0", - "mkdirp": "^0.5.0", - "natural-compare": "^1.4.0", - "optionator": "^0.8.2", - "path-is-inside": "^1.0.1", - "pluralize": "^1.2.1", - "progress": "^1.1.8", - "require-uncached": "^1.0.2", - "shelljs": "^0.7.5", - "strip-bom": "^3.0.0", - "strip-json-comments": "~2.0.1", - "table": "^3.7.8", - "text-table": "~0.2.0", - "user-home": "^2.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "inquirer": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-0.12.0.tgz", - "integrity": "sha1-HvK/1jUE3wvHV4X/+MLEHfEvB34=", - "dev": true, - "requires": { - "ansi-escapes": "^1.1.0", - "ansi-regex": "^2.0.0", - "chalk": "^1.0.0", - "cli-cursor": "^1.0.1", - "cli-width": "^2.0.0", - "figures": "^1.3.5", - "lodash": "^4.3.0", - "readline2": "^1.0.1", - "run-async": "^0.1.0", - "rx-lite": "^3.1.2", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.0", - "through": "^2.3.6" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "run-async": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz", - "integrity": "sha1-yK1KXhEGYeQCp9IbUw4AnyX444k=", - "dev": true, - "requires": { - "once": "^1.3.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } - }, - "eslint-config-airbnb": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-10.0.1.tgz", - "integrity": "sha1-pHAQhkbWxF4fY5oD8R1QShqkrtw=", - "dev": true, - "requires": { - "eslint-config-airbnb-base": "^5.0.2" - } - }, - "eslint-config-airbnb-base": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-5.0.3.tgz", - "integrity": "sha1-lxSsNews1/qw1E0Uip+R2ylEB00=", - "dev": true - }, - "eslint-import-resolver-node": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.2.3.tgz", - "integrity": "sha1-Wt2BBujJKNssuiMrzZ76hG49oWw=", - "dev": true, - "requires": { - "debug": "^2.2.0", - "object-assign": "^4.0.1", - "resolve": "^1.1.6" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "eslint-plugin-import": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-1.16.0.tgz", - "integrity": "sha1-svoH68xTUE0PKkR3WC7Iv/GHG58=", - "dev": true, - "requires": { - "builtin-modules": "^1.1.1", - "contains-path": "^0.1.0", - "debug": "^2.2.0", - "doctrine": "1.3.x", - "es6-map": "^0.1.3", - "es6-set": "^0.1.4", - "eslint-import-resolver-node": "^0.2.0", - "has": "^1.0.1", - "lodash.cond": "^4.3.0", - "lodash.endswith": "^4.0.1", - "lodash.find": "^4.3.0", - "lodash.findindex": "^4.3.0", - "minimatch": "^3.0.3", - "object-assign": "^4.0.1", - "pkg-dir": "^1.0.0", - "pkg-up": "^1.0.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "doctrine": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.3.0.tgz", - "integrity": "sha1-E+dWgrVVGEJCdvfBc3g0Vu+RPSY=", - "dev": true, - "requires": { - "esutils": "^2.0.2", - "isarray": "^1.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "eslint-plugin-jsx-a11y": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-2.2.3.tgz", - "integrity": "sha1-TjXLcbin23AqxBXIBuuOjZ6mxl0=", - "dev": true, - "requires": { - "damerau-levenshtein": "^1.0.0", - "jsx-ast-utils": "^1.0.0", - "object-assign": "^4.0.1" - } - }, - "eslint-plugin-react": { - "version": "6.10.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-6.10.3.tgz", - "integrity": "sha1-xUNb6wZ3ThLH2y9qut3L+QDNP3g=", - "dev": true, - "requires": { - "array.prototype.find": "^2.0.1", - "doctrine": "^1.2.2", - "has": "^1.0.1", - "jsx-ast-utils": "^1.3.4", - "object.assign": "^4.0.4" - }, - "dependencies": { - "doctrine": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", - "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", - "dev": true, - "requires": { - "esutils": "^2.0.2", - "isarray": "^1.0.0" - } - } - } - }, - "espree": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.4.tgz", - "integrity": "sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==", - "dev": true, - "requires": { - "acorn": "^5.5.0", - "acorn-jsx": "^3.0.0" - } - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" - }, - "esquery": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", - "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", - "dev": true, - "requires": { - "estraverse": "^4.0.0" - } - }, - "esrecurse": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", - "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", - "dev": true, - "requires": { - "estraverse": "^4.1.0" - } - }, - "estraverse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", - "dev": true - }, - "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", - "dev": true - }, - "event-emitter": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", - "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "~0.10.14" - } - }, - "events": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", - "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" - }, - "exec-sh": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.2.tgz", - "integrity": "sha512-9sLAvzhI5nc8TpuQUh4ahMdCrWT00wPWz7j47/emR5+2qEfoZP5zzUXvx+vdx+H6ohhnsYC31iX04QLYJK8zTg==", - "dev": true - }, - "execa": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", - "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", - "requires": { - "cross-spawn": "^5.0.1", - "get-stream": "^3.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", - "dev": true - }, - "exit-hook": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", - "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=" - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "expand-range": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", - "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", - "dev": true, - "requires": { - "fill-range": "^2.1.0" - }, - "dependencies": { - "fill-range": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", - "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", - "dev": true, - "requires": { - "is-number": "^2.1.0", - "isobject": "^2.0.0", - "randomatic": "^3.0.0", - "repeat-element": "^1.1.2", - "repeat-string": "^1.5.2" - } - }, - "is-number": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", - "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } - }, - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "expect": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-24.8.0.tgz", - "integrity": "sha512-/zYvP8iMDrzaaxHVa724eJBCKqSHmO0FA7EDkBiRHxg6OipmMn1fN+C8T9L9K8yr7UONkOifu6+LLH+z76CnaA==", - "dev": true, - "requires": { - "@jest/types": "^24.8.0", - "ansi-styles": "^3.2.0", - "jest-get-type": "^24.8.0", - "jest-matcher-utils": "^24.8.0", - "jest-message-util": "^24.8.0", - "jest-regex-util": "^24.3.0" - } - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - } - }, - "external-editor": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-1.1.1.tgz", - "integrity": "sha1-Etew24UPf/fnCBuvQAVwAGDEYAs=", - "requires": { - "extend": "^3.0.0", - "spawn-sync": "^1.0.15", - "tmp": "^0.0.29" - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true - } - } - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "dev": true - }, - "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", - "dev": true - }, - "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" - }, - "fb-watchman": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.0.tgz", - "integrity": "sha1-VOmr99+i8mzZsWNsWIwa/AXeXVg=", - "dev": true, - "requires": { - "bser": "^2.0.0" - } - }, - "fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", - "requires": { - "pend": "~1.2.0" - } - }, - "figures": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", - "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", - "requires": { - "escape-string-regexp": "^1.0.5", - "object-assign": "^4.1.0" - } - }, - "file-entry-cache": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", - "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", - "dev": true, - "requires": { - "flat-cache": "^1.2.1", - "object-assign": "^4.0.1" - } - }, - "file-type": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", - "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=" - }, - "filename-reserved-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", - "integrity": "sha1-q/c9+rc10EVECr/qLZHzieu/oik=" - }, - "filenamify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-2.1.0.tgz", - "integrity": "sha512-ICw7NTT6RsDp2rnYKVd8Fu4cr6ITzGy3+u4vUujPkabyaz+03F24NWEX7fs5fp+kBonlaqPH8fAO2NM+SXt/JA==", - "requires": { - "filename-reserved-regex": "^2.0.0", - "strip-outer": "^1.0.0", - "trim-repeated": "^1.0.0" - } - }, - "filesize": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/filesize/-/filesize-3.6.1.tgz", - "integrity": "sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg==" - }, - "fill-keys": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/fill-keys/-/fill-keys-1.0.2.tgz", - "integrity": "sha1-mo+jb06K1jTjv2tPPIiCVRRS6yA=", - "dev": true, - "requires": { - "is-object": "~1.0.1", - "merge-descriptors": "~1.0.0" - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true - } - } - }, - "find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", - "dev": true, - "requires": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "flat-cache": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.4.tgz", - "integrity": "sha512-VwyB3Lkgacfik2vhqR4uv2rvebqmDvFu4jlN/C1RzWoJEo8I7z4Q404oiqYCkq41mni8EzQnm95emU9seckwtg==", - "dev": true, - "requires": { - "circular-json": "^0.3.1", - "graceful-fs": "^4.1.2", - "rimraf": "~2.6.2", - "write": "^0.2.1" - } - }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "dev": true - }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "formatio": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/formatio/-/formatio-1.1.1.tgz", - "integrity": "sha1-XtPM1jZVEJc4NGXZlhmRAOhhYek=", - "dev": true, - "requires": { - "samsam": "~1.1" - } - }, - "formidable": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.1.tgz", - "integrity": "sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg==" - }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "requires": { - "map-cache": "^0.2.2" - } - }, - "fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" - }, - "fs-extra": { - "version": "0.26.7", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.26.7.tgz", - "integrity": "sha1-muH92UiXeY7at20JGM9C0MMYT6k=", - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^2.1.0", - "klaw": "^1.0.0", - "path-is-absolute": "^1.0.0", - "rimraf": "^2.2.8" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "fsevents": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", - "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==", - "dev": true, - "optional": true, - "requires": { - "nan": "^2.12.1", - "node-pre-gyp": "^0.12.0" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "debug": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "^2.1.1" - } - }, - "deep-extend": { - "version": "0.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "glob": { - "version": "7.1.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.24", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore-walk": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true, - "optional": true - }, - "minipass": { - "version": "2.3.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.2.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "needle": { - "version": "2.3.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "^4.1.0", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.12.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.4.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "rimraf": { - "version": "2.6.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "dev": true, - "optional": true - }, - "semver": { - "version": "5.7.0", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.3.4", - "minizlib": "^1.1.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "yallist": { - "version": "3.0.3", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "gauge": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-1.2.7.tgz", - "integrity": "sha1-6c7FSD09TuDvRLYKfZnkk14TbZM=", - "requires": { - "ansi": "^0.3.0", - "has-unicode": "^2.0.0", - "lodash.pad": "^4.1.0", - "lodash.padend": "^4.1.0", - "lodash.padstart": "^4.1.0" - } - }, - "generate-function": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", - "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", - "dev": true, - "requires": { - "is-property": "^1.0.2" - } - }, - "generate-object-property": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", - "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", - "dev": true, - "requires": { - "is-property": "^1.0.0" - } - }, - "get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", - "dev": true - }, - "get-proxy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/get-proxy/-/get-proxy-2.1.0.tgz", - "integrity": "sha512-zmZIaQTWnNQb4R4fJUEp/FC51eZsc6EkErspy3xtIYStaq8EB/hDIWipxsal+E8rz0qD7f2sL/NA9Xee4RInJw==", - "requires": { - "npm-conf": "^1.1.0" - } - }, - "get-stdin": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-5.0.1.tgz", - "integrity": "sha1-Ei4WFZHiH/TFJTAwVpPyDmOTo5g=" - }, - "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" - }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "global-dirs": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", - "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", - "requires": { - "ini": "^1.3.4" - } - }, - "globals": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", - "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", - "dev": true - }, - "globby": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", - "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", - "requires": { - "array-union": "^1.0.1", - "glob": "^7.0.3", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "got": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", - "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", - "requires": { - "create-error-class": "^3.0.0", - "duplexer3": "^0.1.4", - "get-stream": "^3.0.0", - "is-redirect": "^1.0.0", - "is-retry-allowed": "^1.0.0", - "is-stream": "^1.0.0", - "lowercase-keys": "^1.0.0", - "safe-buffer": "^5.0.1", - "timed-out": "^4.0.0", - "unzip-response": "^2.0.1", - "url-parse-lax": "^1.0.0" - } - }, - "graceful-fs": { - "version": "4.1.15", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", - "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==" - }, - "graceful-readlink": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", - "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=" - }, - "graphlib": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.7.tgz", - "integrity": "sha512-TyI9jIy2J4j0qgPmOOrHTCtpPqJGN/aurBwc6ZT+bRii+di1I+Wv3obRhVrmBEXet+qkMaEX67dXrwsd3QQM6w==", - "requires": { - "lodash": "^4.17.5" - } - }, - "gray-matter": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-2.1.1.tgz", - "integrity": "sha1-MELZrewqHe1qdwep7SOA+KF6Qw4=", - "dev": true, - "requires": { - "ansi-red": "^0.1.1", - "coffee-script": "^1.12.4", - "extend-shallow": "^2.0.1", - "js-yaml": "^3.8.1", - "toml": "^2.3.2" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true - } - } - }, - "growl": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", - "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", - "dev": true - }, - "growly": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", - "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", - "dev": true - }, - "handlebars": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz", - "integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==", - "dev": true, - "requires": { - "neo-async": "^2.6.0", - "optimist": "^0.6.1", - "source-map": "^0.6.1", - "uglify-js": "^3.1.4" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "dev": true - }, - "har-validator": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", - "dev": true, - "requires": { - "ajv": "^6.5.5", - "har-schema": "^2.0.0" - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" - }, - "has-symbol-support-x": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz", - "integrity": "sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==" - }, - "has-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", - "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", - "dev": true - }, - "has-to-string-tag-x": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz", - "integrity": "sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw==", - "requires": { - "has-symbol-support-x": "^1.4.1" - } - }, - "has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" - }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - } - }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "dependencies": { - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "he": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", - "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", - "dev": true - }, - "hosted-git-info": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", - "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", - "dev": true - }, - "html-encoding-sniffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", - "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", - "dev": true, - "requires": { - "whatwg-encoding": "^1.0.1" - } - }, - "http-basic": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/http-basic/-/http-basic-2.5.1.tgz", - "integrity": "sha1-jORHvbW2xXf4pj4/p4BW7Eu02/s=", - "dev": true, - "requires": { - "caseless": "~0.11.0", - "concat-stream": "^1.4.6", - "http-response-object": "^1.0.0" - }, - "dependencies": { - "caseless": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", - "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=", - "dev": true - } - } - }, - "http-response-object": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-1.1.0.tgz", - "integrity": "sha1-p8TnWq6C87tJBOT0P2FWc7TVGMM=", - "dev": true - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "https-proxy-agent": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz", - "integrity": "sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==", - "requires": { - "agent-base": "^4.1.0", - "debug": "^3.1.0" - } - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ieee754": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" - }, - "ignore": { - "version": "3.3.10", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", - "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", - "dev": true - }, - "immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=" - }, - "import-lazy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", - "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=" - }, - "import-local": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", - "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", - "dev": true, - "requires": { - "pkg-dir": "^3.0.0", - "resolve-cwd": "^2.0.0" - }, - "dependencies": { - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "pkg-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", - "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", - "dev": true, - "requires": { - "find-up": "^3.0.0" - } - } - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" - }, - "inquirer": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-1.2.3.tgz", - "integrity": "sha1-TexvMvN+97sLLtPx0aXD9UUHSRg=", - "requires": { - "ansi-escapes": "^1.1.0", - "chalk": "^1.0.0", - "cli-cursor": "^1.0.1", - "cli-width": "^2.0.0", - "external-editor": "^1.1.0", - "figures": "^1.3.5", - "lodash": "^4.3.0", - "mute-stream": "0.0.6", - "pinkie-promise": "^2.0.0", - "run-async": "^2.2.0", - "rx": "^4.1.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.0", - "through": "^2.3.6" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - } - } - }, - "interpret": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz", - "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==", - "dev": true - }, - "invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "dev": true, - "requires": { - "loose-envify": "^1.0.0" - } - }, - "invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", - "dev": true - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-arguments": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", - "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==", - "dev": true - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" - }, - "is-callable": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", - "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", - "dev": true - }, - "is-ci": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz", - "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", - "requires": { - "ci-info": "^1.5.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-date-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", - "dev": true - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-docker": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-1.1.0.tgz", - "integrity": "sha1-8EN01O7lMQ6ajhE78UlUEeRhdqE=" - }, - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "^2.0.4" - } - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true - }, - "is-generator-function": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.7.tgz", - "integrity": "sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw==", - "dev": true - }, - "is-installed-globally": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.1.0.tgz", - "integrity": "sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=", - "requires": { - "global-dirs": "^0.1.0", - "is-path-inside": "^1.0.0" - } - }, - "is-local-path": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-local-path/-/is-local-path-0.1.6.tgz", - "integrity": "sha1-gV0USxTVac7L6tTVaTCX8Aqb9sU=", - "dev": true - }, - "is-my-ip-valid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz", - "integrity": "sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==", - "dev": true - }, - "is-my-json-valid": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.20.0.tgz", - "integrity": "sha512-XTHBZSIIxNsIsZXg7XB5l8z/OBFosl1Wao4tXLpeC7eKU4Vm/kdop2azkPqULwnfGQjmeDIyey9g7afMMtdWAA==", - "dev": true, - "requires": { - "generate-function": "^2.0.0", - "generate-object-property": "^1.1.0", - "is-my-ip-valid": "^1.0.0", - "jsonpointer": "^4.0.0", - "xtend": "^4.0.0" - } - }, - "is-natural-number": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", - "integrity": "sha1-q5124dtM7VHjXeDHLr7PCfc0zeg=" - }, - "is-npm": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", - "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=" - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=" - }, - "is-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz", - "integrity": "sha1-iVJojF7C/9awPsyF52ngKQMINHA=" - }, - "is-path-inside": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", - "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", - "requires": { - "path-is-inside": "^1.0.1" - } - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "requires": { - "isobject": "^3.0.1" - } - }, - "is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", - "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" - }, - "is-property": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", - "dev": true - }, - "is-redirect": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", - "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=" - }, - "is-regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", - "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", - "dev": true, - "requires": { - "has": "^1.0.1" - } - }, - "is-resolvable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", - "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", - "dev": true - }, - "is-retry-allowed": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz", - "integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=" - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" - }, - "is-symbol": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", - "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", - "dev": true, - "requires": { - "has-symbols": "^1.0.0" - } - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true - }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" - }, - "is-wsl": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", - "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", - "dev": true - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", - "dev": true - }, - "istanbul": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", - "integrity": "sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=", - "dev": true, - "requires": { - "abbrev": "1.0.x", - "async": "1.x", - "escodegen": "1.8.x", - "esprima": "2.7.x", - "glob": "^5.0.15", - "handlebars": "^4.0.1", - "js-yaml": "3.x", - "mkdirp": "0.5.x", - "nopt": "3.x", - "once": "1.x", - "resolve": "1.1.x", - "supports-color": "^3.1.0", - "which": "^1.1.1", - "wordwrap": "^1.0.0" - }, - "dependencies": { - "esprima": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", - "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", - "dev": true - }, - "glob": { - "version": "5.0.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", - "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", - "dev": true, - "requires": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", - "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", - "dev": true - }, - "resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", - "dev": true - }, - "supports-color": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", - "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", - "dev": true, - "requires": { - "has-flag": "^1.0.0" - } - } - } - }, - "istanbul-lib-coverage": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", - "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", - "dev": true - }, - "istanbul-lib-instrument": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz", - "integrity": "sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==", - "dev": true, - "requires": { - "@babel/generator": "^7.4.0", - "@babel/parser": "^7.4.3", - "@babel/template": "^7.4.0", - "@babel/traverse": "^7.4.3", - "@babel/types": "^7.4.0", - "istanbul-lib-coverage": "^2.0.5", - "semver": "^6.0.0" - }, - "dependencies": { - "semver": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.0.0.tgz", - "integrity": "sha512-0UewU+9rFapKFnlbirLi3byoOuhrSsli/z/ihNnvM24vgF+8sNBiI1LZPBSH9wJKUwaUbw+s3hToDLCXkrghrQ==", - "dev": true - } - } - }, - "istanbul-lib-report": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz", - "integrity": "sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ==", - "dev": true, - "requires": { - "istanbul-lib-coverage": "^2.0.5", - "make-dir": "^2.1.0", - "supports-color": "^6.1.0" - }, - "dependencies": { - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - } - }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true - }, - "supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "istanbul-lib-source-maps": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz", - "integrity": "sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==", - "dev": true, - "requires": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^2.0.5", - "make-dir": "^2.1.0", - "rimraf": "^2.6.3", - "source-map": "^0.6.1" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - } - }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "istanbul-reports": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.4.tgz", - "integrity": "sha512-QCHGyZEK0bfi9GR215QSm+NJwFKEShbtc7tfbUdLAEzn3kKhLDDZqvljn8rPZM9v8CEOhzL1nlYoO4r1ryl67w==", - "dev": true, - "requires": { - "handlebars": "^4.1.2" - } - }, - "isurl": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz", - "integrity": "sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==", - "requires": { - "has-to-string-tag-x": "^1.2.0", - "is-object": "^1.0.1" - } - }, - "jest-changed-files": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-24.8.0.tgz", - "integrity": "sha512-qgANC1Yrivsq+UrLXsvJefBKVoCsKB0Hv+mBb6NMjjZ90wwxCDmU3hsCXBya30cH+LnPYjwgcU65i6yJ5Nfuug==", - "dev": true, - "requires": { - "@jest/types": "^24.8.0", - "execa": "^1.0.0", - "throat": "^4.0.0" - }, - "dependencies": { - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - } - } - }, - "jest-cli": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-24.8.0.tgz", - "integrity": "sha512-+p6J00jSMPQ116ZLlHJJvdf8wbjNbZdeSX9ptfHX06/MSNaXmKihQzx5vQcw0q2G6JsdVkUIdWbOWtSnaYs3yA==", - "dev": true, - "requires": { - "@jest/core": "^24.8.0", - "@jest/test-result": "^24.8.0", - "@jest/types": "^24.8.0", - "chalk": "^2.0.1", - "exit": "^0.1.2", - "import-local": "^2.0.0", - "is-ci": "^2.0.0", - "jest-config": "^24.8.0", - "jest-util": "^24.8.0", - "jest-validate": "^24.8.0", - "prompts": "^2.0.1", - "realpath-native": "^1.1.0", - "yargs": "^12.0.2" - }, - "dependencies": { - "ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", - "dev": true - }, - "is-ci": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", - "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", - "dev": true, - "requires": { - "ci-info": "^2.0.0" - } - } - } - }, - "jest-config": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-24.8.0.tgz", - "integrity": "sha512-Czl3Nn2uEzVGsOeaewGWoDPD8GStxCpAe0zOYs2x2l0fZAgPbCr3uwUkgNKV3LwE13VXythM946cd5rdGkkBZw==", - "dev": true, - "requires": { - "@babel/core": "^7.1.0", - "@jest/test-sequencer": "^24.8.0", - "@jest/types": "^24.8.0", - "babel-jest": "^24.8.0", - "chalk": "^2.0.1", - "glob": "^7.1.1", - "jest-environment-jsdom": "^24.8.0", - "jest-environment-node": "^24.8.0", - "jest-get-type": "^24.8.0", - "jest-jasmine2": "^24.8.0", - "jest-regex-util": "^24.3.0", - "jest-resolve": "^24.8.0", - "jest-util": "^24.8.0", - "jest-validate": "^24.8.0", - "micromatch": "^3.1.10", - "pretty-format": "^24.8.0", - "realpath-native": "^1.1.0" - } - }, - "jest-diff": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-24.8.0.tgz", - "integrity": "sha512-wxetCEl49zUpJ/bvUmIFjd/o52J+yWcoc5ZyPq4/W1LUKGEhRYDIbP1KcF6t+PvqNrGAFk4/JhtxDq/Nnzs66g==", - "dev": true, - "requires": { - "chalk": "^2.0.1", - "diff-sequences": "^24.3.0", - "jest-get-type": "^24.8.0", - "pretty-format": "^24.8.0" - } - }, - "jest-docblock": { - "version": "24.3.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-24.3.0.tgz", - "integrity": "sha512-nlANmF9Yq1dufhFlKG9rasfQlrY7wINJbo3q01tu56Jv5eBU5jirylhF2O5ZBnLxzOVBGRDz/9NAwNyBtG4Nyg==", - "dev": true, - "requires": { - "detect-newline": "^2.1.0" - } - }, - "jest-each": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-24.8.0.tgz", - "integrity": "sha512-NrwK9gaL5+XgrgoCsd9svsoWdVkK4gnvyhcpzd6m487tXHqIdYeykgq3MKI1u4I+5Zf0tofr70at9dWJDeb+BA==", - "dev": true, - "requires": { - "@jest/types": "^24.8.0", - "chalk": "^2.0.1", - "jest-get-type": "^24.8.0", - "jest-util": "^24.8.0", - "pretty-format": "^24.8.0" - } - }, - "jest-environment-jsdom": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-24.8.0.tgz", - "integrity": "sha512-qbvgLmR7PpwjoFjM/sbuqHJt/NCkviuq9vus9NBn/76hhSidO+Z6Bn9tU8friecegbJL8gzZQEMZBQlFWDCwAQ==", - "dev": true, - "requires": { - "@jest/environment": "^24.8.0", - "@jest/fake-timers": "^24.8.0", - "@jest/types": "^24.8.0", - "jest-mock": "^24.8.0", - "jest-util": "^24.8.0", - "jsdom": "^11.5.1" - } - }, - "jest-environment-node": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-24.8.0.tgz", - "integrity": "sha512-vIGUEScd1cdDgR6sqn2M08sJTRLQp6Dk/eIkCeO4PFHxZMOgy+uYLPMC4ix3PEfM5Au/x3uQ/5Tl0DpXXZsJ/Q==", - "dev": true, - "requires": { - "@jest/environment": "^24.8.0", - "@jest/fake-timers": "^24.8.0", - "@jest/types": "^24.8.0", - "jest-mock": "^24.8.0", - "jest-util": "^24.8.0" - } - }, - "jest-get-type": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-24.8.0.tgz", - "integrity": "sha512-RR4fo8jEmMD9zSz2nLbs2j0zvPpk/KCEz3a62jJWbd2ayNo0cb+KFRxPHVhE4ZmgGJEQp0fosmNz84IfqM8cMQ==", - "dev": true - }, - "jest-haste-map": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-24.8.0.tgz", - "integrity": "sha512-ZBPRGHdPt1rHajWelXdqygIDpJx8u3xOoLyUBWRW28r3tagrgoepPrzAozW7kW9HrQfhvmiv1tncsxqHJO1onQ==", - "dev": true, - "requires": { - "@jest/types": "^24.8.0", - "anymatch": "^2.0.0", - "fb-watchman": "^2.0.0", - "fsevents": "^1.2.7", - "graceful-fs": "^4.1.15", - "invariant": "^2.2.4", - "jest-serializer": "^24.4.0", - "jest-util": "^24.8.0", - "jest-worker": "^24.6.0", - "micromatch": "^3.1.10", - "sane": "^4.0.3", - "walker": "^1.0.7" - } - }, - "jest-jasmine2": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-24.8.0.tgz", - "integrity": "sha512-cEky88npEE5LKd5jPpTdDCLvKkdyklnaRycBXL6GNmpxe41F0WN44+i7lpQKa/hcbXaQ+rc9RMaM4dsebrYong==", - "dev": true, - "requires": { - "@babel/traverse": "^7.1.0", - "@jest/environment": "^24.8.0", - "@jest/test-result": "^24.8.0", - "@jest/types": "^24.8.0", - "chalk": "^2.0.1", - "co": "^4.6.0", - "expect": "^24.8.0", - "is-generator-fn": "^2.0.0", - "jest-each": "^24.8.0", - "jest-matcher-utils": "^24.8.0", - "jest-message-util": "^24.8.0", - "jest-runtime": "^24.8.0", - "jest-snapshot": "^24.8.0", - "jest-util": "^24.8.0", - "pretty-format": "^24.8.0", - "throat": "^4.0.0" - } - }, - "jest-leak-detector": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-24.8.0.tgz", - "integrity": "sha512-cG0yRSK8A831LN8lIHxI3AblB40uhv0z+SsQdW3GoMMVcK+sJwrIIyax5tu3eHHNJ8Fu6IMDpnLda2jhn2pD/g==", - "dev": true, - "requires": { - "pretty-format": "^24.8.0" - } - }, - "jest-matcher-utils": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-24.8.0.tgz", - "integrity": "sha512-lex1yASY51FvUuHgm0GOVj7DCYEouWSlIYmCW7APSqB9v8mXmKSn5+sWVF0MhuASG0bnYY106/49JU1FZNl5hw==", - "dev": true, - "requires": { - "chalk": "^2.0.1", - "jest-diff": "^24.8.0", - "jest-get-type": "^24.8.0", - "pretty-format": "^24.8.0" - } - }, - "jest-message-util": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-24.8.0.tgz", - "integrity": "sha512-p2k71rf/b6ns8btdB0uVdljWo9h0ovpnEe05ZKWceQGfXYr4KkzgKo3PBi8wdnd9OtNh46VpNIJynUn/3MKm1g==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@jest/test-result": "^24.8.0", - "@jest/types": "^24.8.0", - "@types/stack-utils": "^1.0.1", - "chalk": "^2.0.1", - "micromatch": "^3.1.10", - "slash": "^2.0.0", - "stack-utils": "^1.0.1" - }, - "dependencies": { - "slash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", - "dev": true - } - } - }, - "jest-mock": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-24.8.0.tgz", - "integrity": "sha512-6kWugwjGjJw+ZkK4mDa0Df3sDlUTsV47MSrT0nGQ0RBWJbpODDQ8MHDVtGtUYBne3IwZUhtB7elxHspU79WH3A==", - "dev": true, - "requires": { - "@jest/types": "^24.8.0" - } - }, - "jest-pnp-resolver": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.1.tgz", - "integrity": "sha512-pgFw2tm54fzgYvc/OHrnysABEObZCUNFnhjoRjaVOCN8NYc032/gVjPaHD4Aq6ApkSieWtfKAFQtmDKAmhupnQ==", - "dev": true - }, - "jest-regex-util": { - "version": "24.3.0", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-24.3.0.tgz", - "integrity": "sha512-tXQR1NEOyGlfylyEjg1ImtScwMq8Oh3iJbGTjN7p0J23EuVX1MA8rwU69K4sLbCmwzgCUbVkm0FkSF9TdzOhtg==", - "dev": true - }, - "jest-resolve": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-24.8.0.tgz", - "integrity": "sha512-+hjSzi1PoRvnuOICoYd5V/KpIQmkAsfjFO71458hQ2Whi/yf1GDeBOFj8Gxw4LrApHsVJvn5fmjcPdmoUHaVKw==", - "dev": true, - "requires": { - "@jest/types": "^24.8.0", - "browser-resolve": "^1.11.3", - "chalk": "^2.0.1", - "jest-pnp-resolver": "^1.2.1", - "realpath-native": "^1.1.0" - } - }, - "jest-resolve-dependencies": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-24.8.0.tgz", - "integrity": "sha512-hyK1qfIf/krV+fSNyhyJeq3elVMhK9Eijlwy+j5jqmZ9QsxwKBiP6qukQxaHtK8k6zql/KYWwCTQ+fDGTIJauw==", - "dev": true, - "requires": { - "@jest/types": "^24.8.0", - "jest-regex-util": "^24.3.0", - "jest-snapshot": "^24.8.0" - } - }, - "jest-runner": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-24.8.0.tgz", - "integrity": "sha512-utFqC5BaA3JmznbissSs95X1ZF+d+4WuOWwpM9+Ak356YtMhHE/GXUondZdcyAAOTBEsRGAgH/0TwLzfI9h7ow==", - "dev": true, - "requires": { - "@jest/console": "^24.7.1", - "@jest/environment": "^24.8.0", - "@jest/test-result": "^24.8.0", - "@jest/types": "^24.8.0", - "chalk": "^2.4.2", - "exit": "^0.1.2", - "graceful-fs": "^4.1.15", - "jest-config": "^24.8.0", - "jest-docblock": "^24.3.0", - "jest-haste-map": "^24.8.0", - "jest-jasmine2": "^24.8.0", - "jest-leak-detector": "^24.8.0", - "jest-message-util": "^24.8.0", - "jest-resolve": "^24.8.0", - "jest-runtime": "^24.8.0", - "jest-util": "^24.8.0", - "jest-worker": "^24.6.0", - "source-map-support": "^0.5.6", - "throat": "^4.0.0" - } - }, - "jest-runtime": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-24.8.0.tgz", - "integrity": "sha512-Mq0aIXhvO/3bX44ccT+czU1/57IgOMyy80oM0XR/nyD5zgBcesF84BPabZi39pJVA6UXw+fY2Q1N+4BiVUBWOA==", - "dev": true, - "requires": { - "@jest/console": "^24.7.1", - "@jest/environment": "^24.8.0", - "@jest/source-map": "^24.3.0", - "@jest/transform": "^24.8.0", - "@jest/types": "^24.8.0", - "@types/yargs": "^12.0.2", - "chalk": "^2.0.1", - "exit": "^0.1.2", - "glob": "^7.1.3", - "graceful-fs": "^4.1.15", - "jest-config": "^24.8.0", - "jest-haste-map": "^24.8.0", - "jest-message-util": "^24.8.0", - "jest-mock": "^24.8.0", - "jest-regex-util": "^24.3.0", - "jest-resolve": "^24.8.0", - "jest-snapshot": "^24.8.0", - "jest-util": "^24.8.0", - "jest-validate": "^24.8.0", - "realpath-native": "^1.1.0", - "slash": "^2.0.0", - "strip-bom": "^3.0.0", - "yargs": "^12.0.2" - }, - "dependencies": { - "slash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", - "dev": true - } - } - }, - "jest-serializer": { - "version": "24.4.0", - "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-24.4.0.tgz", - "integrity": "sha512-k//0DtglVstc1fv+GY/VHDIjrtNjdYvYjMlbLUed4kxrE92sIUewOi5Hj3vrpB8CXfkJntRPDRjCrCvUhBdL8Q==", - "dev": true - }, - "jest-snapshot": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-24.8.0.tgz", - "integrity": "sha512-5ehtWoc8oU9/cAPe6fez6QofVJLBKyqkY2+TlKTOf0VllBB/mqUNdARdcjlZrs9F1Cv+/HKoCS/BknT0+tmfPg==", - "dev": true, - "requires": { - "@babel/types": "^7.0.0", - "@jest/types": "^24.8.0", - "chalk": "^2.0.1", - "expect": "^24.8.0", - "jest-diff": "^24.8.0", - "jest-matcher-utils": "^24.8.0", - "jest-message-util": "^24.8.0", - "jest-resolve": "^24.8.0", - "mkdirp": "^0.5.1", - "natural-compare": "^1.4.0", - "pretty-format": "^24.8.0", - "semver": "^5.5.0" - } - }, - "jest-util": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-24.8.0.tgz", - "integrity": "sha512-DYZeE+XyAnbNt0BG1OQqKy/4GVLPtzwGx5tsnDrFcax36rVE3lTA5fbvgmbVPUZf9w77AJ8otqR4VBbfFJkUZA==", - "dev": true, - "requires": { - "@jest/console": "^24.7.1", - "@jest/fake-timers": "^24.8.0", - "@jest/source-map": "^24.3.0", - "@jest/test-result": "^24.8.0", - "@jest/types": "^24.8.0", - "callsites": "^3.0.0", - "chalk": "^2.0.1", - "graceful-fs": "^4.1.15", - "is-ci": "^2.0.0", - "mkdirp": "^0.5.1", - "slash": "^2.0.0", - "source-map": "^0.6.0" - }, - "dependencies": { - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true - }, - "ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", - "dev": true - }, - "is-ci": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", - "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", - "dev": true, - "requires": { - "ci-info": "^2.0.0" - } - }, - "slash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "jest-validate": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-24.8.0.tgz", - "integrity": "sha512-+/N7VOEMW1Vzsrk3UWBDYTExTPwf68tavEPKDnJzrC6UlHtUDU/fuEdXqFoHzv9XnQ+zW6X3qMZhJ3YexfeLDA==", - "dev": true, - "requires": { - "@jest/types": "^24.8.0", - "camelcase": "^5.0.0", - "chalk": "^2.0.1", - "jest-get-type": "^24.8.0", - "leven": "^2.1.0", - "pretty-format": "^24.8.0" - }, - "dependencies": { - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - } - } - }, - "jest-watcher": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-24.8.0.tgz", - "integrity": "sha512-SBjwHt5NedQoVu54M5GEx7cl7IGEFFznvd/HNT8ier7cCAx/Qgu9ZMlaTQkvK22G1YOpcWBLQPFSImmxdn3DAw==", - "dev": true, - "requires": { - "@jest/test-result": "^24.8.0", - "@jest/types": "^24.8.0", - "@types/yargs": "^12.0.9", - "ansi-escapes": "^3.0.0", - "chalk": "^2.0.1", - "jest-util": "^24.8.0", - "string-length": "^2.0.0" - }, - "dependencies": { - "ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", - "dev": true - } - } - }, - "jest-worker": { - "version": "24.6.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-24.6.0.tgz", - "integrity": "sha512-jDwgW5W9qGNvpI1tNnvajh0a5IE/PuGLFmHk6aR/BZFz8tSgGw17GsDPXAJ6p91IvYDjOw8GpFbvvZGAK+DPQQ==", - "dev": true, - "requires": { - "merge-stream": "^1.0.1", - "supports-color": "^6.1.0" - }, - "dependencies": { - "supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "jmespath": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", - "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" - }, - "js-tokens": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", - "dev": true - }, - "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "dev": true - }, - "jsdom": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.12.0.tgz", - "integrity": "sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw==", - "dev": true, - "requires": { - "abab": "^2.0.0", - "acorn": "^5.5.3", - "acorn-globals": "^4.1.0", - "array-equal": "^1.0.0", - "cssom": ">= 0.3.2 < 0.4.0", - "cssstyle": "^1.0.0", - "data-urls": "^1.0.0", - "domexception": "^1.0.1", - "escodegen": "^1.9.1", - "html-encoding-sniffer": "^1.0.2", - "left-pad": "^1.3.0", - "nwsapi": "^2.0.7", - "parse5": "4.0.0", - "pn": "^1.1.0", - "request": "^2.87.0", - "request-promise-native": "^1.0.5", - "sax": "^1.2.4", - "symbol-tree": "^3.2.2", - "tough-cookie": "^2.3.4", - "w3c-hr-time": "^1.0.1", - "webidl-conversions": "^4.0.2", - "whatwg-encoding": "^1.0.3", - "whatwg-mimetype": "^2.1.0", - "whatwg-url": "^6.4.1", - "ws": "^5.2.0", - "xml-name-validator": "^3.0.0" - }, - "dependencies": { - "escodegen": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.11.1.tgz", - "integrity": "sha512-JwiqFD9KdGVVpeuRa68yU3zZnBEOcPs0nKW7wZzXky8Z7tffdYUHbe11bPCV5jYlK6DVdKLWLm0f5I/QlL0Kmw==", - "dev": true, - "requires": { - "esprima": "^3.1.3", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" - } - }, - "esprima": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", - "dev": true - }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true - } - } - }, - "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true - }, - "json-cycle": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/json-cycle/-/json-cycle-1.3.0.tgz", - "integrity": "sha512-FD/SedD78LCdSvJaOUQAXseT8oQBb5z6IVYaQaCrVUlu9zOAr1BDdKyVYQaSD/GDsAMrXpKcOyBD4LIl8nfjHw==" - }, - "json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true - }, - "json-refs": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/json-refs/-/json-refs-2.1.7.tgz", - "integrity": "sha1-uesB/in16j6Sh48VrqEK04taz4k=", - "requires": { - "commander": "^2.9.0", - "graphlib": "^2.1.1", - "js-yaml": "^3.8.3", - "native-promise-only": "^0.8.1", - "path-loader": "^1.0.2", - "slash": "^1.0.0", - "uri-js": "^3.0.2" - }, - "dependencies": { - "commander": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", - "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==" - } - } - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "json-stable-stringify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", - "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", - "dev": true, - "requires": { - "jsonify": "~0.0.0" - } - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" - }, - "json5": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.0.tgz", - "integrity": "sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ==", - "dev": true, - "requires": { - "minimist": "^1.2.0" - } - }, - "jsonfile": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", - "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", - "requires": { - "graceful-fs": "^4.1.6" - } - }, - "jsonify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", - "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", - "dev": true - }, - "jsonpointer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", - "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", - "dev": true - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "dev": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "jsx-ast-utils": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-1.4.1.tgz", - "integrity": "sha1-OGchPo3Xm/Ho8jAMDPwe+xgsDfE=", - "dev": true - }, - "jszip": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.2.1.tgz", - "integrity": "sha512-iCMBbo4eE5rb1VCpm5qXOAaUiRKRUKiItn8ah2YQQx9qymmSAY98eyQfioChEYcVQLh0zxJ3wS4A0mh90AVPvw==", - "requires": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "set-immediate-shim": "~1.0.1" - } - }, - "jwt-decode": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-2.2.0.tgz", - "integrity": "sha1-fYa9VmefWM5qhHBKZX3TkruoGnk=" - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - }, - "klaw": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz", - "integrity": "sha1-QIhDO0azsbolnXh4XY6W9zugJDk=", - "requires": { - "graceful-fs": "^4.1.9" - } - }, - "kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true - }, - "latest-version": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", - "integrity": "sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=", - "requires": { - "package-json": "^4.0.0" - } - }, - "lazy-cache": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-2.0.2.tgz", - "integrity": "sha1-uRkKT5EzVGlIQIWfio9whNiCImQ=", - "dev": true, - "requires": { - "set-getter": "^0.1.0" - } - }, - "lazystream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", - "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", - "requires": { - "readable-stream": "^2.0.5" - } - }, - "lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "dev": true, - "requires": { - "invert-kv": "^2.0.0" - } - }, - "lcov-parse": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-0.0.10.tgz", - "integrity": "sha1-GwuP+ayceIklBYK3C3ExXZ2m2aM=", - "dev": true - }, - "left-pad": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz", - "integrity": "sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==", - "dev": true - }, - "leven": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", - "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=", - "dev": true - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "requires": { - "immediate": "~3.0.5" - } - }, - "list-item": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/list-item/-/list-item-1.1.1.tgz", - "integrity": "sha1-DGXQDih8tmPMs8s4Sad+iewmilY=", - "dev": true, - "requires": { - "expand-range": "^1.8.1", - "extend-shallow": "^2.0.1", - "is-number": "^2.1.0", - "repeat-string": "^1.5.2" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true - }, - "is-number": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", - "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "load-json-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - }, - "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - } - } - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "dependencies": { - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true - } - } - }, - "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" - }, - "lodash.cond": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/lodash.cond/-/lodash.cond-4.5.2.tgz", - "integrity": "sha1-9HGh2khr5g9quVXRcRVSPdHSVdU=", - "dev": true - }, - "lodash.difference": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", - "integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=" - }, - "lodash.endswith": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/lodash.endswith/-/lodash.endswith-4.2.1.tgz", - "integrity": "sha1-/tWawXOO0+I27dcGTsRWRIs3vAk=", - "dev": true - }, - "lodash.find": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.find/-/lodash.find-4.6.0.tgz", - "integrity": "sha1-ywcE1Hq3F4n/oN6Ll92Sb7iLE7E=", - "dev": true - }, - "lodash.findindex": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.findindex/-/lodash.findindex-4.6.0.tgz", - "integrity": "sha1-oyRd7mH7m24GJLU1ElYku2nBEQY=", - "dev": true - }, - "lodash.pad": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/lodash.pad/-/lodash.pad-4.5.1.tgz", - "integrity": "sha1-QzCUmoM6fI2iLMIPaibE1Z3runA=" - }, - "lodash.padend": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/lodash.padend/-/lodash.padend-4.6.1.tgz", - "integrity": "sha1-U8y6BH0G4VjTEfRdpiX05J5vFm4=" - }, - "lodash.padstart": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/lodash.padstart/-/lodash.padstart-4.6.1.tgz", - "integrity": "sha1-0uPuv/DZ05rVD1y9G1KnvOa7YRs=" - }, - "lodash.sortby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", - "dev": true - }, - "lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=" - }, - "log-driver": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", - "integrity": "sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg==", - "dev": true - }, - "lolex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-1.3.2.tgz", - "integrity": "sha1-fD2mL/yzDw9agKJWbKJORdigHzE=", - "dev": true - }, - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" - } - }, - "lowercase-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", - "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==" - }, - "lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "lsmod": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lsmod/-/lsmod-1.0.0.tgz", - "integrity": "sha1-mgD3bco26yP6BTUK/htYXUKZ5ks=" - }, - "make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", - "requires": { - "pify": "^3.0.0" - }, - "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" - } - } - }, - "makeerror": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", - "integrity": "sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=", - "dev": true, - "requires": { - "tmpl": "1.0.x" - } - }, - "map-age-cleaner": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", - "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", - "dev": true, - "requires": { - "p-defer": "^1.0.0" - } - }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=" - }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "requires": { - "object-visit": "^1.0.0" - } - }, - "markdown-link": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/markdown-link/-/markdown-link-0.1.1.tgz", - "integrity": "sha1-MsXGUZmmRXMWMi0eQinRNAfIx88=", - "dev": true - }, - "markdown-magic": { - "version": "0.1.25", - "resolved": "https://registry.npmjs.org/markdown-magic/-/markdown-magic-0.1.25.tgz", - "integrity": "sha512-NBVMv2IPdKaRIXcL8qmLkfq9O17tkByTr8sRkJ4l76tkp401hxCUA0r9mkhtnGJRevCqZ2KoHrIf9WYQUn8ztA==", - "dev": true, - "requires": { - "commander": "^2.9.0", - "deepmerge": "^1.3.0", - "find-up": "^2.1.0", - "fs-extra": "^1.0.0", - "globby": "^6.1.0", - "is-local-path": "^0.1.6", - "markdown-toc": "^1.0.2", - "sync-request": "^3.0.1" - }, - "dependencies": { - "commander": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", - "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", - "dev": true - }, - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "^2.0.0" - } - }, - "fs-extra": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-1.0.0.tgz", - "integrity": "sha1-zTzl9+fLYUWIP8rjGR6Yd/hYeVA=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^2.1.0", - "klaw": "^1.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true - } - } - }, - "markdown-table": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-1.1.3.tgz", - "integrity": "sha512-1RUZVgQlpJSPWYbFSpmudq5nHY1doEIv89gBtF0s4gW1GF2XorxcA/70M5vq7rLv0a6mhOUccRsqkwhwLCIQ2Q==", - "dev": true - }, - "markdown-toc": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/markdown-toc/-/markdown-toc-1.2.0.tgz", - "integrity": "sha512-eOsq7EGd3asV0oBfmyqngeEIhrbkc7XVP63OwcJBIhH2EpG2PzFcbZdhy1jutXSlRBBVMNXHvMtSr5LAxSUvUg==", - "dev": true, - "requires": { - "concat-stream": "^1.5.2", - "diacritics-map": "^0.1.0", - "gray-matter": "^2.1.0", - "lazy-cache": "^2.0.2", - "list-item": "^1.1.1", - "markdown-link": "^0.1.1", - "minimist": "^1.2.0", - "mixin-deep": "^1.1.3", - "object.pick": "^1.2.0", - "remarkable": "^1.7.1", - "repeat-string": "^1.6.1", - "strip-color": "^0.1.0" - } - }, - "math-random": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.4.tgz", - "integrity": "sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==", - "dev": true - }, - "mem": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", - "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", - "dev": true, - "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^2.0.0", - "p-is-promise": "^2.0.0" - } - }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", - "dev": true - }, - "merge-stream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-1.0.1.tgz", - "integrity": "sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE=", - "dev": true, - "requires": { - "readable-stream": "^2.0.1" - } - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" - }, - "mime-db": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", - "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" - }, - "mime-types": { - "version": "2.1.24", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", - "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", - "requires": { - "mime-db": "1.40.0" - } - }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - }, - "mixin-deep": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", - "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", - "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - } - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "requires": { - "minimist": "0.0.8" - }, - "dependencies": { - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" - } - } - }, - "mocha": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", - "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", - "dev": true, - "requires": { - "browser-stdout": "1.3.1", - "commander": "2.15.1", - "debug": "3.1.0", - "diff": "3.5.0", - "escape-string-regexp": "1.0.5", - "glob": "7.1.2", - "growl": "1.10.5", - "he": "1.1.1", - "minimatch": "3.0.4", - "mkdirp": "0.5.1", - "supports-color": "5.4.0" - }, - "dependencies": { - "commander": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", - "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", - "dev": true - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "mocha-lcov-reporter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/mocha-lcov-reporter/-/mocha-lcov-reporter-1.3.0.tgz", - "integrity": "sha1-Rpve9PivyaEWBW8HnfYYLQr7A4Q=", - "dev": true - }, - "mock-require": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/mock-require/-/mock-require-1.3.0.tgz", - "integrity": "sha1-gmFElS5QR2L45pJKqPY5Rl0deiQ=", - "dev": true, - "requires": { - "caller-id": "^0.1.0" - } - }, - "module-not-found-error": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/module-not-found-error/-/module-not-found-error-1.0.1.tgz", - "integrity": "sha1-z4tP9PKWQGdNbN0CsOO8UjwrvcA=", - "dev": true - }, - "moment": { - "version": "2.24.0", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", - "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" - }, - "mute-stream": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.6.tgz", - "integrity": "sha1-SJYrGeFp/R38JAs/HnMXYnu8R9s=" - }, - "nan": { - "version": "2.13.2", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz", - "integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==", - "dev": true, - "optional": true - }, - "nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - } - }, - "native-promise-only": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/native-promise-only/-/native-promise-only-0.8.1.tgz", - "integrity": "sha1-IKMYwwy0X3H+et+/eyHJnBRy7xE=" - }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", - "dev": true - }, - "neo-async": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", - "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", - "dev": true - }, - "next-tick": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", - "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", - "dev": true - }, - "nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, - "node-fetch": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", - "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", - "requires": { - "encoding": "^0.1.11", - "is-stream": "^1.0.1" - } - }, - "node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", - "dev": true - }, - "node-modules-regexp": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", - "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=", - "dev": true - }, - "node-notifier": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.4.0.tgz", - "integrity": "sha512-SUDEb+o71XR5lXSTyivXd9J7fCloE3SyP4lSgt3lU2oSANiox+SxlNRGPjDKrwU1YN3ix2KN/VGGCg0t01rttQ==", - "dev": true, - "requires": { - "growly": "^1.3.0", - "is-wsl": "^1.1.0", - "semver": "^5.5.0", - "shellwords": "^0.1.1", - "which": "^1.3.0" - } - }, - "nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", - "dev": true, - "requires": { - "abbrev": "1" - } - }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "requires": { - "remove-trailing-separator": "^1.0.1" - } - }, - "npm-conf": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/npm-conf/-/npm-conf-1.1.3.tgz", - "integrity": "sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw==", - "requires": { - "config-chain": "^1.1.11", - "pify": "^3.0.0" - }, - "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" - } - } - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "requires": { - "path-key": "^2.0.0" - } - }, - "npmlog": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-2.0.4.tgz", - "integrity": "sha1-mLUlMPJRTKkNCexbIsiEZyI3VpI=", - "requires": { - "ansi": "~0.3.1", - "are-we-there-yet": "~1.1.2", - "gauge": "~1.2.5" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" - }, - "nwsapi": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.1.4.tgz", - "integrity": "sha512-iGfd9Y6SFdTNldEy2L0GUhcarIutFmk+MPWIn9dmj8NMIup03G08uUF2KGbbmv/Ux4RT0VZJoP/sVbWA6d/VIw==", - "dev": true - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "requires": { - "kind-of": "^3.0.2" - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "requires": { - "kind-of": "^3.0.2" - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" - } - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "object-hash": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-1.3.1.tgz", - "integrity": "sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==" - }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true - }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "requires": { - "isobject": "^3.0.0" - } - }, - "object.assign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", - "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.0", - "object-keys": "^1.0.11" - } - }, - "object.entries": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.0.tgz", - "integrity": "sha512-l+H6EQ8qzGRxbkHOd5I/aHRhHDKoQXQ8g0BYt4uSweQU1/J6dZUOyWh9a2Vky35YCKjzmgxOzta2hH6kf9HuXA==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.12.0", - "function-bind": "^1.1.1", - "has": "^1.0.3" - } - }, - "object.getownpropertydescriptors": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", - "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.5.1" - } - }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", - "requires": { - "isobject": "^3.0.1" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" - } - }, - "onetime": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", - "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=" - }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "dev": true, - "requires": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" - }, - "dependencies": { - "minimist": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", - "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", - "dev": true - }, - "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", - "dev": true - } - } - }, - "optionator": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", - "dev": true, - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.4", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "wordwrap": "~1.0.0" - } - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true - }, - "os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "dev": true, - "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - }, - "dependencies": { - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - } - } - }, - "os-shim": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/os-shim/-/os-shim-0.1.3.tgz", - "integrity": "sha1-a2LDeRz3kJ6jXtRuF2WLtBfLORc=" - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" - }, - "p-defer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", - "dev": true - }, - "p-each-series": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-1.0.0.tgz", - "integrity": "sha1-kw89Et0fUOdDRFeiLNbwSsatf3E=", - "dev": true, - "requires": { - "p-reduce": "^1.0.0" - } - }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" - }, - "p-is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", - "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", - "dev": true - }, - "p-limit": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", - "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "p-reduce": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-1.0.0.tgz", - "integrity": "sha1-GMKw3ZNqRpClKfgjH1ig/bakffo=", - "dev": true - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "package-json": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz", - "integrity": "sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=", - "requires": { - "got": "^6.7.1", - "registry-auth-token": "^3.0.1", - "registry-url": "^3.0.3", - "semver": "^5.1.0" - } - }, - "pako": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz", - "integrity": "sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw==" - }, - "parse-github-url": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/parse-github-url/-/parse-github-url-1.0.2.tgz", - "integrity": "sha512-kgBf6avCbO3Cn6+RnzRGLkUsv4ZVqv/VfAYkRsyBcgkshNvVBkRn1FEZcW0Jb+npXQWm2vHPnnOqFteZxRRGNw==", - "dev": true - }, - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "dev": true, - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - } - }, - "parse5": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz", - "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==", - "dev": true - }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" - }, - "path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", - "dev": true, - "requires": { - "pinkie-promise": "^2.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=" - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" - }, - "path-loader": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/path-loader/-/path-loader-1.0.10.tgz", - "integrity": "sha512-CMP0v6S6z8PHeJ6NFVyVJm6WyJjIwFvyz2b0n2/4bKdS/0uZa/9sKUlYZzubrn3zuDRU0zIuEDX9DZYQ2ZI8TA==", - "requires": { - "native-promise-only": "^0.8.1", - "superagent": "^3.8.3" - } - }, - "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", - "dev": true - }, - "path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "dev": true, - "requires": { - "pify": "^3.0.0" - }, - "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - } - } - }, - "pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=" - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", - "dev": true - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" - }, - "pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" - }, - "pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "requires": { - "pinkie": "^2.0.0" - } - }, - "pirates": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz", - "integrity": "sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==", - "dev": true, - "requires": { - "node-modules-regexp": "^1.0.0" - } - }, - "pkg-dir": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-1.0.0.tgz", - "integrity": "sha1-ektQio1bstYp1EcFb/TpyTFM89Q=", - "dev": true, - "requires": { - "find-up": "^1.0.0" - } - }, - "pkg-up": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-1.0.0.tgz", - "integrity": "sha1-Pgj7RhUlxEIWJKM7n35tCvWwWiY=", - "dev": true, - "requires": { - "find-up": "^1.0.0" - } - }, - "pluralize": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-1.2.1.tgz", - "integrity": "sha1-0aIUg/0iu0HlihL6NCGCMUCJfEU=", - "dev": true - }, - "pn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", - "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", - "dev": true - }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", - "dev": true - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true - }, - "prepend-http": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", - "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=" - }, - "pretty-format": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-24.8.0.tgz", - "integrity": "sha512-P952T7dkrDEplsR+TuY7q3VXDae5Sr7zmQb12JU/NDQa/3CH7/QW0yvqLcGN6jL+zQFKaoJcPc+yJxMTGmosqw==", - "dev": true, - "requires": { - "@jest/types": "^24.8.0", - "ansi-regex": "^4.0.0", - "ansi-styles": "^3.2.0", - "react-is": "^16.8.4" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - } - } - }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" - }, - "progress": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", - "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=", - "dev": true - }, - "promise": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", - "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", - "dev": true, - "requires": { - "asap": "~2.0.3" - } - }, - "promise-queue": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/promise-queue/-/promise-queue-2.2.5.tgz", - "integrity": "sha1-L29ffA9tCBCelnZZx5uIqe1ek7Q=" - }, - "prompts": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.0.4.tgz", - "integrity": "sha512-HTzM3UWp/99A0gk51gAegwo1QRYA7xjcZufMNe33rCclFszUYAuHe1fIN/3ZmiHeGPkUsNaRyQm1hHOfM0PKxA==", - "dev": true, - "requires": { - "kleur": "^3.0.2", - "sisteransi": "^1.0.0" - } - }, - "proto-list": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", - "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=" - }, - "proxyquire": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/proxyquire/-/proxyquire-1.8.0.tgz", - "integrity": "sha1-AtUUpb7ZhvBMuyCTrxZ0FTX3ntw=", - "dev": true, - "requires": { - "fill-keys": "^1.0.2", - "module-not-found-error": "^1.0.0", - "resolve": "~1.1.7" - }, - "dependencies": { - "resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", - "dev": true - } - } - }, - "pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" - }, - "psl": { - "version": "1.1.31", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz", - "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==", - "dev": true - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" - }, - "qs": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" - }, - "querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" - }, - "randomatic": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz", - "integrity": "sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw==", - "dev": true, - "requires": { - "is-number": "^4.0.0", - "kind-of": "^6.0.0", - "math-random": "^1.0.1" - }, - "dependencies": { - "is-number": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", - "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", - "dev": true - } - } - }, - "raven": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/raven/-/raven-1.2.1.tgz", - "integrity": "sha1-lJwTTbAooZC3u/j3kKrlQbfAIL0=", - "requires": { - "cookie": "0.3.1", - "json-stringify-safe": "5.0.1", - "lsmod": "1.0.0", - "stack-trace": "0.0.9", - "uuid": "3.0.0" - }, - "dependencies": { - "uuid": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.0.tgz", - "integrity": "sha1-Zyj8BFnEUNeWqZwxg3VpvfZy1yg=" - } - } - }, - "rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - } - }, - "react-is": { - "version": "16.8.6", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz", - "integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==", - "dev": true - }, - "read-pkg": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", - "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", - "dev": true, - "requires": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" - } - }, - "read-pkg-up": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-4.0.0.tgz", - "integrity": "sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==", - "dev": true, - "requires": { - "find-up": "^3.0.0", - "read-pkg": "^3.0.0" - }, - "dependencies": { - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - } - } - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "readline2": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/readline2/-/readline2-1.0.1.tgz", - "integrity": "sha1-QQWWCP/BVHV7cV2ZidGZ/783LjU=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "mute-stream": "0.0.5" - }, - "dependencies": { - "mute-stream": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz", - "integrity": "sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA=", - "dev": true - } - } - }, - "realpath-native": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-1.1.0.tgz", - "integrity": "sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA==", - "dev": true, - "requires": { - "util.promisify": "^1.0.0" - } - }, - "rechoir": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", - "dev": true, - "requires": { - "resolve": "^1.1.6" - } - }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - } - }, - "registry-auth-token": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.4.0.tgz", - "integrity": "sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A==", - "requires": { - "rc": "^1.1.6", - "safe-buffer": "^5.0.1" - } - }, - "registry-url": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", - "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", - "requires": { - "rc": "^1.0.1" - } - }, - "remarkable": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/remarkable/-/remarkable-1.7.1.tgz", - "integrity": "sha1-qspJchALZqZCpjoQIcpLrBvjv/Y=", - "dev": true, - "requires": { - "argparse": "~0.1.15", - "autolinker": "~0.15.0" - }, - "dependencies": { - "argparse": { - "version": "0.1.16", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-0.1.16.tgz", - "integrity": "sha1-z9AeD7uj1srtBJ+9dY1A9lGW9Xw=", - "dev": true, - "requires": { - "underscore": "~1.7.0", - "underscore.string": "~2.4.0" - } - } - } - }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" - }, - "repeat-element": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", - "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", - "dev": true - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true - }, - "replaceall": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/replaceall/-/replaceall-0.1.6.tgz", - "integrity": "sha1-gdgax663LX9cSUKt8ml6MiBojY4=" - }, - "request": { - "version": "2.88.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", - "dev": true, - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.0", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.4.3", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "dependencies": { - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", - "dev": true - }, - "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", - "dev": true - } - } - }, - "request-promise-core": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.2.tgz", - "integrity": "sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag==", - "dev": true, - "requires": { - "lodash": "^4.17.11" - } - }, - "request-promise-native": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.7.tgz", - "integrity": "sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w==", - "dev": true, - "requires": { - "request-promise-core": "1.1.2", - "stealthy-require": "^1.1.1", - "tough-cookie": "^2.3.3" - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true - }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true - }, - "require-uncached": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", - "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", - "dev": true, - "requires": { - "caller-path": "^0.1.0", - "resolve-from": "^1.0.0" - } - }, - "resolve": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.1.tgz", - "integrity": "sha512-KuIe4mf++td/eFb6wkaPbMDnP6kObCaEtIDuHOUED6MNUo4K670KZUHuuvYPZDxNF0WVLw49n06M2m2dXphEzA==", - "dev": true, - "requires": { - "path-parse": "^1.0.6" - } - }, - "resolve-cwd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", - "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", - "dev": true, - "requires": { - "resolve-from": "^3.0.0" - }, - "dependencies": { - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", - "dev": true - } - } - }, - "resolve-from": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", - "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", - "dev": true - }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" - }, - "restore-cursor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", - "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", - "requires": { - "exit-hook": "^1.0.0", - "onetime": "^1.0.0" - } - }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" - }, - "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "requires": { - "glob": "^7.1.3" - } - }, - "rsvp": { - "version": "4.8.4", - "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.4.tgz", - "integrity": "sha512-6FomvYPfs+Jy9TfXmBpBuMWNH94SgCsZmJKcanySzgNNP6LjWxBvyLTa9KaMfDDM5oxRfrKDB0r/qeRsLwnBfA==", - "dev": true - }, - "run-async": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", - "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", - "requires": { - "is-promise": "^2.1.0" - } - }, - "rx": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/rx/-/rx-4.1.0.tgz", - "integrity": "sha1-pfE/957zt0D+MKqAP7CfmIBdR4I=" - }, - "rx-lite": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-3.1.2.tgz", - "integrity": "sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI=", - "dev": true - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "requires": { - "ret": "~0.1.10" - } - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "samsam": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.1.2.tgz", - "integrity": "sha1-vsEf3IOp/aBjQBIQ5AF2wwJNFWc=", - "dev": true - }, - "sane": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/sane/-/sane-4.1.0.tgz", - "integrity": "sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==", - "dev": true, - "requires": { - "@cnakazawa/watch": "^1.0.3", - "anymatch": "^2.0.0", - "capture-exit": "^2.0.0", - "exec-sh": "^0.3.2", - "execa": "^1.0.0", - "fb-watchman": "^2.0.0", - "micromatch": "^3.1.4", - "minimist": "^1.1.1", - "walker": "~1.0.5" - }, - "dependencies": { - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - } - } - }, - "sax": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", - "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" - }, - "seek-bzip": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.5.tgz", - "integrity": "sha1-z+kXyz0nS8/6x5J1ivUxc+sfq9w=", - "requires": { - "commander": "~2.8.1" - } - }, - "semver": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", - "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" - }, - "semver-diff": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", - "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=", - "requires": { - "semver": "^5.0.3" - } - }, - "semver-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-1.0.0.tgz", - "integrity": "sha1-kqSWkGX5xwxpR1PVUkj8aPj2Usk=" - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true - }, - "set-getter": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/set-getter/-/set-getter-0.1.0.tgz", - "integrity": "sha1-12nBgsnVpR9AkUXy+6guXoboA3Y=", - "dev": true, - "requires": { - "to-object-path": "^0.3.0" - } - }, - "set-immediate-shim": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", - "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=" - }, - "set-value": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", - "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" - } - } - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" - }, - "shelljs": { - "version": "0.7.8", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.7.8.tgz", - "integrity": "sha1-3svPh0sNHl+3LhSxZKloMEjprLM=", - "dev": true, - "requires": { - "glob": "^7.0.0", - "interpret": "^1.0.0", - "rechoir": "^0.6.2" - } - }, - "shellwords": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", - "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", - "dev": true - }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" - }, - "sinon": { - "version": "1.17.7", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-1.17.7.tgz", - "integrity": "sha1-RUKk9JugxFwF6y6d2dID4rjv4L8=", - "dev": true, - "requires": { - "formatio": "1.1.1", - "lolex": "1.3.2", - "samsam": "1.1.2", - "util": ">=0.10.3 <1" - } - }, - "sinon-bluebird": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/sinon-bluebird/-/sinon-bluebird-3.1.0.tgz", - "integrity": "sha1-+SaA+lRtVTpPX2LekEJetdxHhB0=", - "dev": true - }, - "sinon-chai": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/sinon-chai/-/sinon-chai-2.14.0.tgz", - "integrity": "sha512-9stIF1utB0ywNHNT7RgiXbdmen8QDCRsrTjw+G9TgKt1Yexjiv8TOWZ6WHsTPz57Yky3DIswZvEqX8fpuHNDtQ==", - "dev": true - }, - "sisteransi": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.0.tgz", - "integrity": "sha512-N+z4pHB4AmUv0SjveWRd6q1Nj5w62m5jodv+GD8lvmbY/83T/rpbJGZOnK5T149OldDj4Db07BSv9xY4K6NTPQ==", - "dev": true - }, - "slash": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", - "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=" - }, - "slice-ansi": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", - "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", - "dev": true - }, - "snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "requires": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - } - } - }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "dev": true, - "requires": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - } - } - }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "dev": true, - "requires": { - "kind-of": "^3.2.0" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" - }, - "source-map-resolve": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", - "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", - "requires": { - "atob": "^2.1.1", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, - "source-map-support": { - "version": "0.5.12", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.12.tgz", - "integrity": "sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "source-map-url": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" - }, - "spawn-sync": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/spawn-sync/-/spawn-sync-1.0.15.tgz", - "integrity": "sha1-sAeZVX63+wyDdsKdROih6mfldHY=", - "requires": { - "concat-stream": "^1.4.7", - "os-shim": "^0.1.2" - } - }, - "spdx-correct": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", - "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", - "dev": true, - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", - "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", - "dev": true - }, - "spdx-expression-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", - "dev": true, - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.4.tgz", - "integrity": "sha512-7j8LYJLeY/Yb6ACbQ7F76qy5jHkp0U6jgBfJsk97bwWlVUnUWsAgpyaCvo17h0/RQGnQ036tVDomiwoI4pDkQA==", - "dev": true - }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "requires": { - "extend-shallow": "^3.0.0" - } - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" - }, - "sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "dev": true, - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "stack-trace": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.9.tgz", - "integrity": "sha1-qPbq7KkGdMMz58Q5U/J1tFFRBpU=" - }, - "stack-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.2.tgz", - "integrity": "sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA==", - "dev": true - }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "requires": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" - } - } - }, - "stealthy-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", - "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", - "dev": true - }, - "string-length": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-2.0.0.tgz", - "integrity": "sha1-1A27aGo6zpYMHP/KVivyxF+DY+0=", - "dev": true, - "requires": { - "astral-regex": "^1.0.0", - "strip-ansi": "^4.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - }, - "dependencies": { - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "^2.0.0" - } - } - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - } - } - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - }, - "strip-color": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/strip-color/-/strip-color-0.1.0.tgz", - "integrity": "sha1-EG9l09PmotlAHKwOsM6LinArT3s=", - "dev": true - }, - "strip-dirs": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", - "integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==", - "requires": { - "is-natural-number": "^4.0.1" - } - }, - "strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" - }, - "strip-outer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", - "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", - "requires": { - "escape-string-regexp": "^1.0.2" - } - }, - "superagent": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", - "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", - "requires": { - "component-emitter": "^1.2.0", - "cookiejar": "^2.1.0", - "debug": "^3.1.0", - "extend": "^3.0.0", - "form-data": "^2.3.1", - "formidable": "^1.2.0", - "methods": "^1.1.1", - "mime": "^1.4.1", - "qs": "^6.5.1", - "readable-stream": "^2.3.5" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - }, - "symbol-tree": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.2.tgz", - "integrity": "sha1-rifbOPZgp64uHDt9G8KQgZuFGeY=", - "dev": true - }, - "sync-request": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/sync-request/-/sync-request-3.0.1.tgz", - "integrity": "sha1-yqEjWq+Im6UBB2oYNMQ2gwqC+3M=", - "dev": true, - "requires": { - "concat-stream": "^1.4.7", - "http-response-object": "^1.0.1", - "then-request": "^2.0.1" - } - }, - "table": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/table/-/table-3.8.3.tgz", - "integrity": "sha1-K7xULw/amGGnVdOUf+/Ys/UThV8=", - "dev": true, - "requires": { - "ajv": "^4.7.0", - "ajv-keywords": "^1.0.0", - "chalk": "^1.1.1", - "lodash": "^4.0.0", - "slice-ansi": "0.0.4", - "string-width": "^2.0.0" - }, - "dependencies": { - "ajv": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", - "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", - "dev": true, - "requires": { - "co": "^4.6.0", - "json-stable-stringify": "^1.0.1" - } - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } - }, - "tabtab": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/tabtab/-/tabtab-2.2.2.tgz", - "integrity": "sha1-egR/FDsBC0y9MfhX6ClhUSy/ThQ=", - "requires": { - "debug": "^2.2.0", - "inquirer": "^1.0.2", - "lodash.difference": "^4.5.0", - "lodash.uniq": "^4.5.0", - "minimist": "^1.2.0", - "mkdirp": "^0.5.1", - "npmlog": "^2.0.3", - "object-assign": "^4.1.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - } - } - }, - "tar-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", - "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", - "requires": { - "bl": "^1.0.0", - "buffer-alloc": "^1.2.0", - "end-of-stream": "^1.0.0", - "fs-constants": "^1.0.0", - "readable-stream": "^2.3.0", - "to-buffer": "^1.1.1", - "xtend": "^4.0.0" - } - }, - "term-size": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", - "integrity": "sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=", - "requires": { - "execa": "^0.7.0" - } - }, - "test-exclude": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-5.2.3.tgz", - "integrity": "sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g==", - "dev": true, - "requires": { - "glob": "^7.1.3", - "minimatch": "^3.0.4", - "read-pkg-up": "^4.0.0", - "require-main-filename": "^2.0.0" - } - }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", - "dev": true - }, - "then-request": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/then-request/-/then-request-2.2.0.tgz", - "integrity": "sha1-ZnizL6DKIY/laZgbvYhxtZQGDYE=", - "dev": true, - "requires": { - "caseless": "~0.11.0", - "concat-stream": "^1.4.7", - "http-basic": "^2.5.1", - "http-response-object": "^1.1.0", - "promise": "^7.1.1", - "qs": "^6.1.0" - }, - "dependencies": { - "caseless": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", - "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=", - "dev": true - } - } - }, - "throat": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/throat/-/throat-4.1.0.tgz", - "integrity": "sha1-iQN8vJLFarGJJua6TLsgDhVnKmo=", - "dev": true - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" - }, - "timed-out": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", - "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=" - }, - "tmp": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.29.tgz", - "integrity": "sha1-8lEl/w3Z2jzLDC3Tce4SiLuRKMA=", - "requires": { - "os-tmpdir": "~1.0.1" - } - }, - "tmpl": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", - "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=", - "dev": true - }, - "to-buffer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", - "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==" - }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true - }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "requires": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - } - }, - "toml": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/toml/-/toml-2.3.6.tgz", - "integrity": "sha512-gVweAectJU3ebq//Ferr2JUY4WKSDe5N+z0FvjDncLGyHmIDoxgY/2Ie4qfEIDm4IS7OA6Rmdm7pdEEdMcV/xQ==", - "dev": true - }, - "tough-cookie": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", - "dev": true, - "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" - }, - "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true - } - } - }, - "tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", - "dev": true, - "requires": { - "punycode": "^2.1.0" - }, - "dependencies": { - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true - } - } - }, - "trim-repeated": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", - "integrity": "sha1-42RqLqTokTEr9+rObPsFOAvAHCE=", - "requires": { - "escape-string-regexp": "^1.0.2" - } - }, - "trim-right": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", - "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", - "dev": true - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "dev": true - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2" - } - }, - "type-detect": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-1.0.0.tgz", - "integrity": "sha1-diIXzAbbJY7EiQihKY6LlRIejqI=", - "dev": true - }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" - }, - "uglify-js": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.5.12.tgz", - "integrity": "sha512-KeQesOpPiZNgVwJj8Ge3P4JYbQHUdZzpx6Fahy6eKAYRSV4zhVmLXoC+JtOeYxcHCHTve8RG1ZGdTvpeOUM26Q==", - "dev": true, - "optional": true, - "requires": { - "commander": "~2.20.0", - "source-map": "~0.6.1" - }, - "dependencies": { - "commander": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", - "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", - "dev": true, - "optional": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true - } - } - }, - "unbzip2-stream": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.3.3.tgz", - "integrity": "sha512-fUlAF7U9Ah1Q6EieQ4x4zLNejrRvDWUYmxXUpN3uziFYCHapjWFaCAnreY9bGgxzaMCFAPPpYNng57CypwJVhg==", - "requires": { - "buffer": "^5.2.1", - "through": "^2.3.8" - } - }, - "underscore": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz", - "integrity": "sha1-a7rwh3UA02vjTsqlhODbn+8DUgk=", - "dev": true - }, - "underscore.string": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.4.0.tgz", - "integrity": "sha1-jN2PusTi0uoefi6Al8QvRCKA+Fs=", - "dev": true - }, - "union-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", - "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", - "requires": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^0.4.3" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" - }, - "set-value": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", - "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.1", - "to-object-path": "^0.3.0" - } - } - } - }, - "unique-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", - "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", - "requires": { - "crypto-random-string": "^1.0.0" - } - }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "requires": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "requires": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" - } - } - }, - "untildify": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/untildify/-/untildify-3.0.3.tgz", - "integrity": "sha512-iSk/J8efr8uPT/Z4eSUywnqyrQU7DSdMfdqK4iWEaUVVmcP5JcnpRqmVMwcwcnmI1ATFNgC5V90u09tBynNFKA==" - }, - "unzip-response": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", - "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=" - }, - "update-notifier": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-2.5.0.tgz", - "integrity": "sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==", - "requires": { - "boxen": "^1.2.1", - "chalk": "^2.0.1", - "configstore": "^3.0.0", - "import-lazy": "^2.1.0", - "is-ci": "^1.0.10", - "is-installed-globally": "^0.1.0", - "is-npm": "^1.0.0", - "latest-version": "^3.0.0", - "semver-diff": "^2.0.0", - "xdg-basedir": "^3.0.0" - } - }, - "uri-js": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-3.0.2.tgz", - "integrity": "sha1-+QuFhQf4HepNz7s8TD2/orVX+qo=", - "requires": { - "punycode": "^2.1.0" - }, - "dependencies": { - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" - } - } - }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" - }, - "url": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", - "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", - "requires": { - "punycode": "1.3.2", - "querystring": "0.2.0" - } - }, - "url-parse-lax": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", - "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", - "requires": { - "prepend-http": "^1.0.1" - } - }, - "url-to-options": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz", - "integrity": "sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k=" - }, - "use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" - }, - "user-home": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz", - "integrity": "sha1-nHC/2Babwdy/SGBODwS4tJzenp8=", - "dev": true, - "requires": { - "os-homedir": "^1.0.0" - } - }, - "util": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.0.tgz", - "integrity": "sha512-pPSOFl7VLhZ7LO/SFABPraZEEurkJUWSMn3MuA/r3WQZc+Z1fqou2JqLSOZbCLl73EUIxuUVX8X4jkX2vfJeAA==", - "dev": true, - "requires": { - "inherits": "2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "object.entries": "^1.1.0", - "safe-buffer": "^5.1.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "util.promisify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", - "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "object.getownpropertydescriptors": "^2.0.3" - } - }, - "uuid": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz", - "integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=" - }, - "validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "w3c-hr-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz", - "integrity": "sha1-gqwr/2PZUOqeMYmlimViX+3xkEU=", - "dev": true, - "requires": { - "browser-process-hrtime": "^0.1.2" - } - }, - "walkdir": { - "version": "0.0.11", - "resolved": "https://registry.npmjs.org/walkdir/-/walkdir-0.0.11.tgz", - "integrity": "sha1-oW0CXrkxvQO1LzCMrtD0D86+lTI=" - }, - "walker": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", - "integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=", - "dev": true, - "requires": { - "makeerror": "1.0.x" - } - }, - "webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", - "dev": true - }, - "whatwg-encoding": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", - "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", - "dev": true, - "requires": { - "iconv-lite": "0.4.24" - } - }, - "whatwg-mimetype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", - "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", - "dev": true - }, - "whatwg-url": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz", - "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==", - "dev": true, - "requires": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "requires": { - "isexe": "^2.0.0" - } - }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", - "dev": true - }, - "widest-line": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz", - "integrity": "sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA==", - "requires": { - "string-width": "^2.1.1" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", - "dev": true - }, - "wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", - "dev": true, - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" - }, - "dependencies": { - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - } - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "write": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", - "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", - "dev": true, - "requires": { - "mkdirp": "^0.5.1" - } - }, - "write-file-atomic": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.2.tgz", - "integrity": "sha512-s0b6vB3xIVRLWywa6X9TOMA7k9zio0TMOsl9ZnDkliA/cfJlpHXAscj0gbHVJiTdIuAYpIyqS5GW91fqm6gG5g==", - "requires": { - "graceful-fs": "^4.1.11", - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.2" - } - }, - "ws": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz", - "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==", - "dev": true, - "requires": { - "async-limiter": "~1.0.0" - } - }, - "xdg-basedir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", - "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=" - }, - "xml-name-validator": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", - "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", - "dev": true - }, - "xml2js": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", - "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", - "requires": { - "sax": ">=0.6.0", - "xmlbuilder": "~9.0.1" - } - }, - "xmlbuilder": { - "version": "9.0.7", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", - "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" - }, - "xtend": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" - }, - "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", - "dev": true - }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" - }, - "yaml-ast-parser": { - "version": "0.0.34", - "resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.34.tgz", - "integrity": "sha1-0A88+ddztyQUCa6SpnQNHbGfSeY=" - }, - "yargs": { - "version": "12.0.5", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", - "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", - "dev": true, - "requires": { - "cliui": "^4.0.0", - "decamelize": "^1.2.0", - "find-up": "^3.0.0", - "get-caller-file": "^1.0.1", - "os-locale": "^3.0.0", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^2.0.0", - "which-module": "^2.0.0", - "y18n": "^3.2.1 || ^4.0.0", - "yargs-parser": "^11.1.1" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", - "dev": true - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "yargs-parser": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", - "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, - "dependencies": { - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - } - } - }, - "yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", - "requires": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, - "zip-stream": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-1.2.0.tgz", - "integrity": "sha1-qLxF9MG0lpnGuQGYuqyqzbzUugQ=", - "requires": { - "archiver-utils": "^1.3.0", - "compress-commons": "^1.2.0", - "lodash": "^4.8.0", - "readable-stream": "^2.0.0" - } - } - } -} diff --git a/package.json b/package.json index dd3bdcab0..6aee4e928 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,10 @@ { "name": "serverless", - "version": "1.42.3", + "version": "1.47.0", "engines": { - "node": ">=4.0" + "node": ">=6.0" }, + "engineStrict": true, "preferGlobal": true, "homepage": "https://github.com/serverless/serverless#readme", "description": "Serverless Framework - Build web, mobile and IoT applications with serverless architectures using AWS Lambda, Azure Functions, Google CloudFunctions & more", @@ -39,99 +40,121 @@ "scripts/preuninstall.js", "scripts/pre-release.js", "package.json", - "package-lock.json", "README.md", "LICENSE.txt", "CHANGELOG.md" ], "main": "lib/Serverless.js", "bin": { - "serverless": "./bin/serverless", - "slss": "./bin/serverless", - "sls": "./bin/serverless" + "serverless": "./bin/serverless.js", + "slss": "./bin/serverless.js", + "sls": "./bin/serverless.js" }, "scripts": { - "test-bare": "node bin/test", - "test": "istanbul cover -x \"**/*.test.js\" bin/test", + "test": "mocha \"!(node_modules)/**/*.test.js\"", + "test-ci": "npm run prettier-check-updated && npm run lint-updated && npm run test-isolated", + "test-isolated": "node scripts/test-isolated.js", + "coverage": "nyc --reporter=lcov --reporter=html --reporter=text-summary npm test", "lint": "eslint . --cache", - "docs": "node scripts/generate-readme.js", - "integration-test-cleanup": "node scripts/integration-test-cleanup.js", - "simple-integration-test": "jest --maxWorkers=5 simple-suite", - "complex-integration-test": "jest --maxWorkers=5 integration", - "postinstall": "node ./scripts/postinstall.js" + "lint:fix": "npm run lint -- --fix", + "lint-updated": "pipe-git-updated --ext=js -- eslint --cache", + "integration-test-run-package": "jest --maxWorkers=5 integration-package", + "integration-test-run-basic": "jest --maxWorkers=5 integration-basic", + "integration-test-run-all": "jest --maxWorkers=5 integration-all", + "integration-test-cleanup": "node tests/utils/aws-cleanup.js", + "postinstall": "node ./scripts/postinstall.js", + "prettier-check": "prettier -c --ignore-path .gitignore \"**/*.{css,html,js,json,md,yaml,yml}\"", + "prettier-check-updated": "pipe-git-updated --ext=css --ext=html --ext=js --ext=json --ext=md --ext=yaml --ext=yml -- prettier -c", + "prettify": "prettier --write --ignore-path .gitignore \"**/*.{css,html,js,json,md,yaml,yml}\"", + "prettify-updated": "pipe-git-updated --ext=css --ext=html --ext=js --ext=json --ext=md --ext=yaml --ext=yml -- prettier --write" + }, + "eslintConfig": { + "extends": "@serverless/eslint-config/node", + "root": true + }, + "eslintIgnore": [ + "lib/plugins/create/templates/**" + ], + "mocha": { + "reporter": "tests/mocha-reporter", + "timeout": 5000 }, "jest": { - "testRegex": "(\\.|/)(tests)\\.js$", - "testEnvironment": "node", "setupFilesAfterEnv": [ - "/tests/setupTests.js" - ] + "/tests/setup-tests.js" + ], + "testEnvironment": "node", + "testRegex": "(\\.|/)(tests)\\.js$", + "testRunner": "jest-circus/runner", + "useStderr": true }, "devDependencies": { - "chai": "^3.5.0", - "chai-as-promised": "^6.0.0", - "coveralls": "^3.0.3", - "eslint": "^3.3.1", - "eslint-config-airbnb": "^10.0.1", - "eslint-config-airbnb-base": "^5.0.2", - "eslint-plugin-import": "^1.13.0", - "eslint-plugin-jsx-a11y": "^2.1.0", - "eslint-plugin-react": "^6.1.1", - "istanbul": "^0.4.4", - "jest-cli": "^24.5.0", - "markdown-link": "^0.1.1", - "markdown-magic": "^0.1.25", - "markdown-table": "^1.1.1", - "mocha": "^5.2.0", - "mocha-lcov-reporter": "^1.2.0", - "mock-require": "^1.3.0", - "parse-github-url": "^1.0.1", - "proxyquire": "^1.7.10", - "sinon": "^1.17.5", - "sinon-bluebird": "^3.1.0", - "sinon-chai": "^2.9.0", + "@serverless/eslint-config": "^1.0.1", + "chai": "^4.2.0", + "chai-as-promised": "^7.1.1", + "child-process-ext": "^2.0.0", + "cli-progress-footer": "^1.1.1", + "coveralls": "^3.0.4", + "eslint": "^6.0.1", + "eslint-plugin-import": "^2.18.0", + "git-list-updated": "^1.2.0", + "jest-circus": "^24.8.0", + "jest-cli": "^24.8.0", + "mocha": "^6.1.4", + "mocha-lcov-reporter": "^1.3.0", + "mock-require": "^3.0.3", + "nyc": "^14.1.1", + "p-limit": "^2.2.0", + "prettier": "^1.18.2", + "process-utils": "^2.3.1", + "proxyquire": "^2.1.0", + "sinon": "^7.3.2", + "sinon-chai": "^3.3.0", "strip-ansi": "^5.2.0" }, "dependencies": { - "archiver": "^1.1.0", + "@serverless/enterprise-plugin": "^1.2.0", + "archiver": "^1.3.0", "async": "^1.5.2", - "aws-sdk": "^2.430.0", - "bluebird": "^3.5.0", + "aws-sdk": "^2.490.0", + "bluebird": "^3.5.5", "cachedir": "^2.2.0", - "chalk": "^2.0.0", - "ci-info": "^1.1.1", - "download": "^5.0.2", + "chalk": "^2.4.2", + "ci-info": "^1.6.0", + "download": "^5.0.3", "fast-levenshtein": "^2.0.6", - "filesize": "^3.3.0", + "filesize": "^3.6.1", "fs-extra": "^0.26.7", "get-stdin": "^5.0.1", "globby": "^6.1.0", - "graceful-fs": "^4.1.11", - "https-proxy-agent": "^2.2.1", + "graceful-fs": "^4.2.0", + "https-proxy-agent": "^2.2.2", + "inquirer": "^6.4.1", "is-docker": "^1.1.0", - "js-yaml": "^3.13.0", + "js-yaml": "^3.13.1", "json-cycle": "^1.3.0", - "json-refs": "^2.1.5", - "jszip": "^3.2.1", + "json-refs": "^2.1.7", + "jszip": "^3.2.2", "jwt-decode": "^2.2.0", - "lodash": "^4.13.1", + "lodash": "^4.17.13", "minimist": "^1.2.0", "mkdirp": "^0.5.1", - "moment": "^2.13.0", + "moment": "^2.24.0", "nanomatch": "^1.2.13", - "node-fetch": "^1.6.0", - "object-hash": "^1.2.0", - "promise-queue": "^2.2.3", + "ncjsm": "^2.3.0", + "node-fetch": "^1.7.3", + "object-hash": "^1.3.1", + "promise-queue": "^2.2.5", "raven": "^1.2.1", - "rc": "^1.1.6", + "rc": "^1.2.8", "replaceall": "^0.1.6", "semver": "^5.7.0", "semver-regex": "^1.0.0", "tabtab": "^2.2.2", "untildify": "^3.0.3", - "update-notifier": "^2.2.0", - "uuid": "^2.0.2", - "write-file-atomic": "^2.1.0", + "update-notifier": "^2.5.0", + "uuid": "^2.0.3", + "write-file-atomic": "^2.4.3", "yaml-ast-parser": "0.0.34" } } diff --git a/prettier.config.js b/prettier.config.js new file mode 100644 index 000000000..5f45afa55 --- /dev/null +++ b/prettier.config.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('@serverless/eslint-config/prettier.config'); diff --git a/scripts/generate-readme.js b/scripts/generate-readme.js deleted file mode 100644 index 9c9454a2c..000000000 --- a/scripts/generate-readme.js +++ /dev/null @@ -1,73 +0,0 @@ -'use strict'; - -/** - * adds content to the repos README.md file - */ -const path = require('path'); -const _ = require('lodash'); -const mdTable = require('markdown-table'); -const parseGithubURL = require('parse-github-url'); -const mdLink = require('markdown-link'); -const markdownMagic = require('markdown-magic'); -const remoteRequest = require('markdown-magic/lib/utils/remoteRequest'); - -function getExamplesList() { - const examplesUrl = 'https://raw.githubusercontent.com/serverless/examples/master/community-examples.json'; - const remoteContent = remoteRequest(examplesUrl); - - return JSON.parse(remoteContent); -} - -function getPluginsList() { - const pluginUrl = 'https://raw.githubusercontent.com/serverless/plugins/master/plugins.json'; - const remoteContent = remoteRequest(pluginUrl); - - return JSON.parse(remoteContent).sort((a, b) => // eslint-disable-line - a.name < b.name ? -1 : 1 - ); -} - -function getTableRow(name, description, url) { - const { owner } = parseGithubURL(url); - const profileURL = `http://github.com/${owner}`; - - return [ - `**${mdLink(_.startCase(name), url)}**
    ${description}`, - `${mdLink(owner, profileURL)}`, - ]; -} - -function getReadmeTable(rowsData, columns) { - const mdTableData = [columns]; - - rowsData.forEach(({ name, description, githubUrl }) => { - const tableRow = getTableRow(name, description, githubUrl); - mdTableData.push(tableRow); - }); - - return mdTable(mdTableData, { - align: ['l', 'c'], - pad: false, - }); -} - -const config = { - transforms: { - GENERATE_SERVERLESS_EXAMPLES_TABLE(content, options) { // eslint-disable-line - const examplesList = getExamplesList(); - - return getReadmeTable(examplesList, ['Project Name', 'Author']); - }, - GENERATE_SERVERLESS_PLUGIN_TABLE(content, options) { // eslint-disable-line - const pluginsList = getPluginsList(); - - return getReadmeTable(pluginsList, ['Plugin', 'Author']); - }, - }, -}; - -const markdownPath = path.join(__dirname, '..', 'README.md'); -// const markdownPath = path.join(__dirname, '..', 'test/fixtures/test.md') -markdownMagic(markdownPath, config, () => { - console.log(`${markdownPath} updated!`); // eslint-disable-line -}); diff --git a/scripts/integration-test-cleanup.js b/scripts/integration-test-cleanup.js deleted file mode 100644 index 35276c64f..000000000 --- a/scripts/integration-test-cleanup.js +++ /dev/null @@ -1,97 +0,0 @@ -'use strict'; - -const BbPromise = require('bluebird'); -const AWS = require('aws-sdk'); - -const CF = new AWS.CloudFormation({ region: 'us-east-1' }); -const S3 = new AWS.S3({ region: 'us-east-1' }); - -BbPromise.promisifyAll(CF, { suffix: 'Promised' }); -BbPromise.promisifyAll(S3, { suffix: 'Promised' }); - -const logger = console; - -const emptyS3Bucket = (bucket) => ( - S3.listObjectsPromised({ Bucket: bucket }) - .then(data => { - logger.log('Bucket', bucket, 'has', data.Contents.length, 'items'); - if (data.Contents.length) { - const keys = data.Contents.map(item => Object.assign({}, { Key: item.Key })); - return S3.deleteObjectsPromised({ - Bucket: bucket, - Delete: { - Objects: keys, - }, - }); - } - return BbPromise.resolve(); - }) -); - -const deleteS3Bucket = (bucket) => ( - emptyS3Bucket(bucket) - .then(() => { - logger.log('Bucket', bucket, 'is now empty, deleting ...'); - return S3.deleteBucketPromised({ Bucket: bucket }); - }) -); - -const cleanupS3Buckets = (token) => { - logger.log('Looking through buckets ...'); - - const params = {}; - - if (token) { - params.NextToken = token; - } - - return S3.listBucketsPromised() - .then(response => - response.Buckets.reduce((memo, bucket) => memo - .then(() => deleteS3Bucket(bucket.Name)), BbPromise.resolve()) - .then(() => { - if (response.NextToken) { - return cleanupS3Buckets(response.NextToken); - } - return BbPromise.resolve(); - }) - ); -}; - -const cleanupCFStacks = (token) => { - const params = { - StackStatusFilter: [ - 'CREATE_FAILED', - 'CREATE_COMPLETE', - 'ROLLBACK_FAILED', - 'ROLLBACK_COMPLETE', - 'DELETE_FAILED', - 'UPDATE_ROLLBACK_FAILED', - 'UPDATE_ROLLBACK_COMPLETE', - ], - }; - - if (token) { - params.NextToken = token; - } - - logger.log('Looking through stacks ...'); - return CF.listStacksPromised(params) - .then(response => - response.StackSummaries.reduce((memo, stack) => { - if (['DELETE_COMPLETE', 'DELETE_IN_PROGRESS'].indexOf(stack.StackStatus) === -1) { - logger.log('Deleting stack', stack.StackName); - return memo.then(() => CF.deleteStackPromised({ StackName: stack.StackName })); - } - return memo; - }, BbPromise.resolve()) - .then(() => { - if (response.NextToken) { - return cleanupCFStacks(response.NextToken); - } - return BbPromise.resolve(); - }) - ); -}; - -cleanupS3Buckets().then(cleanupCFStacks); diff --git a/scripts/postinstall.js b/scripts/postinstall.js index 1a6324cef..ed322e724 100644 --- a/scripts/postinstall.js +++ b/scripts/postinstall.js @@ -1,21 +1,37 @@ 'use strict'; const path = require('path'); +const chalk = require('chalk'); /* eslint-disable no-console */ -/* eslint-disable no-use-before-define */ const Serverless = require('../lib/Serverless'); const execSync = require('child_process').execSync; +const truthyStr = val => val && !['0', 'false', 'f', 'n', 'no'].includes(val.toLowerCase()); +const { CI, ADBLOCK, SILENT } = process.env; +if (!truthyStr(CI) && !truthyStr(ADBLOCK) && !truthyStr(SILENT)) { + console.log( + chalk.yellow(`\ + +------------------------------------------------------------------+ + | | + | Serverless Framework successfully installed! | + | To start your first project, run “serverless” in a new folder. | + | | + +------------------------------------------------------------------+ +`) + ); +} + try { const serverless = new Serverless(); - (() => serverless.init() - .then(() => serverless.utils.logStat(serverless, 'install')) - .then(() => setupAutocomplete()) - .catch(() => Promise.resolve()) - )(); + (() => + serverless + .init() + .then(() => serverless.utils.logStat(serverless, 'install')) + .then(() => setupAutocomplete()) + .catch(() => Promise.resolve()))(); } catch (error) { // fail silently } diff --git a/scripts/preuninstall.js b/scripts/preuninstall.js index cd04648c1..3a75dcc73 100644 --- a/scripts/preuninstall.js +++ b/scripts/preuninstall.js @@ -5,10 +5,11 @@ const Serverless = require('../lib/Serverless'); try { const serverless = new Serverless(); - (() => serverless.init() - .then(() => serverless.utils.logStat(serverless, 'uninstall')) - .catch(() => Promise.resolve()) - )(); + (() => + serverless + .init() + .then(() => serverless.utils.logStat(serverless, 'uninstall')) + .catch(() => Promise.resolve()))(); } catch (error) { // fail silently } diff --git a/scripts/prs-since-last-tag b/scripts/prs-since-last-tag index 17b9a8d3d..90c9575dd 100755 --- a/scripts/prs-since-last-tag +++ b/scripts/prs-since-last-tag @@ -12,12 +12,12 @@ version=$(jq -r .version package.json) token=$(git config --global github.token) echo "# $version ($(date +%F))" +echo for pr in $(git log --reverse --grep "Merge pull request" "$last_tag"..HEAD | sed -nEe 's/.*#([0-9]+).*/\1/p'); do title=$(curl -s https://$token@api.github.com/repos/serverless/serverless/pulls/$pr | jq -r .title) - echo " - [$title](https://github.com/serverless/serverless/pull/$pr)" + echo "- [$title](https://github.com/serverless/serverless/pull/$pr)" done echo echo "## Meta" -echo " - [Comparison since last release](https://github.com/serverless/serverless/compare/$last_tag...v$version)" -echo +echo "- [Comparison since last release](https://github.com/serverless/serverless/compare/$last_tag...v$version)" echo diff --git a/scripts/shrinkwrap b/scripts/shrinkwrap deleted file mode 100755 index bef5c337b..000000000 --- a/scripts/shrinkwrap +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -# NOTE this script should only be executed from the root directory (not within "scripts") - -rm -rf node_modules -rm -f npm-shrinkwrap.json -npm install -npm prune --production -npm shrinkwrap -rm -f package-lock.json diff --git a/scripts/test-isolated.js b/scripts/test-isolated.js new file mode 100755 index 000000000..af2138270 --- /dev/null +++ b/scripts/test-isolated.js @@ -0,0 +1,114 @@ +#!/usr/bin/env node + +// Basic isolated tests runner +// Ensures each test file is run in distinct process and does not interfere with other test runs. +// To be used to confirm test files do not introduce and work by chance of side effects +// Temporary solution until we migrate to runner which provides that (reliably) on its own + +'use strict'; + +process.on('unhandledRejection', err => { + throw err; +}); + +const globby = require('globby'); +const spawn = require('child-process-ext/spawn'); +const chalk = require('chalk'); +const pLimit = require('p-limit'); + +const shouldApplyFsCleanupCheck = (() => { + const argIndex = process.argv.indexOf('--skip-fs-cleanup-check'); + if (argIndex === -1) return true; + process.argv.splice(argIndex, 1); + return false; +})(); + +const patterns = process.argv.length <= 2 ? ['**/*.test.js'] : process.argv.slice(2); +patterns.push('!node_modules/**'); + +const resolveGitStatus = () => + spawn('git', ['status', '--porcelain']).then( + ({ stdoutBuffer }) => String(stdoutBuffer), + error => { + process.stdout.write(error.stdoutBuffer); + process.stderr.write(error.stderrBuffer); + throw error; + } + ); + +const initialGitStatusDeferred = shouldApplyFsCleanupCheck ? resolveGitStatus() : null; + +const initialSetupDeferred = shouldApplyFsCleanupCheck + ? initialGitStatusDeferred + : Promise.resolve(); + +globby(patterns).then(paths => { + if (!paths.length) { + process.stderr.write(chalk.red.bold('No test files matched\n\n')); + process.exit(1); + } + + const processesCount = shouldApplyFsCleanupCheck + ? 1 + : Math.max(require('os').cpus().length - 1, 1); + + const isMultiProcessRun = processesCount > 1; + + const { ongoing, cliFooter } = (() => { + if (!isMultiProcessRun) return {}; + return { ongoing: new Set(), cliFooter: require('cli-progress-footer')() }; + })(); + + const run = path => { + if (isMultiProcessRun) { + ongoing.add(path); + cliFooter.updateProgress(Array.from(ongoing)); + } + + const onFinally = (() => { + if (isMultiProcessRun) { + return ({ stdoutBuffer, stderrBuffer }) => { + ongoing.delete(path); + cliFooter.updateProgress(Array.from(ongoing)); + process.stdout.write(stdoutBuffer); + process.stderr.write(stderrBuffer); + return Promise.resolve(); + }; + } + if (!shouldApplyFsCleanupCheck) return () => Promise.resolve(); + return () => + Promise.all([initialGitStatusDeferred, resolveGitStatus()]).then( + ([initialStatus, currentStatus]) => { + if (initialStatus !== currentStatus) { + process.stderr.write( + chalk.red.bold(`${path} didn't clean created temporary files\n\n`) + ); + process.exit(1); + } + } + ); + })(); + + return spawn('npx', ['mocha', path], { + stdio: isMultiProcessRun ? null : 'inherit', + env: { + APPDATA: process.env.APPDATA, + FORCE_COLOR: '1', + HOME: process.env.HOME, + PATH: process.env.PATH, + TMPDIR: process.env.TMPDIR, + USERPROFILE: process.env.USERPROFILE, + }, + }).then(onFinally, error => { + if (isMultiProcessRun) ongoing.clear(); + return onFinally(error).then(() => { + process.stderr.write(chalk.red.bold(`${path} failed\n\n`)); + if (error.code <= 2) process.exit(error.code); + throw error; + }); + }); + }; + + const limit = pLimit(processesCount); + return initialSetupDeferred.then(() => Promise.all(paths.map(path => limit(() => run(path))))); +}); diff --git a/scripts/update-dep-shrinkwrap b/scripts/update-dep-shrinkwrap deleted file mode 100755 index 997dec345..000000000 --- a/scripts/update-dep-shrinkwrap +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -set -e - -rm -fr node_modules - -docker pull node:latest -docker-compose run serverless-node npm install -docker-compose run serverless-node npm shrinkwrap diff --git a/tests/.eslintrc.js b/tests/.eslintrc.js new file mode 100644 index 000000000..9938cda39 --- /dev/null +++ b/tests/.eslintrc.js @@ -0,0 +1,9 @@ +module.exports = { + parserOptions: { + ecmaVersion: 2017, + }, + rules: { + // console.info allowed to report on long going tasks or valuable debug information + 'no-console': ['error', { allow: ['info'] }], + }, +}; diff --git a/tests/integration-all/api-gateway/service/core.js b/tests/integration-all/api-gateway/service/core.js new file mode 100644 index 000000000..24b040dab --- /dev/null +++ b/tests/integration-all/api-gateway/service/core.js @@ -0,0 +1,68 @@ +'use strict'; + +async function minimal(event) { + return { + statusCode: 200, + body: JSON.stringify({ + message: 'Hello from API Gateway! - (minimal)', + event, + }), + }; +} + +async function cors(event) { + return { + statusCode: 200, + headers: { + 'Access-Control-Allow-Origin': '*', + }, + body: JSON.stringify({ + message: 'Hello from API Gateway! - (cors)', + event, + }), + }; +} + +async function customAuthorizers(event) { + return { + statusCode: 200, + body: JSON.stringify({ + message: 'Hello from API Gateway! - (customAuthorizers)', + event, + }), + }; +} + +async function apiKeys(event) { + return { + statusCode: 200, + body: JSON.stringify({ + message: 'Hello from API Gateway! - (apiKeys)', + event, + }), + }; +} + +async function timeout(event) { + return new Promise(resolve => + setTimeout( + () => + resolve({ + statusCode: 200, + body: JSON.stringify({ + message: 'Should not happen (timeout expected)', + event, + }), + }), + 2000 + ) + ); +} + +module.exports = { + minimal, + cors, + customAuthorizers, + apiKeys, + timeout, +}; diff --git a/tests/integration/aws/api-gateway/integration-lambda/custom-authorizers/service/handler.js b/tests/integration-all/api-gateway/service/helper.js similarity index 63% rename from tests/integration/aws/api-gateway/integration-lambda/custom-authorizers/service/handler.js rename to tests/integration-all/api-gateway/service/helper.js index d47160ae6..000efbc5c 100644 --- a/tests/integration/aws/api-gateway/integration-lambda/custom-authorizers/service/handler.js +++ b/tests/integration-all/api-gateway/service/helper.js @@ -1,6 +1,7 @@ 'use strict'; -const generatePolicy = (principalId, effect, resource) => { +// custom authorizer +function generatePolicy(principalId, effect, resource) { const authResponse = {}; authResponse.principalId = principalId; @@ -18,20 +19,18 @@ const generatePolicy = (principalId, effect, resource) => { } return authResponse; -}; +} -// protected function -module.exports.hello = (event, context, callback) => { - callback(null, { message: 'Successfully authorized!', event }); -}; - -// auth function -module.exports.auth = (event, context) => { +async function auth(event, context) { const token = event.authorizationToken.split(' '); if (token[0] === 'Bearer' && token[1] === 'ShouldBeAuthorized') { - context.succeed(generatePolicy('SomeRandomId', 'Allow', '*')); + return context.succeed(generatePolicy('SomeRandomId', 'Allow', '*')); } - context.fail('Unauthorized'); + return context.fail('Unauthorized'); +} + +module.exports = { + auth, }; diff --git a/tests/integration-all/api-gateway/service/serverless.yml b/tests/integration-all/api-gateway/service/serverless.yml new file mode 100644 index 000000000..d3a5d6a33 --- /dev/null +++ b/tests/integration-all/api-gateway/service/serverless.yml @@ -0,0 +1,70 @@ +service: CHANGE_TO_UNIQUE_PER_RUN + +provider: + name: aws + runtime: nodejs10.x + versionFunctions: false + apiKeys: + - name: CHANGE_TO_UNIQUE_PER_RUN + value: CHANGE_TO_UNIQUE_PER_RUN + +functions: + # core functions + minimal: + handler: core.minimal + events: + - http: GET / + - http: + method: POST + path: minimal-1 + - http: + method: PUT + path: /minimal-2 + - http: + method: DELETE + path: /minimal-3/ + cors: + handler: core.cors + events: + - http: + method: GET + path: simple-cors + cors: true + - http: + method: GET + path: complex-cors + cors: + origin: '*' + headers: + - Content-Type + - X-Amz-Date + - Authorization + - X-Api-Key + - X-Amz-Security-Token + - X-Amz-User-Agent + allowCredentials: true + customAuthorizers: + handler: core.customAuthorizers + events: + - http: + path: custom-auth + method: GET + authorizer: authorizer + apiKeys: + handler: core.apiKeys + events: + - http: + path: api-keys + method: GET + private: true + timeout: + handler: core.timeout + timeout: 1 + events: + - http: + method: GET + integration: lambda + path: integration-lambda-timeout + # helper functions + authorizer: + handler: helper.auth diff --git a/tests/integration-all/api-gateway/tests.js b/tests/integration-all/api-gateway/tests.js new file mode 100644 index 000000000..29041cc30 --- /dev/null +++ b/tests/integration-all/api-gateway/tests.js @@ -0,0 +1,290 @@ +'use strict'; + +const path = require('path'); +const AWS = require('aws-sdk'); +const _ = require('lodash'); +const fetch = require('node-fetch'); +const { expect } = require('chai'); + +const { getTmpDirPath, readYamlFile, writeYamlFile } = require('../../utils/fs'); +const { region, createTestService, deployService, removeService } = require('../../utils/misc'); +const { createRestApi, deleteRestApi, getResources } = require('../../utils/api-gateway'); + +const CF = new AWS.CloudFormation({ region }); + +describe('AWS - API Gateway Integration Test', () => { + let serviceName; + let endpoint; + let stackName; + let tmpDirPath; + let serverlessFilePath; + let restApiId; + let restApiRootResourceId; + let apiKey; + const stage = 'dev'; + + beforeAll(() => { + tmpDirPath = getTmpDirPath(); + console.info(`Temporary path: ${tmpDirPath}`); + serverlessFilePath = path.join(tmpDirPath, 'serverless.yml'); + const serverlessConfig = createTestService(tmpDirPath, { + templateDir: path.join(__dirname, 'service'), + serverlessConfigHook: + // Ensure unique API key for each test (to avoid collision among concurrent CI runs) + config => { + apiKey = `${config.service}-api-key-1`; + config.provider.apiKeys[0] = { name: apiKey, value: apiKey }; + }, + }); + serviceName = serverlessConfig.service; + stackName = `${serviceName}-${stage}`; + console.info(`Deploying "${stackName}" service...`); + deployService(); + // create an external REST API + const externalRestApiName = `${stage}-${serviceName}-ext-api`; + return createRestApi(externalRestApiName) + .then(restApiMeta => { + restApiId = restApiMeta.id; + return getResources(restApiId); + }) + .then(resources => { + restApiRootResourceId = resources[0].id; + console.info( + 'Created external rest API ' + + `(id: ${restApiId}, root resource id: ${restApiRootResourceId})` + ); + }); + }); + + afterAll(() => { + // NOTE: deleting the references to the old, external REST API + const serverless = readYamlFile(serverlessFilePath); + delete serverless.provider.apiGateway.restApiId; + delete serverless.provider.apiGateway.restApiRootResourceId; + writeYamlFile(serverlessFilePath, serverless); + // NOTE: deploying once again to get the stack into the original state + console.info('Redeploying service...'); + deployService(); + console.info('Removing service...'); + removeService(); + console.info('Deleting external rest API...'); + return deleteRestApi(restApiId); + }); + + beforeEach(() => { + return CF.describeStacks({ StackName: stackName }) + .promise() + .then( + result => _.find(result.Stacks[0].Outputs, { OutputKey: 'ServiceEndpoint' }).OutputValue + ) + .then(endpointOutput => { + endpoint = endpointOutput.match(/https:\/\/.+\.execute-api\..+\.amazonaws\.com.+/)[0]; + endpoint = `${endpoint}`; + }); + }); + + describe('Minimal Setup', () => { + const expectedMessage = 'Hello from API Gateway! - (minimal)'; + + it('should expose an accessible GET HTTP endpoint', () => { + const testEndpoint = `${endpoint}`; + + return fetch(testEndpoint, { method: 'GET' }) + .then(response => response.json()) + .then(json => expect(json.message).to.equal(expectedMessage)); + }); + + it('should expose an accessible POST HTTP endpoint', () => { + const testEndpoint = `${endpoint}/minimal-1`; + + return fetch(testEndpoint, { method: 'POST' }) + .then(response => response.json()) + .then(json => expect(json.message).to.equal(expectedMessage)); + }); + + it('should expose an accessible PUT HTTP endpoint', () => { + const testEndpoint = `${endpoint}/minimal-2`; + + return fetch(testEndpoint, { method: 'PUT' }) + .then(response => response.json()) + .then(json => expect(json.message).to.equal(expectedMessage)); + }); + + it('should expose an accessible DELETE HTTP endpoint', () => { + const testEndpoint = `${endpoint}/minimal-3`; + + return fetch(testEndpoint, { method: 'DELETE' }) + .then(response => response.json()) + .then(json => expect(json.message).to.equal(expectedMessage)); + }); + }); + + describe('CORS', () => { + it('should setup simple CORS support via cors: true config', () => { + const testEndpoint = `${endpoint}/simple-cors`; + + return fetch(testEndpoint, { method: 'OPTIONS' }).then(response => { + const headers = response.headers; + const allowHeaders = [ + 'Content-Type', + 'X-Amz-Date', + 'Authorization', + 'X-Api-Key', + 'X-Amz-Security-Token', + 'X-Amz-User-Agent', + ].join(','); + expect(headers.get('access-control-allow-headers')).to.equal(allowHeaders); + expect(headers.get('access-control-allow-methods')).to.equal('OPTIONS,GET'); + expect(headers.get('access-control-allow-credentials')).to.equal('false'); + // TODO: for some reason this test fails for now... + // expect(headers.get('access-control-allow-origin')).to.equal('*'); + }); + }); + + it('should setup CORS support with complex object config', () => { + const testEndpoint = `${endpoint}/complex-cors`; + + return fetch(testEndpoint, { method: 'OPTIONS' }).then(response => { + const headers = response.headers; + const allowHeaders = [ + 'Content-Type', + 'X-Amz-Date', + 'Authorization', + 'X-Api-Key', + 'X-Amz-Security-Token', + 'X-Amz-User-Agent', + ].join(','); + expect(headers.get('access-control-allow-headers')).to.equal(allowHeaders); + expect(headers.get('access-control-allow-methods')).to.equal('OPTIONS,GET'); + expect(headers.get('access-control-allow-credentials')).to.equal('true'); + expect(headers.get('access-control-allow-origin')).to.equal('*'); + }); + }); + }); + + describe('Custom Authorizers', () => { + let testEndpoint; + + beforeEach(() => { + testEndpoint = `${endpoint}/custom-auth`; + }); + + it('should reject requests without authorization', () => { + return fetch(testEndpoint).then(response => { + expect(response.status).to.equal(401); + }); + }); + + it('should reject requests with wrong authorization', () => { + return fetch(testEndpoint, { + headers: { Authorization: 'Bearer ShouldNotBeAuthorized' }, + }).then(response => { + expect(response.status).to.equal(401); + }); + }); + + it('should authorize requests with correct authorization', () => { + return fetch(testEndpoint, { headers: { Authorization: 'Bearer ShouldBeAuthorized' } }) + .then(response => response.json()) + .then(json => { + expect(json.message).to.equal('Hello from API Gateway! - (customAuthorizers)'); + expect(json.event.requestContext.authorizer.principalId).to.equal('SomeRandomId'); + expect(json.event.headers.Authorization).to.equal('Bearer ShouldBeAuthorized'); + }); + }); + }); + + describe('API Keys', () => { + let testEndpoint; + + beforeEach(() => { + testEndpoint = `${endpoint}/api-keys`; + }); + + it('should reject a request with an invalid API Key', () => { + return fetch(testEndpoint).then(response => { + expect(response.status).to.equal(403); + }); + }); + + it('should succeed if correct API key is given', () => { + return fetch(testEndpoint, { headers: { 'X-API-Key': apiKey } }) + .then(response => response.json()) + .then(json => { + expect(json.message).to.equal('Hello from API Gateway! - (apiKeys)'); + }); + }); + }); + + describe('Using stage specific configuration', () => { + beforeAll(() => { + const serverless = readYamlFile(serverlessFilePath); + // enable Logs, Tags and Tracing + _.merge(serverless.provider, { + tags: { + foo: 'bar', + baz: 'qux', + }, + tracing: { + apiGateway: true, + }, + logs: { + restApi: true, + }, + }); + writeYamlFile(serverlessFilePath, serverless); + deployService(); + }); + + it('should update the stage without service interruptions', () => { + // re-using the endpoint from the "minimal" test case + const testEndpoint = `${endpoint}`; + + return fetch(testEndpoint, { method: 'GET' }) + .then(response => response.json()) + .then(json => expect(json.message).to.equal('Hello from API Gateway! - (minimal)')); + }); + }); + + // NOTE: this test should be at the very end because we're using an external REST API here + describe('when using an existing REST API with stage specific configuration', () => { + beforeAll(() => { + const serverless = readYamlFile(serverlessFilePath); + // enable Logs, Tags and Tracing + _.merge(serverless.provider, { + apiGateway: { + restApiId, + restApiRootResourceId, + }, + tags: { + foo: 'bar', + baz: 'qux', + }, + tracing: { + apiGateway: true, + }, + logs: { + restApi: true, + }, + }); + writeYamlFile(serverlessFilePath, serverless); + deployService(); + }); + + it('should update the stage without service interruptions', () => { + // re-using the endpoint from the "minimal" test case + const testEndpoint = `${endpoint}/minimal-1`; + + return fetch(testEndpoint, { method: 'POST' }) + .then(response => response.json()) + .then(json => expect(json.message).to.equal('Hello from API Gateway! - (minimal)')); + }); + }); + + describe('Integration Lambda Timeout', () => { + it('should result with 504 status code', () => + fetch(`${endpoint}/integration-lambda-timeout`).then(response => + expect(response.status).to.equal(504) + )); + }); +}); diff --git a/tests/integration-all/s3/service/core.js b/tests/integration-all/s3/service/core.js new file mode 100644 index 000000000..4a9a8f848 --- /dev/null +++ b/tests/integration-all/s3/service/core.js @@ -0,0 +1,44 @@ +'use strict'; + +const { log } = require('./utils'); + +function minimal(event, context, callback) { + const functionName = 'minimal'; + const response = { message: `Hello from S3! - (${functionName})`, event }; + const message = [ + event.Records[0].eventSource, + event.Records[0].eventName, + ' ', + response.message, + ].join(''); + log(functionName, message); + return callback(null, response); +} + +function extended(event, context, callback) { + const functionName = 'extended'; + const response = { message: `Hello from S3! - (${functionName})`, event }; + const message = [ + event.Records[0].eventSource, + event.Records[0].eventName, + ' ', + response.message, + ].join(''); + log(functionName, message); + return callback(null, response); +} + +function existing(event, context, callback) { + const functionName = 'existing'; + const response = { message: `Hello from S3! - (${functionName})`, event }; + const message = [ + event.Records[0].eventSource, + event.Records[0].eventName, + ' ', + response.message, + ].join(''); + log(functionName, message); + return callback(null, response); +} + +module.exports = { minimal, extended, existing }; diff --git a/tests/integration-all/s3/service/serverless.yml b/tests/integration-all/s3/service/serverless.yml new file mode 100644 index 000000000..22e450ac3 --- /dev/null +++ b/tests/integration-all/s3/service/serverless.yml @@ -0,0 +1,31 @@ +service: CHANGE_TO_UNIQUE_PER_RUN + +provider: + name: aws + runtime: nodejs10.x + versionFunctions: false + +functions: + minimal: + handler: core.minimal + events: + - s3: CHANGE_TO_UNIQUE_PER_RUN + extended: + handler: core.extended + events: + - s3: + bucket: CHANGE_TO_UNIQUE_PER_RUN + event: s3:ObjectRemoved:* + rules: + - prefix: photos/ + - suffix: .jpg + existing: + handler: core.existing + events: + - s3: + bucket: CHANGE_TO_UNIQUE_PER_RUN + event: s3:ObjectCreated:* + rules: + - prefix: files/ + - suffix: .txt + existing: true diff --git a/tests/integration-all/s3/service/utils.js b/tests/integration-all/s3/service/utils.js new file mode 100644 index 000000000..f4a99e00a --- /dev/null +++ b/tests/integration-all/s3/service/utils.js @@ -0,0 +1,22 @@ +'use strict'; + +const logger = console; + +function getMarkers(functionName) { + return { + start: `--- START ${functionName} ---`, + end: `--- END ${functionName} ---`, + }; +} + +function log(functionName, message) { + const markers = getMarkers(functionName); + logger.log(markers.start); + logger.log(message); + logger.log(markers.end); +} + +module.exports = { + getMarkers, + log, +}; diff --git a/tests/integration-all/s3/tests.js b/tests/integration-all/s3/tests.js new file mode 100644 index 000000000..8af13d8d3 --- /dev/null +++ b/tests/integration-all/s3/tests.js @@ -0,0 +1,106 @@ +'use strict'; + +const path = require('path'); +const { expect } = require('chai'); + +const { getTmpDirPath } = require('../../utils/fs'); +const { createBucket, createAndRemoveInBucket, deleteBucket } = require('../../utils/s3'); +const { + createTestService, + deployService, + removeService, + waitForFunctionLogs, +} = require('../../utils/misc'); +const { getMarkers } = require('./service/utils'); + +describe('AWS - S3 Integration Test', () => { + let serviceName; + let stackName; + let tmpDirPath; + let bucketMinimalSetup; + let bucketExtendedSetup; + let bucketExistingSetup; + const stage = 'dev'; + + beforeAll(() => { + tmpDirPath = getTmpDirPath(); + console.info(`Temporary path: ${tmpDirPath}`); + const serverlessConfig = createTestService(tmpDirPath, { + templateDir: path.join(__dirname, 'service'), + serverlessConfigHook: + // Ensure unique S3 bucket names for each test (to avoid collision among concurrent CI runs) + config => { + bucketMinimalSetup = `${config.service}-s3-minimal`; + bucketExtendedSetup = `${config.service}-s3-extended`; + bucketExistingSetup = `${config.service}-s3-existing`; + config.functions.minimal.events[0].s3 = bucketMinimalSetup; + config.functions.extended.events[0].s3.bucket = bucketExtendedSetup; + config.functions.existing.events[0].s3.bucket = bucketExistingSetup; + }, + }); + serviceName = serverlessConfig.service; + stackName = `${serviceName}-${stage}`; + // create an external S3 bucket + // NOTE: deployment can only be done once the S3 bucket is created + console.info(`Creating S3 bucket "${bucketExistingSetup}"...`); + return createBucket(bucketExistingSetup).then(() => { + console.info(`Deploying "${stackName}" service...`); + deployService(); + }); + }); + + afterAll(() => { + console.info('Removing service...'); + removeService(); + console.info(`Deleting S3 bucket "${bucketExistingSetup}"...`); + return deleteBucket(bucketExistingSetup); + }); + + describe('Minimal Setup', () => { + it('should invoke function when an object is created', () => { + const functionName = 'minimal'; + const markers = getMarkers(functionName); + const expectedMessage = `Hello from S3! - (${functionName})`; + + return createAndRemoveInBucket(bucketMinimalSetup) + .then(() => waitForFunctionLogs(functionName, markers.start, markers.end)) + .then(logs => { + expect(/aws:s3/g.test(logs)).to.equal(true); + expect(/ObjectCreated:Put/g.test(logs)).to.equal(true); + expect(logs.includes(expectedMessage)).to.equal(true); + }); + }); + }); + + describe('Extended Setup', () => { + it('should invoke function when an object is removed', () => { + const functionName = 'extended'; + const markers = getMarkers(functionName); + const expectedMessage = `Hello from S3! - (${functionName})`; + + return createAndRemoveInBucket(bucketExtendedSetup, { prefix: 'photos/', suffix: '.jpg' }) + .then(() => waitForFunctionLogs(functionName, markers.start, markers.end)) + .then(logs => { + expect(/aws:s3/g.test(logs)).to.equal(true); + expect(/ObjectRemoved:Delete/g.test(logs)).to.equal(true); + expect(logs.includes(expectedMessage)).to.equal(true); + }); + }); + }); + + describe('Existing Setup', () => { + it('should invoke function when an object is created', () => { + const functionName = 'existing'; + const markers = getMarkers(functionName); + const expectedMessage = `Hello from S3! - (${functionName})`; + + return createAndRemoveInBucket(bucketExistingSetup, { prefix: 'files/', suffix: '.txt' }) + .then(() => waitForFunctionLogs(functionName, markers.start, markers.end)) + .then(logs => { + expect(/aws:s3/g.test(logs)).to.equal(true); + expect(/ObjectCreated:Put/g.test(logs)).to.equal(true); + expect(logs.includes(expectedMessage)).to.equal(true); + }); + }); + }); +}); diff --git a/tests/simple-suite/tests.js b/tests/integration-basic/tests.js similarity index 67% rename from tests/simple-suite/tests.js rename to tests/integration-basic/tests.js index 84713c8a4..a9b8f6e7e 100644 --- a/tests/simple-suite/tests.js +++ b/tests/integration-basic/tests.js @@ -1,53 +1,64 @@ 'use strict'; -const fs = require('fs'); -const expect = require('chai').expect; const path = require('path'); +const fs = require('fs'); const fse = require('fs-extra'); const BbPromise = require('bluebird'); -const execSync = require('child_process').execSync; const AWS = require('aws-sdk'); -const testUtils = require('../utils/index'); +const stripAnsi = require('strip-ansi'); +const { expect } = require('chai'); +const { execSync } = require('../utils/child-process'); +const { getTmpDirPath, replaceTextInFile } = require('../utils/fs'); +const { region, getServiceName } = require('../utils/misc'); const serverlessExec = path.join(__dirname, '..', '..', 'bin', 'serverless'); -const tmpDir = testUtils.getTmpDirPath(); -fse.mkdirsSync(tmpDir); -process.chdir(tmpDir); - -const templateName = 'aws-nodejs'; -const newServiceName = `service-${(new Date()).getTime().toString()}`; -const stackName = `${newServiceName}-dev`; - -const CF = new AWS.CloudFormation({ region: 'us-east-1' }); -BbPromise.promisifyAll(CF, { suffix: 'Promised' }); +const CF = new AWS.CloudFormation({ region }); describe('Service Lifecyle Integration Test', () => { + const templateName = 'aws-nodejs'; + const tmpDir = getTmpDirPath(); + let oldCwd; + let serviceName; + let StackName; + + beforeAll(() => { + oldCwd = process.cwd(); + serviceName = getServiceName(); + StackName = `${serviceName}-dev`; + fse.mkdirsSync(tmpDir); + process.chdir(tmpDir); + }); + + afterAll(() => { + process.chdir(oldCwd); + }); + it('should create service in tmp directory', () => { - execSync(`${serverlessExec} create --template ${templateName}`, { stdio: 'inherit' }); - testUtils.replaceTextInFile('serverless.yml', templateName, newServiceName); + execSync(`${serverlessExec} create --template ${templateName}`); + replaceTextInFile('serverless.yml', templateName, serviceName); expect(fs.existsSync(path.join(tmpDir, 'serverless.yml'))).to.be.equal(true); expect(fs.existsSync(path.join(tmpDir, 'handler.js'))).to.be.equal(true); }); it('should deploy service to aws', () => { - execSync(`${serverlessExec} deploy`, { stdio: 'inherit' }); + execSync(`${serverlessExec} deploy`); - return CF.describeStacksPromised({ StackName: stackName }) + return CF.describeStacks({ StackName }) + .promise() .then(d => expect(d.Stacks[0].StackStatus).to.be.equal('UPDATE_COMPLETE')); }); it('should invoke function from aws', () => { const invoked = execSync(`${serverlessExec} invoke --function hello --noGreeting true`); - const result = JSON.parse(new Buffer(invoked, 'base64').toString()); + const result = JSON.parse(Buffer.from(invoked, 'base64').toString()); // parse it once again because the body is stringified to be LAMBDA-PROXY ready const message = JSON.parse(result.body).message; expect(message).to.be.equal('Go Serverless v1.0! Your function executed successfully!'); }); it('should deploy updated service to aws', () => { - const newHandler = - ` + const newHandler = ` 'use strict'; module.exports.hello = (event, context, cb) => cb(null, @@ -56,19 +67,19 @@ describe('Service Lifecyle Integration Test', () => { `; fs.writeFileSync(path.join(tmpDir, 'handler.js'), newHandler); - execSync(`${serverlessExec} deploy`, { stdio: 'inherit' }); + execSync(`${serverlessExec} deploy`); }); it('should invoke updated function from aws', () => { const invoked = execSync(`${serverlessExec} invoke --function hello --noGreeting true`); - const result = JSON.parse(new Buffer(invoked, 'base64').toString()); + const result = JSON.parse(Buffer.from(invoked, 'base64').toString()); expect(result.message).to.be.equal('Service Update Succeeded'); }); it('should list existing deployments and roll back to first deployment', () => { let timestamp; const listDeploys = execSync(`${serverlessExec} deploy list`); - const output = listDeploys.toString(); + const output = stripAnsi(listDeploys.toString()); const match = output.match(new RegExp('Datetime: (.+)')); if (match) { timestamp = match[1]; @@ -79,16 +90,17 @@ describe('Service Lifecyle Integration Test', () => { execSync(`${serverlessExec} rollback -t ${timestamp}`); const invoked = execSync(`${serverlessExec} invoke --function hello --noGreeting true`); - const result = JSON.parse(new Buffer(invoked, 'base64').toString()); + const result = JSON.parse(Buffer.from(invoked, 'base64').toString()); // parse it once again because the body is stringified to be LAMBDA-PROXY ready const message = JSON.parse(result.body).message; expect(message).to.be.equal('Go Serverless v1.0! Your function executed successfully!'); }); it('should remove service from aws', () => { - execSync(`${serverlessExec} remove`, { stdio: 'inherit' }); + execSync(`${serverlessExec} remove`); - return CF.describeStacksPromised({ StackName: stackName }) + return CF.describeStacks({ StackName }) + .promise() .then(d => expect(d.Stacks[0].StackStatus).to.be.equal('DELETE_COMPLETE')) .catch(error => { if (error.message.indexOf('does not exist') > -1) return BbPromise.resolve(); diff --git a/tests/integration-package/cloudformation.tests.js b/tests/integration-package/cloudformation.tests.js new file mode 100644 index 000000000..2f3d946e0 --- /dev/null +++ b/tests/integration-package/cloudformation.tests.js @@ -0,0 +1,128 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const fse = require('fs-extra'); +const { execSync } = require('../utils/child-process'); +const { serverlessExec } = require('../utils/misc'); +const { getTmpDirPath } = require('../utils/fs'); + +const fixturePaths = { + regular: path.join(__dirname, 'fixtures/regular'), + individually: path.join(__dirname, 'fixtures/individually'), + artifact: path.join(__dirname, 'fixtures/artifact'), +}; + +describe('Integration test - Packaging', () => { + let cwd; + beforeEach(() => { + cwd = getTmpDirPath(); + }); + + it('package artifact directive works', () => { + fse.copySync(fixturePaths.artifact, cwd); + execSync(`${serverlessExec} package`, { cwd }); + const cfnTemplate = JSON.parse( + fs.readFileSync(path.join(cwd, '.serverless/cloudformation-template-update-stack.json')) + ); + expect(cfnTemplate.Resources.HelloLambdaFunction.Properties.Code.S3Key).toMatch( + /serverless\/aws-nodejs\/dev\/[^]*\/artifact.zip/ + ); + delete cfnTemplate.Resources.HelloLambdaFunction.Properties.Code.S3Key; + expect(cfnTemplate.Resources.HelloLambdaFunction).toEqual({ + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: { + Ref: 'ServerlessDeploymentBucket', + }, + }, + FunctionName: 'aws-nodejs-dev-hello', + Handler: 'handler.hello', + MemorySize: 1024, + Role: { + 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'], + }, + Runtime: 'nodejs10.x', + Timeout: 6, + }, + DependsOn: ['HelloLogGroup', 'IamRoleLambdaExecution'], + }); + }); + + it('creates the correct default function resource in cfn template', () => { + fse.copySync(fixturePaths.regular, cwd); + execSync(`${serverlessExec} package`, { cwd }); + const cfnTemplate = JSON.parse( + fs.readFileSync(path.join(cwd, '.serverless/cloudformation-template-update-stack.json')) + ); + expect(cfnTemplate.Resources.HelloLambdaFunction.Properties.Code.S3Key).toMatch( + /serverless\/aws-nodejs\/dev\/[^]*\/aws-nodejs.zip/ + ); + delete cfnTemplate.Resources.HelloLambdaFunction.Properties.Code.S3Key; + expect(cfnTemplate.Resources.HelloLambdaFunction).toEqual({ + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: { + Ref: 'ServerlessDeploymentBucket', + }, + }, + FunctionName: 'aws-nodejs-dev-hello', + Handler: 'handler.hello', + MemorySize: 1024, + Role: { + 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'], + }, + Runtime: 'nodejs10.x', + Timeout: 6, + }, + DependsOn: ['HelloLogGroup', 'IamRoleLambdaExecution'], + }); + }); + + it('handles package individually with include/excludes correctly', () => { + fse.copySync(fixturePaths.individually, cwd); + execSync(`${serverlessExec} package`, { cwd }); + const cfnTemplate = JSON.parse( + fs.readFileSync(path.join(cwd, '.serverless/cloudformation-template-update-stack.json')) + ); + expect(cfnTemplate.Resources.HelloLambdaFunction.Properties.Code.S3Key).toMatch( + /serverless\/aws-nodejs\/dev\/[^]*\/hello.zip/ + ); + expect(cfnTemplate.Resources.Hello2LambdaFunction.Properties.Code.S3Key).toMatch( + /serverless\/aws-nodejs\/dev\/[^]*\/hello2.zip/ + ); + delete cfnTemplate.Resources.HelloLambdaFunction.Properties.Code.S3Key; + expect(cfnTemplate.Resources.HelloLambdaFunction).toEqual({ + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: { + Ref: 'ServerlessDeploymentBucket', + }, + }, + FunctionName: 'aws-nodejs-dev-hello', + Handler: 'handler.hello', + MemorySize: 1024, + Role: { + 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'], + }, + Runtime: 'nodejs10.x', + Timeout: 6, + }, + DependsOn: ['HelloLogGroup', 'IamRoleLambdaExecution'], + }); + }); + + it('resolves self.provider.region', () => { + fse.copySync(fixturePaths.regular, cwd); + execSync(`${serverlessExec} package`, { cwd }); + const cfnTemplate = JSON.parse( + fs.readFileSync(path.join(cwd, '.serverless/cloudformation-template-update-stack.json')) + ); + expect(cfnTemplate.Resources.CustomDashnameLambdaFunction.Properties.FunctionName).toEqual( + 'aws-nodejs-us-east-1-custom-name' + ); + }); +}); diff --git a/tests/integration-package/fixtures/artifact/artifact.zip b/tests/integration-package/fixtures/artifact/artifact.zip new file mode 100644 index 000000000..325409f5c Binary files /dev/null and b/tests/integration-package/fixtures/artifact/artifact.zip differ diff --git a/tests/integration-package/fixtures/artifact/handler.js b/tests/integration-package/fixtures/artifact/handler.js new file mode 100644 index 000000000..773780bbd --- /dev/null +++ b/tests/integration-package/fixtures/artifact/handler.js @@ -0,0 +1,18 @@ +'use strict'; + +module.exports.hello = function(event) { + return { + statusCode: 200, + body: JSON.stringify( + { + message: 'Go Serverless v1.0! Your function executed successfully!', + input: event, + }, + null, + 2 + ), + }; + + // Use this code if you don't use the http event with the LAMBDA-PROXY integration + // return { message: 'Go Serverless v1.0! Your function executed successfully!', event }; +}; diff --git a/tests/integration/aws/s3/single-event-single-function-single-bucket/service/serverless.yml b/tests/integration-package/fixtures/artifact/serverless.yml similarity index 59% rename from tests/integration/aws/s3/single-event-single-function-single-bucket/service/serverless.yml rename to tests/integration-package/fixtures/artifact/serverless.yml index 90a22cd81..e5668f229 100644 --- a/tests/integration/aws/s3/single-event-single-function-single-bucket/service/serverless.yml +++ b/tests/integration-package/fixtures/artifact/serverless.yml @@ -1,12 +1,12 @@ - service: aws-nodejs provider: name: aws - runtime: nodejs6.10 + runtime: nodejs10.x functions: hello: handler: handler.hello - events: - - s3: ${env:BUCKET_1} + +package: + artifact: artifact.zip diff --git a/tests/integration-package/fixtures/individually/handler.js b/tests/integration-package/fixtures/individually/handler.js new file mode 100644 index 000000000..773780bbd --- /dev/null +++ b/tests/integration-package/fixtures/individually/handler.js @@ -0,0 +1,18 @@ +'use strict'; + +module.exports.hello = function(event) { + return { + statusCode: 200, + body: JSON.stringify( + { + message: 'Go Serverless v1.0! Your function executed successfully!', + input: event, + }, + null, + 2 + ), + }; + + // Use this code if you don't use the http event with the LAMBDA-PROXY integration + // return { message: 'Go Serverless v1.0! Your function executed successfully!', event }; +}; diff --git a/tests/integration-package/fixtures/individually/handler2.js b/tests/integration-package/fixtures/individually/handler2.js new file mode 100644 index 000000000..773780bbd --- /dev/null +++ b/tests/integration-package/fixtures/individually/handler2.js @@ -0,0 +1,18 @@ +'use strict'; + +module.exports.hello = function(event) { + return { + statusCode: 200, + body: JSON.stringify( + { + message: 'Go Serverless v1.0! Your function executed successfully!', + input: event, + }, + null, + 2 + ), + }; + + // Use this code if you don't use the http event with the LAMBDA-PROXY integration + // return { message: 'Go Serverless v1.0! Your function executed successfully!', event }; +}; diff --git a/tests/integration-package/fixtures/individually/serverless.yml b/tests/integration-package/fixtures/individually/serverless.yml new file mode 100644 index 000000000..df2d79d23 --- /dev/null +++ b/tests/integration-package/fixtures/individually/serverless.yml @@ -0,0 +1,24 @@ +service: aws-nodejs + +provider: + name: aws + runtime: nodejs10.x + +package: + individually: true + +functions: + hello: + handler: handler.hello + package: + include: + - handler.js + exclude: + - handler2.js + hello2: + handler: handler2.hello + package: + include: + - handler2.js + exclude: + - handler.js diff --git a/tests/integration-package/fixtures/regular/handler.js b/tests/integration-package/fixtures/regular/handler.js new file mode 100644 index 000000000..773780bbd --- /dev/null +++ b/tests/integration-package/fixtures/regular/handler.js @@ -0,0 +1,18 @@ +'use strict'; + +module.exports.hello = function(event) { + return { + statusCode: 200, + body: JSON.stringify( + { + message: 'Go Serverless v1.0! Your function executed successfully!', + input: event, + }, + null, + 2 + ), + }; + + // Use this code if you don't use the http event with the LAMBDA-PROXY integration + // return { message: 'Go Serverless v1.0! Your function executed successfully!', event }; +}; diff --git a/tests/integration-package/fixtures/regular/serverless.yml b/tests/integration-package/fixtures/regular/serverless.yml new file mode 100644 index 000000000..b9cffde35 --- /dev/null +++ b/tests/integration-package/fixtures/regular/serverless.yml @@ -0,0 +1,12 @@ +service: aws-nodejs + +provider: + name: aws + runtime: nodejs10.x + +functions: + hello: + handler: handler.hello + custom-name: + name: ${self:service}-${self:provider.region}-custom-name + handler: handler.hello diff --git a/tests/integration-package/lambda-files.tests.js b/tests/integration-package/lambda-files.tests.js new file mode 100644 index 000000000..99ed6f113 --- /dev/null +++ b/tests/integration-package/lambda-files.tests.js @@ -0,0 +1,82 @@ +'use strict'; + +const path = require('path'); +const fse = require('fs-extra'); +const { execSync } = require('../utils/child-process'); +const { serverlessExec } = require('../utils/misc'); +const { getTmpDirPath, listZipFiles } = require('../utils/fs'); + +const fixturePaths = { + regular: path.join(__dirname, 'fixtures/regular'), + individually: path.join(__dirname, 'fixtures/individually'), +}; + +describe('Integration test - Packaging', () => { + let cwd; + beforeEach(() => { + cwd = getTmpDirPath(); + }); + + it('packages the default aws template correctly in the zip', () => { + fse.copySync(fixturePaths.regular, cwd); + execSync(`${serverlessExec} package`, { cwd }); + return listZipFiles(path.join(cwd, '.serverless/aws-nodejs.zip')).then(zipfiles => { + expect(zipfiles).toEqual(['handler.js']); + }); + }); + + it('packages the default aws template with an npm dep correctly in the zip', () => { + fse.copySync(fixturePaths.regular, cwd); + execSync('npm init --yes', { cwd }); + execSync('npm i lodash', { cwd }); + execSync(`${serverlessExec} package`, { cwd }); + return listZipFiles(path.join(cwd, '.serverless/aws-nodejs.zip')).then(zipfiles => { + const nodeModules = new Set( + zipfiles.filter(f => f.startsWith('node_modules')).map(f => f.split(path.sep)[1]) + ); + const nonNodeModulesFiles = zipfiles.filter(f => !f.startsWith('node_modules')); + expect(nodeModules).toEqual(new Set(['lodash'])); + expect(nonNodeModulesFiles).toEqual(['handler.js', 'package-lock.json', 'package.json']); + }); + }); + + it("doesn't package a dev dependency in the zip", () => { + fse.copySync(fixturePaths.regular, cwd); + execSync('npm init --yes', { cwd }); + execSync('npm i --save-dev lodash', { cwd }); + execSync(`${serverlessExec} package`, { cwd }); + return listZipFiles(path.join(cwd, '.serverless/aws-nodejs.zip')).then(zipfiles => { + const nodeModules = new Set( + zipfiles.filter(f => f.startsWith('node_modules')).map(f => f.split(path.sep)[1]) + ); + const nonNodeModulesFiles = zipfiles.filter(f => !f.startsWith('node_modules')); + expect(nodeModules).toEqual(new Set([])); + expect(nonNodeModulesFiles).toEqual(['handler.js', 'package-lock.json', 'package.json']); + }); + }); + + it('ignores package json files per ignore directive in the zip', () => { + fse.copySync(fixturePaths.regular, cwd); + execSync('npm init --yes', { cwd }); + execSync('echo \'package: {exclude: ["package*.json"]}\' >> serverless.yml', { cwd }); + execSync('npm i lodash', { cwd }); + execSync(`${serverlessExec} package`, { cwd }); + return listZipFiles(path.join(cwd, '.serverless/aws-nodejs.zip')).then(zipfiles => { + const nodeModules = new Set( + zipfiles.filter(f => f.startsWith('node_modules')).map(f => f.split(path.sep)[1]) + ); + const nonNodeModulesFiles = zipfiles.filter(f => !f.startsWith('node_modules')); + expect(nodeModules).toEqual(new Set(['lodash'])); + expect(nonNodeModulesFiles).toEqual(['handler.js']); + }); + }); + + it('handles package individually with include/excludes correctly', () => { + fse.copySync(fixturePaths.individually, cwd); + execSync(`${serverlessExec} package`, { cwd }); + return listZipFiles(path.join(cwd, '.serverless/hello.zip')) + .then(zipfiles => expect(zipfiles).toEqual(['handler.js'])) + .then(() => listZipFiles(path.join(cwd, '.serverless/hello2.zip'))) + .then(zipfiles => expect(zipfiles).toEqual(['handler2.js'])); + }); +}); diff --git a/tests/integration/aws/api-gateway/integration-lambda-proxy/api-keys/service/handler.js b/tests/integration/aws/api-gateway/integration-lambda-proxy/api-keys/service/handler.js deleted file mode 100644 index a33dec9e5..000000000 --- a/tests/integration/aws/api-gateway/integration-lambda-proxy/api-keys/service/handler.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; - -module.exports.hello = (event, context, callback) => { - const response = { - statusCode: 200, - body: JSON.stringify({ - message: 'Hello from API Gateway!', - event, - }), - }; - - callback(null, response); -}; diff --git a/tests/integration/aws/api-gateway/integration-lambda-proxy/api-keys/service/serverless.yml b/tests/integration/aws/api-gateway/integration-lambda-proxy/api-keys/service/serverless.yml deleted file mode 100644 index c790bf1e9..000000000 --- a/tests/integration/aws/api-gateway/integration-lambda-proxy/api-keys/service/serverless.yml +++ /dev/null @@ -1,16 +0,0 @@ -service: aws-nodejs # NOTE: update this with your service name - -provider: - name: aws - runtime: nodejs6.10 - apiKeys: - - WillBeReplacedBeforeDeployment - -functions: - hello: - handler: handler.hello - events: - - http: - path: hello - method: GET - private: true diff --git a/tests/integration/aws/api-gateway/integration-lambda-proxy/api-keys/tests.js b/tests/integration/aws/api-gateway/integration-lambda-proxy/api-keys/tests.js deleted file mode 100644 index a3048eda1..000000000 --- a/tests/integration/aws/api-gateway/integration-lambda-proxy/api-keys/tests.js +++ /dev/null @@ -1,83 +0,0 @@ -'use strict'; - -const path = require('path'); -const expect = require('chai').expect; -const BbPromise = require('bluebird'); -const execSync = require('child_process').execSync; -const AWS = require('aws-sdk'); -const _ = require('lodash'); -const fetch = require('node-fetch'); -const fse = require('fs-extra'); -const crypto = require('crypto'); - -const Utils = require('../../../../../utils/index'); - -const CF = new AWS.CloudFormation({ region: 'us-east-1' }); -const APIG = new AWS.APIGateway({ region: 'us-east-1' }); -BbPromise.promisifyAll(CF, { suffix: 'Promised' }); -BbPromise.promisifyAll(APIG, { suffix: 'Promised' }); - -describe('AWS - API Gateway (Integration: Lambda Proxy): API keys test', () => { - let stackName; - let endpoint; - let apiKey; - - beforeAll(() => { - stackName = Utils.createTestService('aws-nodejs', path.join(__dirname, 'service')); - - // replace name of the API key with something unique - const serverlessYmlFilePath = path.join(process.cwd(), 'serverless.yml'); - let serverlessYmlFileContent = fse.readFileSync(serverlessYmlFilePath).toString(); - - const apiKeyName = crypto.randomBytes(8).toString('hex'); - - serverlessYmlFileContent = serverlessYmlFileContent - .replace(/WillBeReplacedBeforeDeployment/, apiKeyName); - - fse.writeFileSync(serverlessYmlFilePath, serverlessYmlFileContent); - - Utils.deployService(); - }); - - beforeAll(() => { - const info = execSync(`${Utils.serverlessExec} info`); - const stringifiedOutput = (new Buffer(info, 'base64').toString()); - // some regex magic to extract the first API key value from the info output - apiKey = stringifiedOutput.match(/(api keys:\n)(\s*)(.+):(\s*)(.+)/)[5]; - }); - - it('should expose the endpoint(s) in the CloudFormation Outputs', () => - CF.describeStacksPromised({ StackName: stackName }) - .then((result) => _.find(result.Stacks[0].Outputs, - { OutputKey: 'ServiceEndpoint' }).OutputValue) - .then((endpointOutput) => { - const matched = endpointOutput.match(/https:\/\/.+\.execute-api\..+\.amazonaws\.com.+/)[0]; - endpoint = `${matched}/hello`; - }) - ); - - it('should expose the API key(s) with its values when running the info command', () => { - expect(apiKey.length).to.be.above(0); - }); - - it('should reject a request with an invalid API Key', () => - fetch(endpoint) - .then((response) => { - expect(response.status).to.equal(403); - }) - ); - - it('should succeed if correct API key is given', () => - fetch(endpoint, { headers: { 'x-api-key': apiKey } }) - .then(response => response.json()) - .then((json) => { - expect(json.message).to.equal('Hello from API Gateway!'); - expect(json.event.requestContext.identity.apiKey).to.equal(apiKey); - expect(json.event.headers['x-api-key']).to.equal(apiKey); - }) - ); - - afterAll(() => { - Utils.removeService(); - }); -}); diff --git a/tests/integration/aws/api-gateway/integration-lambda-proxy/cors/service/handler.js b/tests/integration/aws/api-gateway/integration-lambda-proxy/cors/service/handler.js deleted file mode 100644 index af4dfe714..000000000 --- a/tests/integration/aws/api-gateway/integration-lambda-proxy/cors/service/handler.js +++ /dev/null @@ -1,16 +0,0 @@ -'use strict'; - -module.exports.hello = (event, context, callback) => { - const response = { - statusCode: 200, - headers: { - 'Access-Control-Allow-Origin': '*', - }, - body: JSON.stringify({ - message: 'Hello from API Gateway!', - input: event, - }), - }; - - callback(null, response); -}; diff --git a/tests/integration/aws/api-gateway/integration-lambda-proxy/cors/service/serverless.yml b/tests/integration/aws/api-gateway/integration-lambda-proxy/cors/service/serverless.yml deleted file mode 100644 index 630d7f28b..000000000 --- a/tests/integration/aws/api-gateway/integration-lambda-proxy/cors/service/serverless.yml +++ /dev/null @@ -1,27 +0,0 @@ -service: aws-nodejs # NOTE: update this with your service name - -provider: - name: aws - runtime: nodejs6.10 - -functions: - hello: - handler: handler.hello - events: - - http: - method: GET - path: simple-cors - cors: true - - http: - method: GET - path: complex-cors - cors: - origins: - - '*' - headers: - - Content-Type - - X-Amz-Date - - Authorization - - X-Api-Key - - X-Amz-Security-Token - - X-Amz-User-Agent diff --git a/tests/integration/aws/api-gateway/integration-lambda-proxy/cors/tests.js b/tests/integration/aws/api-gateway/integration-lambda-proxy/cors/tests.js deleted file mode 100644 index 92095960c..000000000 --- a/tests/integration/aws/api-gateway/integration-lambda-proxy/cors/tests.js +++ /dev/null @@ -1,62 +0,0 @@ -'use strict'; - -const path = require('path'); -const expect = require('chai').expect; -const BbPromise = require('bluebird'); -const AWS = require('aws-sdk'); -const _ = require('lodash'); -const fetch = require('node-fetch'); - -const Utils = require('../../../../../utils/index'); - -const CF = new AWS.CloudFormation({ region: 'us-east-1' }); -BbPromise.promisifyAll(CF, { suffix: 'Promised' }); - -describe('AWS - API Gateway (Integration: Lambda Proxy): CORS test', () => { - let stackName; - let endpointBase; - - beforeAll(() => { - stackName = Utils.createTestService('aws-nodejs', path.join(__dirname, 'service')); - Utils.deployService(); - }); - - it('should expose the endpoint(s) in the CloudFormation Outputs', () => - CF.describeStacksPromised({ StackName: stackName }) - .then((result) => _.find(result.Stacks[0].Outputs, - { OutputKey: 'ServiceEndpoint' }).OutputValue) - .then((endpointOutput) => { - endpointBase = endpointOutput.match(/https:\/\/.+\.execute-api\..+\.amazonaws\.com.+/)[0]; - }) - ); - - it('should setup CORS support with simple string config', () => - fetch(`${endpointBase}/simple-cors`, { method: 'OPTIONS' }) - .then((response) => { - const headers = response.headers; - - expect(headers.get('access-control-allow-headers')) - .to.equal('Content-Type,X-Amz-Date,Authorization,X-Api-Key,' - + 'X-Amz-Security-Token,X-Amz-User-Agent'); - expect(headers.get('access-control-allow-methods')).to.equal('OPTIONS,GET'); - expect(headers.get('access-control-allow-origin')).to.equal('*'); - }) - ); - - it('should setup CORS support with complex object config', () => - fetch(`${endpointBase}/complex-cors`, { method: 'OPTIONS' }) - .then((response) => { - const headers = response.headers; - - expect(headers.get('access-control-allow-headers')) - .to.equal('Content-Type,X-Amz-Date,Authorization,X-Api-Key,' - + 'X-Amz-Security-Token,X-Amz-User-Agent'); - expect(headers.get('access-control-allow-methods')).to.equal('OPTIONS,GET'); - expect(headers.get('access-control-allow-origin')).to.equal('*'); - }) - ); - - afterAll(() => { - Utils.removeService(); - }); -}); diff --git a/tests/integration/aws/api-gateway/integration-lambda-proxy/custom-authorizers/service/handler.js b/tests/integration/aws/api-gateway/integration-lambda-proxy/custom-authorizers/service/handler.js deleted file mode 100644 index 08f981b24..000000000 --- a/tests/integration/aws/api-gateway/integration-lambda-proxy/custom-authorizers/service/handler.js +++ /dev/null @@ -1,45 +0,0 @@ -'use strict'; - -const generatePolicy = (principalId, effect, resource) => { - const authResponse = {}; - authResponse.principalId = principalId; - - if (effect && resource) { - const policyDocument = {}; - policyDocument.Version = '2012-10-17'; - policyDocument.Statement = []; - - const statementOne = {}; - statementOne.Action = 'execute-api:Invoke'; - statementOne.Effect = effect; - statementOne.Resource = resource; - policyDocument.Statement[0] = statementOne; - authResponse.policyDocument = policyDocument; - } - - return authResponse; -}; - -// protected function -module.exports.hello = (event, context, callback) => { - const response = { - statusCode: 200, - body: JSON.stringify({ - message: 'Successfully authorized!', - event, - }), - }; - - callback(null, response); -}; - -// auth function -module.exports.auth = (event, context) => { - const token = event.authorizationToken.split(' '); - - if (token[0] === 'Bearer' && token[1] === 'ShouldBeAuthorized') { - context.succeed(generatePolicy('SomeRandomId', 'Allow', '*')); - } - - context.fail('Unauthorized'); -}; diff --git a/tests/integration/aws/api-gateway/integration-lambda-proxy/custom-authorizers/service/serverless.yml b/tests/integration/aws/api-gateway/integration-lambda-proxy/custom-authorizers/service/serverless.yml deleted file mode 100644 index f02629f9f..000000000 --- a/tests/integration/aws/api-gateway/integration-lambda-proxy/custom-authorizers/service/serverless.yml +++ /dev/null @@ -1,16 +0,0 @@ -service: aws-nodejs # NOTE: update this with your service name - -provider: - name: aws - runtime: nodejs6.10 - -functions: - hello: - handler: handler.hello - events: - - http: - path: hello - method: GET - authorizer: auth - auth: - handler: handler.auth diff --git a/tests/integration/aws/api-gateway/integration-lambda-proxy/custom-authorizers/tests.js b/tests/integration/aws/api-gateway/integration-lambda-proxy/custom-authorizers/tests.js deleted file mode 100644 index ffe2ec57b..000000000 --- a/tests/integration/aws/api-gateway/integration-lambda-proxy/custom-authorizers/tests.js +++ /dev/null @@ -1,62 +0,0 @@ -'use strict'; - -const path = require('path'); -const expect = require('chai').expect; -const BbPromise = require('bluebird'); -const AWS = require('aws-sdk'); -const _ = require('lodash'); -const fetch = require('node-fetch'); - -const Utils = require('../../../../../utils/index'); - -const CF = new AWS.CloudFormation({ region: 'us-east-1' }); -BbPromise.promisifyAll(CF, { suffix: 'Promised' }); - - -describe('AWS - API Gateway (Integration: Lambda Proxy): Custom authorizers test', () => { - let stackName; - let endpoint; - - beforeAll(() => { - stackName = Utils.createTestService('aws-nodejs', path.join(__dirname, 'service')); - Utils.deployService(); - }); - - it('should expose the endpoint(s) in the CloudFormation Outputs', () => - CF.describeStacksPromised({ StackName: stackName }) - .then((result) => _.find(result.Stacks[0].Outputs, - { OutputKey: 'ServiceEndpoint' }).OutputValue) - .then((endpointOutput) => { - endpoint = endpointOutput.match(/https:\/\/.+\.execute-api\..+\.amazonaws\.com.+/)[0]; - endpoint = `${endpoint}/hello`; - }) - ); - - it('should reject requests without authorization', () => - fetch(endpoint) - .then((response) => { - expect(response.status).to.equal(401); - }) - ); - - it('should reject requests with wrong authorization', () => - fetch(endpoint, { headers: { Authorization: 'Bearer ShouldNotBeAuthorized' } }) - .then((response) => { - expect(response.status).to.equal(401); - }) - ); - - it('should authorize requests with correct authorization', () => - fetch(endpoint, { headers: { Authorization: 'Bearer ShouldBeAuthorized' } }) - .then(response => response.json()) - .then((json) => { - expect(json.message).to.equal('Successfully authorized!'); - expect(json.event.requestContext.authorizer.principalId).to.equal('SomeRandomId'); - expect(json.event.headers.Authorization).to.equal('Bearer ShouldBeAuthorized'); - }) - ); - - afterAll(() => { - Utils.removeService(); - }); -}); diff --git a/tests/integration/aws/api-gateway/integration-lambda-proxy/simple-api/service/handler.js b/tests/integration/aws/api-gateway/integration-lambda-proxy/simple-api/service/handler.js deleted file mode 100644 index 60924d588..000000000 --- a/tests/integration/aws/api-gateway/integration-lambda-proxy/simple-api/service/handler.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; - -module.exports.hello = (event, context, callback) => { - const response = { - statusCode: 200, - body: JSON.stringify({ - message: 'Hello from API Gateway!', - input: event, - }), - }; - - callback(null, response); -}; diff --git a/tests/integration/aws/api-gateway/integration-lambda-proxy/simple-api/service/serverless.yml b/tests/integration/aws/api-gateway/integration-lambda-proxy/simple-api/service/serverless.yml deleted file mode 100644 index 78e77a2a0..000000000 --- a/tests/integration/aws/api-gateway/integration-lambda-proxy/simple-api/service/serverless.yml +++ /dev/null @@ -1,37 +0,0 @@ -service: aws-nodejs # NOTE: update this with your service name - -provider: - name: aws - runtime: nodejs6.10 - -functions: - hello: - handler: handler.hello - events: - # paths without a slash - - http: POST without-slash - - http: GET without-slash - - http: - method: PUT - path: without-slash - - http: - method: DELETE - path: without-slash - # paths with a slash - - http: POST /with-slash - - http: GET /with-slash - - http: - method: PUT - path: /with-slash - - http: - method: DELETE - path: /with-slash - # root only paths - - http: POST / - - http: GET / - - http: - method: PUT - path: / - - http: - method: DELETE - path: / diff --git a/tests/integration/aws/api-gateway/integration-lambda-proxy/simple-api/tests.js b/tests/integration/aws/api-gateway/integration-lambda-proxy/simple-api/tests.js deleted file mode 100644 index 49b09b77e..000000000 --- a/tests/integration/aws/api-gateway/integration-lambda-proxy/simple-api/tests.js +++ /dev/null @@ -1,139 +0,0 @@ -'use strict'; - -const path = require('path'); -const expect = require('chai').expect; -const BbPromise = require('bluebird'); -const AWS = require('aws-sdk'); -const _ = require('lodash'); -const fetch = require('node-fetch'); - -const Utils = require('../../../../../utils/index'); - -const CF = new AWS.CloudFormation({ region: 'us-east-1' }); -BbPromise.promisifyAll(CF, { suffix: 'Promised' }); - -describe('AWS - API Gateway (Integration: Lambda Proxy): Simple API test', () => { - let stackName; - let endpoint; - - beforeAll(() => { - stackName = Utils.createTestService('aws-nodejs', path.join(__dirname, 'service')); - Utils.deployService(); - }); - - it('should expose the endpoint(s) in the CloudFormation Outputs', () => - CF.describeStacksPromised({ StackName: stackName }) - .then((result) => _.find(result.Stacks[0].Outputs, - { OutputKey: 'ServiceEndpoint' }).OutputValue) - .then((endpointOutput) => { - endpoint = endpointOutput.match(/https:\/\/.+\.execute-api\..+\.amazonaws\.com.+/)[0]; - endpoint = `${endpoint}`; - }) - ); - - describe('when having a "without-slash" path setup', () => { - it('should expose an accessible POST HTTP endpoint', () => { - const testEndpoint = `${endpoint}/without-slash`; - - return fetch(testEndpoint, { method: 'POST' }) - .then(response => response.json()) - .then((json) => expect(json.message).to.equal('Hello from API Gateway!')); - }); - - it('should expose an accessible GET HTTP endpoint', () => { - const testEndpoint = `${endpoint}/without-slash`; - - return fetch(testEndpoint) - .then(response => response.json()) - .then((json) => expect(json.message).to.equal('Hello from API Gateway!')); - }); - - it('should expose an accessible PUT HTTP endpoint', () => { - const testEndpoint = `${endpoint}/without-slash`; - - return fetch(testEndpoint, { method: 'PUT' }) - .then(response => response.json()) - .then((json) => expect(json.message).to.equal('Hello from API Gateway!')); - }); - - it('should expose an accessible DELETE HTTP endpoint', () => { - const testEndpoint = `${endpoint}/without-slash`; - - return fetch(testEndpoint, { method: 'DELETE' }) - .then(response => response.json()) - .then((json) => expect(json.message).to.equal('Hello from API Gateway!')); - }); - }); - - describe('when having a "/with-slash" path setup', () => { - it('should expose an accessible POST HTTP endpoint', () => { - const testEndpoint = `${endpoint}/with-slash`; - - return fetch(testEndpoint, { method: 'POST' }) - .then(response => response.json()) - .then((json) => expect(json.message).to.equal('Hello from API Gateway!')); - }); - - it('should expose an accessible GET HTTP endpoint', () => { - const testEndpoint = `${endpoint}/with-slash`; - - return fetch(testEndpoint) - .then(response => response.json()) - .then((json) => expect(json.message).to.equal('Hello from API Gateway!')); - }); - - it('should expose an accessible PUT HTTP endpoint', () => { - const testEndpoint = `${endpoint}/with-slash`; - - return fetch(testEndpoint, { method: 'PUT' }) - .then(response => response.json()) - .then((json) => expect(json.message).to.equal('Hello from API Gateway!')); - }); - - it('should expose an accessible DELETE HTTP endpoint', () => { - const testEndpoint = `${endpoint}/with-slash`; - - return fetch(testEndpoint, { method: 'DELETE' }) - .then(response => response.json()) - .then((json) => expect(json.message).to.equal('Hello from API Gateway!')); - }); - }); - - describe('when having a "/" path setup', () => { - it('should expose an accessible POST HTTP endpoint', () => { - const testEndpoint = `${endpoint}`; - - return fetch(testEndpoint, { method: 'POST' }) - .then(response => response.json()) - .then((json) => expect(json.message).to.equal('Hello from API Gateway!')); - }); - - it('should expose an accessible GET HTTP endpoint', () => { - const testEndpoint = `${endpoint}`; - - return fetch(testEndpoint) - .then(response => response.json()) - .then((json) => expect(json.message).to.equal('Hello from API Gateway!')); - }); - - it('should expose an accessible PUT HTTP endpoint', () => { - const testEndpoint = `${endpoint}`; - - return fetch(testEndpoint, { method: 'PUT' }) - .then(response => response.json()) - .then((json) => expect(json.message).to.equal('Hello from API Gateway!')); - }); - - it('should expose an accessible DELETE HTTP endpoint', () => { - const testEndpoint = `${endpoint}`; - - return fetch(testEndpoint, { method: 'DELETE' }) - .then(response => response.json()) - .then((json) => expect(json.message).to.equal('Hello from API Gateway!')); - }); - }); - - afterAll(() => { - Utils.removeService(); - }); -}); diff --git a/tests/integration/aws/api-gateway/integration-lambda/api-keys/service/handler.js b/tests/integration/aws/api-gateway/integration-lambda/api-keys/service/handler.js deleted file mode 100644 index a05213486..000000000 --- a/tests/integration/aws/api-gateway/integration-lambda/api-keys/service/handler.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -module.exports.hello = (event, context, callback) => { - callback(null, { message: 'Hello from API Gateway!', event }); -}; diff --git a/tests/integration/aws/api-gateway/integration-lambda/api-keys/service/serverless.yml b/tests/integration/aws/api-gateway/integration-lambda/api-keys/service/serverless.yml deleted file mode 100644 index f183f62f6..000000000 --- a/tests/integration/aws/api-gateway/integration-lambda/api-keys/service/serverless.yml +++ /dev/null @@ -1,17 +0,0 @@ -service: aws-nodejs # NOTE: update this with your service name - -provider: - name: aws - runtime: nodejs6.10 - apiKeys: - - WillBeReplacedBeforeDeployment - -functions: - hello: - handler: handler.hello - events: - - http: - path: hello - method: GET - integration: lambda - private: true diff --git a/tests/integration/aws/api-gateway/integration-lambda/api-keys/tests.js b/tests/integration/aws/api-gateway/integration-lambda/api-keys/tests.js deleted file mode 100644 index 3f21813db..000000000 --- a/tests/integration/aws/api-gateway/integration-lambda/api-keys/tests.js +++ /dev/null @@ -1,83 +0,0 @@ -'use strict'; - -const path = require('path'); -const expect = require('chai').expect; -const BbPromise = require('bluebird'); -const execSync = require('child_process').execSync; -const AWS = require('aws-sdk'); -const _ = require('lodash'); -const fetch = require('node-fetch'); -const fse = require('fs-extra'); -const crypto = require('crypto'); - -const Utils = require('../../../../../utils/index'); - -const CF = new AWS.CloudFormation({ region: 'us-east-1' }); -const APIG = new AWS.APIGateway({ region: 'us-east-1' }); -BbPromise.promisifyAll(CF, { suffix: 'Promised' }); -BbPromise.promisifyAll(APIG, { suffix: 'Promised' }); - -describe('AWS - API Gateway (Integration: Lambda): API keys test', () => { - let stackName; - let endpoint; - let apiKey; - - beforeAll(() => { - stackName = Utils.createTestService('aws-nodejs', path.join(__dirname, 'service')); - - // replace name of the API key with something unique - const serverlessYmlFilePath = path.join(process.cwd(), 'serverless.yml'); - let serverlessYmlFileContent = fse.readFileSync(serverlessYmlFilePath).toString(); - - const apiKeyName = crypto.randomBytes(8).toString('hex'); - - serverlessYmlFileContent = serverlessYmlFileContent - .replace(/WillBeReplacedBeforeDeployment/, apiKeyName); - - fse.writeFileSync(serverlessYmlFilePath, serverlessYmlFileContent); - - Utils.deployService(); - }); - - it('should expose the endpoint(s) in the CloudFormation Outputs', () => - CF.describeStacksPromised({ StackName: stackName }) - .then((result) => _.find(result.Stacks[0].Outputs, - { OutputKey: 'ServiceEndpoint' }).OutputValue) - .then((endpointOutput) => { - endpoint = endpointOutput.match(/https:\/\/.+\.execute-api\..+\.amazonaws\.com.+/)[0]; - endpoint = `${endpoint}/hello`; - }) - ); - - it('should expose the API key(s) with its values when running the info command', () => { - const info = execSync(`${Utils.serverlessExec} info`); - - const stringifiedOutput = (new Buffer(info, 'base64').toString()); - - // some regex magic to extract the first API key value from the info output - apiKey = stringifiedOutput.match(/(api keys:\n)(\s*)(.+):(\s*)(.+)/)[5]; - - expect(apiKey.length).to.be.above(0); - }); - - it('should reject a request with an invalid API Key', () => - fetch(endpoint) - .then((response) => { - expect(response.status).to.equal(403); - }) - ); - - it('should succeed if correct API key is given', () => - fetch(endpoint, { headers: { 'x-api-key': apiKey } }) - .then(response => response.json()) - .then((json) => { - expect(json.message).to.equal('Hello from API Gateway!'); - expect(json.event.identity.apiKey).to.equal(apiKey); - expect(json.event.headers['x-api-key']).to.equal(apiKey); - }) - ); - - afterAll(() => { - Utils.removeService(); - }); -}); diff --git a/tests/integration/aws/api-gateway/integration-lambda/cors/service/handler.js b/tests/integration/aws/api-gateway/integration-lambda/cors/service/handler.js deleted file mode 100644 index a05213486..000000000 --- a/tests/integration/aws/api-gateway/integration-lambda/cors/service/handler.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -module.exports.hello = (event, context, callback) => { - callback(null, { message: 'Hello from API Gateway!', event }); -}; diff --git a/tests/integration/aws/api-gateway/integration-lambda/cors/service/serverless.yml b/tests/integration/aws/api-gateway/integration-lambda/cors/service/serverless.yml deleted file mode 100644 index 2fefd441e..000000000 --- a/tests/integration/aws/api-gateway/integration-lambda/cors/service/serverless.yml +++ /dev/null @@ -1,29 +0,0 @@ -service: aws-nodejs # NOTE: update this with your service name - -provider: - name: aws - runtime: nodejs6.10 - -functions: - hello: - handler: handler.hello - events: - - http: - method: GET - path: simple-cors - integration: lambda - cors: true - - http: - method: GET - path: complex-cors - integration: lambda - cors: - origins: - - '*' - headers: - - Content-Type - - X-Amz-Date - - Authorization - - X-Api-Key - - X-Amz-Security-Token - - X-Amz-User-Agent diff --git a/tests/integration/aws/api-gateway/integration-lambda/cors/tests.js b/tests/integration/aws/api-gateway/integration-lambda/cors/tests.js deleted file mode 100644 index 7b8171559..000000000 --- a/tests/integration/aws/api-gateway/integration-lambda/cors/tests.js +++ /dev/null @@ -1,62 +0,0 @@ -'use strict'; - -const path = require('path'); -const expect = require('chai').expect; -const BbPromise = require('bluebird'); -const AWS = require('aws-sdk'); -const _ = require('lodash'); -const fetch = require('node-fetch'); - -const Utils = require('../../../../../utils/index'); - -const CF = new AWS.CloudFormation({ region: 'us-east-1' }); -BbPromise.promisifyAll(CF, { suffix: 'Promised' }); - -describe('AWS - API Gateway (Integration: Lambda): CORS test', () => { - let stackName; - let endpointBase; - - beforeAll(() => { - stackName = Utils.createTestService('aws-nodejs', path.join(__dirname, 'service')); - Utils.deployService(); - }); - - it('should expose the endpoint(s) in the CloudFormation Outputs', () => - CF.describeStacksPromised({ StackName: stackName }) - .then((result) => _.find(result.Stacks[0].Outputs, - { OutputKey: 'ServiceEndpoint' }).OutputValue) - .then((endpointOutput) => { - endpointBase = endpointOutput.match(/https:\/\/.+\.execute-api\..+\.amazonaws\.com.+/)[0]; - }) - ); - - it('should setup CORS support with simple string config', () => - fetch(`${endpointBase}/simple-cors`, { method: 'OPTIONS' }) - .then((response) => { - const headers = response.headers; - - expect(headers.get('access-control-allow-headers')) - .to.equal('Content-Type,X-Amz-Date,Authorization,X-Api-Key,' - + 'X-Amz-Security-Token,X-Amz-User-Agent'); - expect(headers.get('access-control-allow-methods')).to.equal('OPTIONS,GET'); - expect(headers.get('access-control-allow-origin')).to.equal('*'); - }) - ); - - it('should setup CORS support with complex object config', () => - fetch(`${endpointBase}/complex-cors`, { method: 'OPTIONS' }) - .then((response) => { - const headers = response.headers; - - expect(headers.get('access-control-allow-headers')) - .to.equal('Content-Type,X-Amz-Date,Authorization,X-Api-Key,' - + 'X-Amz-Security-Token,X-Amz-User-Agent'); - expect(headers.get('access-control-allow-methods')).to.equal('OPTIONS,GET'); - expect(headers.get('access-control-allow-origin')).to.equal('*'); - }) - ); - - afterAll(() => { - Utils.removeService(); - }); -}); diff --git a/tests/integration/aws/api-gateway/integration-lambda/custom-authorizers/service/serverless.yml b/tests/integration/aws/api-gateway/integration-lambda/custom-authorizers/service/serverless.yml deleted file mode 100644 index bbe8486fd..000000000 --- a/tests/integration/aws/api-gateway/integration-lambda/custom-authorizers/service/serverless.yml +++ /dev/null @@ -1,17 +0,0 @@ -service: aws-nodejs # NOTE: update this with your service name - -provider: - name: aws - runtime: nodejs6.10 - -functions: - hello: - handler: handler.hello - events: - - http: - path: hello - method: GET - integration: lambda - authorizer: auth - auth: - handler: handler.auth diff --git a/tests/integration/aws/api-gateway/integration-lambda/custom-authorizers/tests.js b/tests/integration/aws/api-gateway/integration-lambda/custom-authorizers/tests.js deleted file mode 100644 index 33a4f9706..000000000 --- a/tests/integration/aws/api-gateway/integration-lambda/custom-authorizers/tests.js +++ /dev/null @@ -1,61 +0,0 @@ -'use strict'; - -const path = require('path'); -const expect = require('chai').expect; -const BbPromise = require('bluebird'); -const AWS = require('aws-sdk'); -const _ = require('lodash'); -const fetch = require('node-fetch'); - -const Utils = require('../../../../../utils/index'); - -const CF = new AWS.CloudFormation({ region: 'us-east-1' }); -BbPromise.promisifyAll(CF, { suffix: 'Promised' }); - -describe('AWS - API Gateway (Integration: Lambda): Custom authorizers test', () => { - let stackName; - let endpoint; - - beforeAll(() => { - stackName = Utils.createTestService('aws-nodejs', path.join(__dirname, 'service')); - Utils.deployService(); - }); - - it('should expose the endpoint(s) in the CloudFormation Outputs', () => - CF.describeStacksPromised({ StackName: stackName }) - .then((result) => _.find(result.Stacks[0].Outputs, - { OutputKey: 'ServiceEndpoint' }).OutputValue) - .then((endpointOutput) => { - endpoint = endpointOutput.match(/https:\/\/.+\.execute-api\..+\.amazonaws\.com.+/)[0]; - endpoint = `${endpoint}/hello`; - }) - ); - - it('should reject requests without authorization', () => - fetch(endpoint) - .then((response) => { - expect(response.status).to.equal(401); - }) - ); - - it('should reject requests with wrong authorization', () => - fetch(endpoint, { headers: { Authorization: 'Bearer ShouldNotBeAuthorized' } }) - .then((response) => { - expect(response.status).to.equal(401); - }) - ); - - it('should authorize requests with correct authorization', () => - fetch(endpoint, { headers: { Authorization: 'Bearer ShouldBeAuthorized' } }) - .then(response => response.json()) - .then((json) => { - expect(json.message).to.equal('Successfully authorized!'); - expect(json.event.principalId).to.equal('SomeRandomId'); - expect(json.event.headers.Authorization).to.equal('Bearer ShouldBeAuthorized'); - }) - ); - - afterAll(() => { - Utils.removeService(); - }); -}); diff --git a/tests/integration/aws/api-gateway/integration-lambda/simple-api/service/handler.js b/tests/integration/aws/api-gateway/integration-lambda/simple-api/service/handler.js deleted file mode 100644 index a05213486..000000000 --- a/tests/integration/aws/api-gateway/integration-lambda/simple-api/service/handler.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -module.exports.hello = (event, context, callback) => { - callback(null, { message: 'Hello from API Gateway!', event }); -}; diff --git a/tests/integration/aws/api-gateway/integration-lambda/simple-api/service/serverless.yml b/tests/integration/aws/api-gateway/integration-lambda/simple-api/service/serverless.yml deleted file mode 100644 index 5906e242a..000000000 --- a/tests/integration/aws/api-gateway/integration-lambda/simple-api/service/serverless.yml +++ /dev/null @@ -1,61 +0,0 @@ -service: aws-nodejs # NOTE: update this with your service name - -provider: - name: aws - runtime: nodejs6.10 - -functions: - hello: - handler: handler.hello - events: - # paths without a slash - - http: - method: POST - path: without-slash - integration: lambda - - http: - method: GET - path: without-slash - integration: lambda - - http: - method: PUT - path: without-slash - integration: lambda - - http: - method: DELETE - path: without-slash - integration: lambda - # paths with a slash - - http: - method: POST - path: /with-slash - integration: lambda - - http: - method: GET - path: /with-slash - integration: lambda - - http: - method: PUT - path: /with-slash - integration: lambda - - http: - method: DELETE - path: /with-slash - integration: lambda - # root only paths - - http: - method: POST - path: / - integration: lambda - - http: - method: GET - path: / - integration: lambda - - http: - method: PUT - path: / - integration: lambda - - http: - method: DELETE - path: / - integration: lambda diff --git a/tests/integration/aws/api-gateway/integration-lambda/simple-api/tests.js b/tests/integration/aws/api-gateway/integration-lambda/simple-api/tests.js deleted file mode 100644 index 6050c13f3..000000000 --- a/tests/integration/aws/api-gateway/integration-lambda/simple-api/tests.js +++ /dev/null @@ -1,139 +0,0 @@ -'use strict'; - -const path = require('path'); -const expect = require('chai').expect; -const BbPromise = require('bluebird'); -const AWS = require('aws-sdk'); -const _ = require('lodash'); -const fetch = require('node-fetch'); - -const Utils = require('../../../../../utils/index'); - -const CF = new AWS.CloudFormation({ region: 'us-east-1' }); -BbPromise.promisifyAll(CF, { suffix: 'Promised' }); - -describe('AWS - API Gateway (Integration: Lambda): Simple API test', () => { - let stackName; - let endpoint; - - beforeAll(() => { - stackName = Utils.createTestService('aws-nodejs', path.join(__dirname, 'service')); - Utils.deployService(); - }); - - it('should expose the endpoint(s) in the CloudFormation Outputs', () => - CF.describeStacksPromised({ StackName: stackName }) - .then((result) => _.find(result.Stacks[0].Outputs, - { OutputKey: 'ServiceEndpoint' }).OutputValue) - .then((endpointOutput) => { - endpoint = endpointOutput.match(/https:\/\/.+\.execute-api\..+\.amazonaws\.com.+/)[0]; - endpoint = `${endpoint}`; - }) - ); - - describe('when having a "without-slash" path setup', () => { - it('should expose an accessible POST HTTP endpoint', () => { - const testEndpoint = `${endpoint}/without-slash`; - - return fetch(testEndpoint, { method: 'POST' }) - .then(response => response.json()) - .then((json) => expect(json.message).to.equal('Hello from API Gateway!')); - }); - - it('should expose an accessible GET HTTP endpoint', () => { - const testEndpoint = `${endpoint}/without-slash`; - - return fetch(testEndpoint) - .then(response => response.json()) - .then((json) => expect(json.message).to.equal('Hello from API Gateway!')); - }); - - it('should expose an accessible PUT HTTP endpoint', () => { - const testEndpoint = `${endpoint}/without-slash`; - - return fetch(testEndpoint, { method: 'PUT' }) - .then(response => response.json()) - .then((json) => expect(json.message).to.equal('Hello from API Gateway!')); - }); - - it('should expose an accessible DELETE HTTP endpoint', () => { - const testEndpoint = `${endpoint}/without-slash`; - - return fetch(testEndpoint, { method: 'DELETE' }) - .then(response => response.json()) - .then((json) => expect(json.message).to.equal('Hello from API Gateway!')); - }); - }); - - describe('when having a "/with-slash" path setup', () => { - it('should expose an accessible POST HTTP endpoint', () => { - const testEndpoint = `${endpoint}/with-slash`; - - return fetch(testEndpoint, { method: 'POST' }) - .then(response => response.json()) - .then((json) => expect(json.message).to.equal('Hello from API Gateway!')); - }); - - it('should expose an accessible GET HTTP endpoint', () => { - const testEndpoint = `${endpoint}/with-slash`; - - return fetch(testEndpoint) - .then(response => response.json()) - .then((json) => expect(json.message).to.equal('Hello from API Gateway!')); - }); - - it('should expose an accessible PUT HTTP endpoint', () => { - const testEndpoint = `${endpoint}/with-slash`; - - return fetch(testEndpoint, { method: 'PUT' }) - .then(response => response.json()) - .then((json) => expect(json.message).to.equal('Hello from API Gateway!')); - }); - - it('should expose an accessible DELETE HTTP endpoint', () => { - const testEndpoint = `${endpoint}/with-slash`; - - return fetch(testEndpoint, { method: 'DELETE' }) - .then(response => response.json()) - .then((json) => expect(json.message).to.equal('Hello from API Gateway!')); - }); - }); - - describe('when having a "/" path setup', () => { - it('should expose an accessible POST HTTP endpoint', () => { - const testEndpoint = `${endpoint}`; - - return fetch(testEndpoint, { method: 'POST' }) - .then(response => response.json()) - .then((json) => expect(json.message).to.equal('Hello from API Gateway!')); - }); - - it('should expose an accessible GET HTTP endpoint', () => { - const testEndpoint = `${endpoint}`; - - return fetch(testEndpoint) - .then(response => response.json()) - .then((json) => expect(json.message).to.equal('Hello from API Gateway!')); - }); - - it('should expose an accessible PUT HTTP endpoint', () => { - const testEndpoint = `${endpoint}`; - - return fetch(testEndpoint, { method: 'PUT' }) - .then(response => response.json()) - .then((json) => expect(json.message).to.equal('Hello from API Gateway!')); - }); - - it('should expose an accessible DELETE HTTP endpoint', () => { - const testEndpoint = `${endpoint}`; - - return fetch(testEndpoint, { method: 'DELETE' }) - .then(response => response.json()) - .then((json) => expect(json.message).to.equal('Hello from API Gateway!')); - }); - }); - - afterAll(() => { - Utils.removeService(); - }); -}); diff --git a/tests/integration/aws/cloud-watch-event/multiple-events-multiple-functions/service/handler.js b/tests/integration/aws/cloud-watch-event/multiple-events-multiple-functions/service/handler.js deleted file mode 100644 index 8ea67e830..000000000 --- a/tests/integration/aws/cloud-watch-event/multiple-events-multiple-functions/service/handler.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; - -module.exports.cwe1 = (event, context, callback) => { - process.stdout.write(JSON.stringify(event)); - callback(null, {}); -}; - -module.exports.cwe2 = (event, context, callback) => { - process.stdout.write(JSON.stringify(event)); - callback(null, {}); -}; diff --git a/tests/integration/aws/cloud-watch-event/multiple-events-multiple-functions/service/serverless.yml b/tests/integration/aws/cloud-watch-event/multiple-events-multiple-functions/service/serverless.yml deleted file mode 100644 index a885180bd..000000000 --- a/tests/integration/aws/cloud-watch-event/multiple-events-multiple-functions/service/serverless.yml +++ /dev/null @@ -1,29 +0,0 @@ -service: aws-nodejs - -provider: - name: aws - runtime: nodejs6.10 - -functions: - cwe1: - handler: handler.cwe1 - events: - - cloudwatchEvent: - event: - source: - - serverless.testapp1 - - cloudwatchEvent: - event: - source: - - serverless.testapp2 - cwe2: - handler: handler.cwe2 - events: - - cloudwatchEvent: - event: - source: - - serverless.testapp1 - - cloudwatchEvent: - event: - source: - - serverless.testapp2 diff --git a/tests/integration/aws/cloud-watch-event/multiple-events-multiple-functions/tests.js b/tests/integration/aws/cloud-watch-event/multiple-events-multiple-functions/tests.js deleted file mode 100644 index 33d4d59b9..000000000 --- a/tests/integration/aws/cloud-watch-event/multiple-events-multiple-functions/tests.js +++ /dev/null @@ -1,29 +0,0 @@ -'use strict'; - -const path = require('path'); -const expect = require('chai').expect; -const Utils = require('../../../../utils/index'); - -describe('AWS - CloudWatch Event: Multiple events with multiple functions', () => { - beforeAll(() => { - Utils.createTestService('aws-nodejs', path.join(__dirname, 'service')); - Utils.deployService(); - }); - - it('should trigger functions when cloudwatchEvent runs', () => Utils - .putCloudWatchEvents(['serverless.testapp1', 'serverless.testapp2']) - .delay(60000) - .then(() => { - const logs1 = Utils.getFunctionLogs('cwe1'); - const logs2 = Utils.getFunctionLogs('cwe2'); - expect(/serverless\.testapp1/g.test(logs1)).to.equal(true); - expect(/serverless\.testapp1/g.test(logs2)).to.equal(true); - expect(/serverless\.testapp2/g.test(logs1)).to.equal(true); - expect(/serverless\.testapp2/g.test(logs2)).to.equal(true); - }) - ); - - afterAll(() => { - Utils.removeService(); - }); -}); diff --git a/tests/integration/aws/cloud-watch-event/multiple-events-single-function/service/handler.js b/tests/integration/aws/cloud-watch-event/multiple-events-single-function/service/handler.js deleted file mode 100644 index 53d42c6dc..000000000 --- a/tests/integration/aws/cloud-watch-event/multiple-events-single-function/service/handler.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict'; - -module.exports.cwe1 = (event, context, callback) => { - process.stdout.write(JSON.stringify(event)); - callback(null, {}); -}; diff --git a/tests/integration/aws/cloud-watch-event/multiple-events-single-function/service/serverless.yml b/tests/integration/aws/cloud-watch-event/multiple-events-single-function/service/serverless.yml deleted file mode 100644 index 8612c47e2..000000000 --- a/tests/integration/aws/cloud-watch-event/multiple-events-single-function/service/serverless.yml +++ /dev/null @@ -1,18 +0,0 @@ -service: aws-nodejs - -provider: - name: aws - runtime: nodejs6.10 - -functions: - cwe1: - handler: handler.cwe1 - events: - - cloudwatchEvent: - event: - source: - - serverless.testapp1 - - cloudwatchEvent: - event: - source: - - serverless.testapp2 diff --git a/tests/integration/aws/cloud-watch-event/multiple-events-single-function/tests.js b/tests/integration/aws/cloud-watch-event/multiple-events-single-function/tests.js deleted file mode 100644 index 9b662b686..000000000 --- a/tests/integration/aws/cloud-watch-event/multiple-events-single-function/tests.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict'; - -const path = require('path'); -const expect = require('chai').expect; -const Utils = require('../../../../utils/index'); - -describe('AWS - CloudWatch Event: Multiple events with single function', () => { - beforeAll(() => { - Utils.createTestService('aws-nodejs', path.join(__dirname, 'service')); - Utils.deployService(); - }); - - it('should trigger function when cloudwatchEvent runs', () => Utils - .putCloudWatchEvents(['serverless.testapp1', 'serverless.testapp2']) - .delay(60000) - .then(() => { - const logs = Utils.getFunctionLogs('cwe1'); - expect(/serverless\.testapp1/g.test(logs)).to.equal(true); - expect(/serverless\.testapp2/g.test(logs)).to.equal(true); - }) - ); - - afterAll(() => { - Utils.removeService(); - }); -}); diff --git a/tests/integration/aws/cloud-watch-event/single-event-multiple-functions/service/handler.js b/tests/integration/aws/cloud-watch-event/single-event-multiple-functions/service/handler.js deleted file mode 100644 index 8ea67e830..000000000 --- a/tests/integration/aws/cloud-watch-event/single-event-multiple-functions/service/handler.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; - -module.exports.cwe1 = (event, context, callback) => { - process.stdout.write(JSON.stringify(event)); - callback(null, {}); -}; - -module.exports.cwe2 = (event, context, callback) => { - process.stdout.write(JSON.stringify(event)); - callback(null, {}); -}; diff --git a/tests/integration/aws/cloud-watch-event/single-event-multiple-functions/service/serverless.yml b/tests/integration/aws/cloud-watch-event/single-event-multiple-functions/service/serverless.yml deleted file mode 100644 index 98985d743..000000000 --- a/tests/integration/aws/cloud-watch-event/single-event-multiple-functions/service/serverless.yml +++ /dev/null @@ -1,21 +0,0 @@ -service: aws-nodejs - -provider: - name: aws - runtime: nodejs6.10 - -functions: - cwe1: - handler: handler.cwe1 - events: - - cloudwatchEvent: - event: - source: - - serverless.testapp1 - cwe2: - handler: handler.cwe2 - events: - - cloudwatchEvent: - event: - source: - - serverless.testapp1 diff --git a/tests/integration/aws/cloud-watch-event/single-event-multiple-functions/tests.js b/tests/integration/aws/cloud-watch-event/single-event-multiple-functions/tests.js deleted file mode 100644 index 433708734..000000000 --- a/tests/integration/aws/cloud-watch-event/single-event-multiple-functions/tests.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict'; - -const path = require('path'); -const expect = require('chai').expect; -const Utils = require('../../../../utils/index'); - -describe('AWS - CloudWatch Event: Single event with multiple functions', () => { - beforeAll(() => { - Utils.createTestService('aws-nodejs', path.join(__dirname, 'service')); - Utils.deployService(); - }); - - it('should trigger functions when cloudwatchEvent runs', () => Utils - .putCloudWatchEvents(['serverless.testapp1']) - .delay(60000) - .then(() => { - const logs1 = Utils.getFunctionLogs('cwe1'); - const logs2 = Utils.getFunctionLogs('cwe2'); - expect(/serverless\.testapp1/g.test(logs1)).to.equal(true); - expect(/serverless\.testapp1/g.test(logs2)).to.equal(true); - }) - ); - - afterAll(() => { - Utils.removeService(); - }); -}); diff --git a/tests/integration/aws/cloud-watch-event/single-event-single-function/service/handler.js b/tests/integration/aws/cloud-watch-event/single-event-single-function/service/handler.js deleted file mode 100644 index 53d42c6dc..000000000 --- a/tests/integration/aws/cloud-watch-event/single-event-single-function/service/handler.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict'; - -module.exports.cwe1 = (event, context, callback) => { - process.stdout.write(JSON.stringify(event)); - callback(null, {}); -}; diff --git a/tests/integration/aws/cloud-watch-event/single-event-single-function/service/serverless.yml b/tests/integration/aws/cloud-watch-event/single-event-single-function/service/serverless.yml deleted file mode 100644 index 4e528e7c3..000000000 --- a/tests/integration/aws/cloud-watch-event/single-event-single-function/service/serverless.yml +++ /dev/null @@ -1,14 +0,0 @@ -service: aws-nodejs - -provider: - name: aws - runtime: nodejs6.10 - -functions: - cwe1: - handler: handler.cwe1 - events: - - cloudwatchEvent: - event: - source: - - serverless.testapp1 diff --git a/tests/integration/aws/cloud-watch-event/single-event-single-function/tests.js b/tests/integration/aws/cloud-watch-event/single-event-single-function/tests.js deleted file mode 100644 index c0ab408d8..000000000 --- a/tests/integration/aws/cloud-watch-event/single-event-single-function/tests.js +++ /dev/null @@ -1,25 +0,0 @@ -'use strict'; - -const path = require('path'); -const expect = require('chai').expect; -const Utils = require('../../../../utils/index'); - -describe('AWS - CloudWatch Event: Single event with single function', () => { - beforeAll(() => { - Utils.createTestService('aws-nodejs', path.join(__dirname, 'service')); - Utils.deployService(); - }); - - it('should trigger function when cloudwatchEvent runs', () => Utils - .putCloudWatchEvents(['serverless.testapp1']) - .delay(60000) - .then(() => { - const logs = Utils.getFunctionLogs('cwe1'); - expect(/serverless\.testapp1/g.test(logs)).to.equal(true); - }) - ); - - afterAll(() => { - Utils.removeService(); - }); -}); diff --git a/tests/integration/aws/cognito-user-pool/multiple-pools-multiple-events-multiple-functions/service/handler.js b/tests/integration/aws/cognito-user-pool/multiple-pools-multiple-events-multiple-functions/service/handler.js deleted file mode 100644 index a1e2afe62..000000000 --- a/tests/integration/aws/cognito-user-pool/multiple-pools-multiple-events-multiple-functions/service/handler.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict'; - -const preSignUp = (event, context, callback) => { - const nextEvent = Object.assign({}, event); - nextEvent.response.autoConfirmUser = true; - - process.stdout.write(JSON.stringify(nextEvent)); - callback(null, nextEvent); -}; - -const customMessage = (event, context, callback) => { - const nextEvent = Object.assign({}, event); - if (event.triggerSource === 'CustomMessage_SignUp') { - nextEvent.response.smsMessage = `Welcome to the service. Your confirmation code is ${ - event.request.codeParameter}`; - nextEvent.response.emailSubject = 'Welcome to the service'; - nextEvent.response.emailMessage = `Thank you for signing up. ${ - event.request.codeParameter} is your verification code`; - } - process.stdout.write(JSON.stringify(nextEvent)); - callback(null, nextEvent); -}; - -module.exports.preSignUp1 = preSignUp; -module.exports.preSignUp2 = preSignUp; -module.exports.customMessage1 = customMessage; -module.exports.customMessage2 = customMessage; diff --git a/tests/integration/aws/cognito-user-pool/multiple-pools-multiple-events-multiple-functions/service/serverless.yml b/tests/integration/aws/cognito-user-pool/multiple-pools-multiple-events-multiple-functions/service/serverless.yml deleted file mode 100644 index 7dc6f310a..000000000 --- a/tests/integration/aws/cognito-user-pool/multiple-pools-multiple-events-multiple-functions/service/serverless.yml +++ /dev/null @@ -1,31 +0,0 @@ -service: aws-nodejs - -provider: - name: aws - runtime: nodejs6.10 - -functions: - preSignUp1: - handler: handler.preSignUp1 - events: - - cognitoUserPool: - pool: ${env:COGNITO_USER_POOL_1} - trigger: PreSignUp - customMessage1: - handler: handler.customMessage1 - events: - - cognitoUserPool: - pool: ${env:COGNITO_USER_POOL_1} - trigger: CustomMessage - preSignUp2: - handler: handler.preSignUp2 - events: - - cognitoUserPool: - pool: ${env:COGNITO_USER_POOL_2} - trigger: PreSignUp - customMessage2: - handler: handler.customMessage2 - events: - - cognitoUserPool: - pool: ${env:COGNITO_USER_POOL_2} - trigger: CustomMessage diff --git a/tests/integration/aws/cognito-user-pool/multiple-pools-multiple-events-multiple-functions/tests.js b/tests/integration/aws/cognito-user-pool/multiple-pools-multiple-events-multiple-functions/tests.js deleted file mode 100644 index df2ba5d40..000000000 --- a/tests/integration/aws/cognito-user-pool/multiple-pools-multiple-events-multiple-functions/tests.js +++ /dev/null @@ -1,77 +0,0 @@ -'use strict'; - -const path = require('path'); -const expect = require('chai').expect; -const Utils = require('../../../../utils/index'); - -describe('AWS - Cognito User Pool: Multiple User Pools with multiple ' + - 'events with multiple functions', () => { - beforeAll(() => { - Utils.createTestService('aws-nodejs', path.join(__dirname, 'service')); - Utils.deployService(); - }); - - it('should call the specified function on the first User Pool when PreSignUp ' + - 'event is triggered', () => Utils - .getCognitoUserPoolId(process.env.COGNITO_USER_POOL_1) - .then((poolId) => - Promise.all([ - poolId, - Utils.createCognitoUser(poolId, 'test@test.com', 'Password123!'), - ]) - ) - .delay(60000) - .then((promiseResponse) => { - const poolId = promiseResponse[0]; - const logs = Utils.getFunctionLogs('preSignUp1'); - - expect(RegExp(`"userPoolId":"${poolId}"`, 'g').test(logs)).to.equal(true); - expect(/"triggerSource":"PreSignUp_\w+"/g.test(logs)).to.equal(true); - }) - ); - - it('should call the specified function on the first User Pool when CustomMessage ' + - 'event is triggered', () => Utils - .getCognitoUserPoolId(process.env.COGNITO_USER_POOL_1) - .then((poolId) => { - const logs = Utils.getFunctionLogs('customMessage1'); - - expect(RegExp(`"userPoolId":"${poolId}"`, 'g').test(logs)).to.equal(true); - expect(/"triggerSource":"CustomMessage_AdminCreateUser"/g.test(logs)).to.equal(true); - }) - ); - - it('should call the specified function on the second User Pool when PreSignUp ' + - 'event is triggered', () => Utils - .getCognitoUserPoolId(process.env.COGNITO_USER_POOL_2) - .then((poolId) => - Promise.all([ - poolId, - Utils.createCognitoUser(poolId, 'test@test.com', 'Password123!'), - ]) - ) - .delay(60000) - .then((promiseResponse) => { - const poolId = promiseResponse[0]; - const logs = Utils.getFunctionLogs('preSignUp2'); - - expect(RegExp(`"userPoolId":"${poolId}"`, 'g').test(logs)).to.equal(true); - expect(/"triggerSource":"PreSignUp_\w+"/g.test(logs)).to.equal(true); - }) - ); - - it('should call the specified function on the second User Pool when CustomMessage ' + - 'event is triggered', () => Utils - .getCognitoUserPoolId(process.env.COGNITO_USER_POOL_2) - .then((poolId) => { - const logs = Utils.getFunctionLogs('customMessage2'); - - expect(RegExp(`"userPoolId":"${poolId}"`, 'g').test(logs)).to.equal(true); - expect(/"triggerSource":"CustomMessage_AdminCreateUser"/g.test(logs)).to.equal(true); - }) - ); - - afterAll(() => { - Utils.removeService(); - }); -}); diff --git a/tests/integration/aws/cognito-user-pool/multiple-pools-single-event-single-function/service/handler.js b/tests/integration/aws/cognito-user-pool/multiple-pools-single-event-single-function/service/handler.js deleted file mode 100644 index b7f044321..000000000 --- a/tests/integration/aws/cognito-user-pool/multiple-pools-single-event-single-function/service/handler.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; - -module.exports.preSignUp = (event, context, callback) => { - const nextEvent = Object.assign({}, event); - nextEvent.response.autoConfirmUser = true; - - process.stdout.write(JSON.stringify(nextEvent)); - callback(null, nextEvent); -}; diff --git a/tests/integration/aws/cognito-user-pool/multiple-pools-single-event-single-function/service/serverless.yml b/tests/integration/aws/cognito-user-pool/multiple-pools-single-event-single-function/service/serverless.yml deleted file mode 100644 index 121de6025..000000000 --- a/tests/integration/aws/cognito-user-pool/multiple-pools-single-event-single-function/service/serverless.yml +++ /dev/null @@ -1,16 +0,0 @@ -service: aws-nodejs - -provider: - name: aws - runtime: nodejs6.10 - -functions: - preSignUp: - handler: handler.preSignUp - events: - - cognitoUserPool: - pool: ${env:COGNITO_USER_POOL_1} - trigger: PreSignUp - - cognitoUserPool: - pool: ${env:COGNITO_USER_POOL_2} - trigger: PreSignUp diff --git a/tests/integration/aws/cognito-user-pool/multiple-pools-single-event-single-function/tests.js b/tests/integration/aws/cognito-user-pool/multiple-pools-single-event-single-function/tests.js deleted file mode 100644 index 9a9477545..000000000 --- a/tests/integration/aws/cognito-user-pool/multiple-pools-single-event-single-function/tests.js +++ /dev/null @@ -1,53 +0,0 @@ -'use strict'; - -const path = require('path'); -const expect = require('chai').expect; -const Utils = require('../../../../utils/index'); - -describe('AWS - Cognito User Pool: Multiple User Pools with single ' + - 'event with single function', () => { - beforeAll(() => { - Utils.createTestService('aws-nodejs', path.join(__dirname, 'service')); - Utils.deployService(); - }); - - it('should call the specified function on the first User Pool when PreSignUp ' + - 'event is triggered', () => Utils - .getCognitoUserPoolId(process.env.COGNITO_USER_POOL_1) - .then((poolId) => - Promise.all([ - poolId, - Utils.createCognitoUser(poolId, 'test@test.com', 'Password123!'), - ]) - ) - .delay(60000) - .then((promiseResponse) => { - const poolId = promiseResponse[0]; - const logs = Utils.getFunctionLogs('preSignUp'); - expect(RegExp(`"userPoolId":"${poolId}"`, 'g').test(logs)).to.equal(true); - expect(/"triggerSource":"PreSignUp_\w+"/g.test(logs)).to.equal(true); - }) - ); - - it('should call the specified function on the second User Pool when PreSignUp ' + - 'event is triggered', () => Utils - .getCognitoUserPoolId(process.env.COGNITO_USER_POOL_2) - .then((poolId) => - Promise.all([ - poolId, - Utils.createCognitoUser(poolId, 'test@test.com', 'Password123!'), - ]) - ) - .delay(60000) - .then((promiseResponse) => { - const poolId = promiseResponse[0]; - const logs = Utils.getFunctionLogs('preSignUp'); - expect(RegExp(`"userPoolId":"${poolId}"`, 'g').test(logs)).to.equal(true); - expect(/"triggerSource":"PreSignUp_\w+"/g.test(logs)).to.equal(true); - }) - ); - - afterAll(() => { - Utils.removeService(); - }); -}); diff --git a/tests/integration/aws/cognito-user-pool/single-pool-multiple-events-multiple-functions/service/handler.js b/tests/integration/aws/cognito-user-pool/single-pool-multiple-events-multiple-functions/service/handler.js deleted file mode 100644 index 2801cf8c5..000000000 --- a/tests/integration/aws/cognito-user-pool/single-pool-multiple-events-multiple-functions/service/handler.js +++ /dev/null @@ -1,22 +0,0 @@ -'use strict'; - -module.exports.preSignUp = (event, context, callback) => { - const nextEvent = Object.assign({}, event); - nextEvent.response.autoConfirmUser = true; - - process.stdout.write(JSON.stringify(nextEvent)); - callback(null, nextEvent); -}; - -module.exports.customMessage = (event, context, callback) => { - const nextEvent = Object.assign({}, event); - if (event.triggerSource === 'CustomMessage_SignUp') { - nextEvent.response.smsMessage = `Welcome to the service. Your confirmation code is ${ - event.request.codeParameter}`; - nextEvent.response.emailSubject = 'Welcome to the service'; - nextEvent.response.emailMessage = `Thank you for signing up. ${ - event.request.codeParameter} is your verification code`; - } - process.stdout.write(JSON.stringify(nextEvent)); - callback(null, nextEvent); -}; diff --git a/tests/integration/aws/cognito-user-pool/single-pool-multiple-events-multiple-functions/service/serverless.yml b/tests/integration/aws/cognito-user-pool/single-pool-multiple-events-multiple-functions/service/serverless.yml deleted file mode 100644 index 9ecaf3f5f..000000000 --- a/tests/integration/aws/cognito-user-pool/single-pool-multiple-events-multiple-functions/service/serverless.yml +++ /dev/null @@ -1,19 +0,0 @@ -service: aws-nodejs - -provider: - name: aws - runtime: nodejs6.10 - -functions: - preSignUp: - handler: handler.preSignUp - events: - - cognitoUserPool: - pool: ${env:COGNITO_USER_POOL_1} - trigger: PreSignUp - customMessage: - handler: handler.customMessage - events: - - cognitoUserPool: - pool: ${env:COGNITO_USER_POOL_1} - trigger: CustomMessage diff --git a/tests/integration/aws/cognito-user-pool/single-pool-multiple-events-multiple-functions/tests.js b/tests/integration/aws/cognito-user-pool/single-pool-multiple-events-multiple-functions/tests.js deleted file mode 100644 index 96d25c39b..000000000 --- a/tests/integration/aws/cognito-user-pool/single-pool-multiple-events-multiple-functions/tests.js +++ /dev/null @@ -1,39 +0,0 @@ -'use strict'; - -const path = require('path'); -const expect = require('chai').expect; -const Utils = require('../../../../utils/index'); - -describe('AWS - Cognito User Pool: Single User Pool with multiple ' + - 'events with multiple functions', () => { - beforeAll(() => { - Utils.createTestService('aws-nodejs', path.join(__dirname, 'service')); - Utils.deployService(); - }); - - it('should call the specified function when PreSignUp event is triggered', () => Utils - .getCognitoUserPoolId(process.env.COGNITO_USER_POOL_1) - .then((poolId) => - Utils.createCognitoUser(poolId, 'test@test.com', 'Password123!') - ) - .delay(60000) - .then(() => { - const logs = Utils.getFunctionLogs('preSignUp'); - expect(/"triggerSource":"PreSignUp_\w+"/g.test(logs)).to.equal(true); - }) - ); - - it('should call the specified function when CustomMessage event is triggered', () => Utils - .getCognitoUserPoolId(process.env.COGNITO_USER_POOL_1) - .then((poolId) => { - const logs = Utils.getFunctionLogs('customMessage'); - - expect(RegExp(`"userPoolId":"${poolId}"`, 'g').test(logs)).to.equal(true); - expect(/"triggerSource":"CustomMessage_AdminCreateUser"/g.test(logs)).to.equal(true); - }) - ); - - afterAll(() => { - Utils.removeService(); - }); -}); diff --git a/tests/integration/aws/cognito-user-pool/single-pool-single-event-single-function/service/handler.js b/tests/integration/aws/cognito-user-pool/single-pool-single-event-single-function/service/handler.js deleted file mode 100644 index b7f044321..000000000 --- a/tests/integration/aws/cognito-user-pool/single-pool-single-event-single-function/service/handler.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; - -module.exports.preSignUp = (event, context, callback) => { - const nextEvent = Object.assign({}, event); - nextEvent.response.autoConfirmUser = true; - - process.stdout.write(JSON.stringify(nextEvent)); - callback(null, nextEvent); -}; diff --git a/tests/integration/aws/cognito-user-pool/single-pool-single-event-single-function/service/serverless.yml b/tests/integration/aws/cognito-user-pool/single-pool-single-event-single-function/service/serverless.yml deleted file mode 100644 index 97167e310..000000000 --- a/tests/integration/aws/cognito-user-pool/single-pool-single-event-single-function/service/serverless.yml +++ /dev/null @@ -1,13 +0,0 @@ -service: aws-nodejs - -provider: - name: aws - runtime: nodejs6.10 - -functions: - preSignUp: - handler: handler.preSignUp - events: - - cognitoUserPool: - pool: ${env:COGNITO_USER_POOL_1} - trigger: PreSignUp diff --git a/tests/integration/aws/cognito-user-pool/single-pool-single-event-single-function/tests.js b/tests/integration/aws/cognito-user-pool/single-pool-single-event-single-function/tests.js deleted file mode 100644 index b77837821..000000000 --- a/tests/integration/aws/cognito-user-pool/single-pool-single-event-single-function/tests.js +++ /dev/null @@ -1,29 +0,0 @@ -'use strict'; - -const path = require('path'); -const expect = require('chai').expect; -const Utils = require('../../../../utils/index'); - -describe('AWS - Cognito User Pool: Single User Pool with single ' + - 'event with single function', () => { - beforeAll(() => { - Utils.createTestService('aws-nodejs', path.join(__dirname, 'service')); - Utils.deployService(); - }); - - it('should call the specified function when PreSignUp event is triggered', () => Utils - .getCognitoUserPoolId(process.env.COGNITO_USER_POOL_1) - .then((poolId) => - Utils.createCognitoUser(poolId, 'test@test.com', 'Password123!') - ) - .delay(60000) - .then(() => { - const logs = Utils.getFunctionLogs('preSignUp'); - expect(/"triggerSource":"PreSignUp_\w+"/g.test(logs)).to.equal(true); - }) - ); - - afterAll(() => { - Utils.removeService(); - }); -}); diff --git a/tests/integration/aws/general/custom-resources/service/handler.js b/tests/integration/aws/general/custom-resources/service/handler.js deleted file mode 100644 index 57b45f44f..000000000 --- a/tests/integration/aws/general/custom-resources/service/handler.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -module.exports.hello = (event, context, callback) => { - callback(null, { message: 'Go Serverless v1.0! Your function executed successfully!', event }); -}; diff --git a/tests/integration/aws/general/custom-resources/service/serverless.yml b/tests/integration/aws/general/custom-resources/service/serverless.yml deleted file mode 100644 index 58e8c1047..000000000 --- a/tests/integration/aws/general/custom-resources/service/serverless.yml +++ /dev/null @@ -1,20 +0,0 @@ -service: aws-nodejs # NOTE: update this with your service name - -provider: - name: aws - runtime: nodejs6.10 - -functions: - hello: - handler: handler.hello - -resources: - Resources: - MyCustomS3Bucket: - Type: 'AWS::S3::Bucket' - Properties: - BucketName: WillBeReplacedBeforeDeployment - Outputs: - MyCustomOutput: - Value: SomeValue - Description: Used to test custom outputs diff --git a/tests/integration/aws/general/custom-resources/tests.js b/tests/integration/aws/general/custom-resources/tests.js deleted file mode 100644 index 66222e495..000000000 --- a/tests/integration/aws/general/custom-resources/tests.js +++ /dev/null @@ -1,58 +0,0 @@ -'use strict'; - -const path = require('path'); -const expect = require('chai').expect; -const AWS = require('aws-sdk'); -const BbPromise = require('bluebird'); -const _ = require('lodash'); -const fse = require('fs-extra'); -const crypto = require('crypto'); - -const Utils = require('../../../../utils/index'); - -const CF = new AWS.CloudFormation({ region: 'us-east-1' }); -const S3 = new AWS.S3({ region: 'us-east-1' }); -BbPromise.promisifyAll(CF, { suffix: 'Promised' }); -BbPromise.promisifyAll(S3, { suffix: 'Promised' }); - -describe('AWS - General: Custom resources test', () => { - let stackName; - let s3BucketName; - - beforeAll(() => { - stackName = Utils.createTestService('aws-nodejs', path.join(__dirname, 'service')); - - // replace name of bucket which is created through custom resources with something unique - const serverlessYmlFilePath = path.join(process.cwd(), 'serverless.yml'); - let serverlessYmlFileContent = fse.readFileSync(serverlessYmlFilePath).toString(); - - s3BucketName = crypto.randomBytes(8).toString('hex'); - - serverlessYmlFileContent = serverlessYmlFileContent - .replace(/WillBeReplacedBeforeDeployment/, s3BucketName); - - fse.writeFileSync(serverlessYmlFilePath, serverlessYmlFileContent); - - Utils.deployService(); - }); - - it('should add the custom outputs to the Outputs section', () => - CF.describeStacksPromised({ StackName: stackName }) - .then((result) => _.find(result.Stacks[0].Outputs, - { OutputKey: 'MyCustomOutput' }).OutputValue) - .then((endpointOutput) => { - expect(endpointOutput).to.equal('SomeValue'); - }) - ); - - it('should create the custom resources (a S3 bucket)', () => - S3.listBucketsPromised() - .then((result) => !!_.find(result.Buckets, - { Name: s3BucketName })) - .then((found) => expect(found).to.equal(true)) - ); - - afterAll(() => { - Utils.removeService(); - }); -}); diff --git a/tests/integration/aws/general/environment-variables/service/handler.js b/tests/integration/aws/general/environment-variables/service/handler.js deleted file mode 100644 index 6f5d68a82..000000000 --- a/tests/integration/aws/general/environment-variables/service/handler.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -module.exports.hello = (event, context, callback) => { - callback(null, { - environment_variables: process.env, - }); -}; diff --git a/tests/integration/aws/general/environment-variables/service/serverless.yml b/tests/integration/aws/general/environment-variables/service/serverless.yml deleted file mode 100644 index e660815a2..000000000 --- a/tests/integration/aws/general/environment-variables/service/serverless.yml +++ /dev/null @@ -1,16 +0,0 @@ -service: aws-nodejs # NOTE: update this with your service name - -provider: - name: aws - runtime: nodejs6.10 - environment: - provider_level_variable_1: provider_level_1 - provider_level_variable_2: provider_level_2 - -functions: - hello: - handler: handler.hello - environment: - function_level_variable_1: function_level_1 - function_level_variable_2: function_level_2 - provider_level_variable_2: overwritten_by_function diff --git a/tests/integration/aws/general/environment-variables/tests.js b/tests/integration/aws/general/environment-variables/tests.js deleted file mode 100644 index af1dcb368..000000000 --- a/tests/integration/aws/general/environment-variables/tests.js +++ /dev/null @@ -1,33 +0,0 @@ -'use strict'; - -const path = require('path'); -const expect = require('chai').expect; -const execSync = require('child_process').execSync; - -const Utils = require('../../../../utils/index'); - -describe('AWS - General: Environment variables test', () => { - beforeAll(() => { - Utils.createTestService('aws-nodejs', path.join(__dirname, 'service')); - Utils.deployService(); - }); - - it('should expose environment variables', () => { - const invoked = execSync(`${Utils.serverlessExec} invoke --function hello --noGreeting true`); - - const result = JSON.parse(new Buffer(invoked, 'base64').toString()); - - expect(result.environment_variables.provider_level_variable_1) - .to.be.equal('provider_level_1'); - expect(result.environment_variables.function_level_variable_1) - .to.be.equal('function_level_1'); - expect(result.environment_variables.function_level_variable_2) - .to.be.equal('function_level_2'); - expect(result.environment_variables.provider_level_variable_2) - .to.be.equal('overwritten_by_function'); - }); - - afterAll(() => { - Utils.removeService(); - }); -}); diff --git a/tests/integration/aws/general/nested-handlers/service/deeply/nested/handler.js b/tests/integration/aws/general/nested-handlers/service/deeply/nested/handler.js deleted file mode 100644 index 6313e306f..000000000 --- a/tests/integration/aws/general/nested-handlers/service/deeply/nested/handler.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict'; - -// Your first function handler -module.exports.hello = (event, context, callback) => { - callback(null, { message: 'Go Serverless v1.0! Your function executed successfully!', event }); -}; diff --git a/tests/integration/aws/general/nested-handlers/service/serverless.yml b/tests/integration/aws/general/nested-handlers/service/serverless.yml deleted file mode 100644 index 9e874ba9a..000000000 --- a/tests/integration/aws/general/nested-handlers/service/serverless.yml +++ /dev/null @@ -1,9 +0,0 @@ -service: aws-nodejs # NOTE: update this with your service name - -provider: - name: aws - runtime: nodejs6.10 - -functions: - hello: - handler: deeply/nested/handler.hello diff --git a/tests/integration/aws/general/nested-handlers/tests.js b/tests/integration/aws/general/nested-handlers/tests.js deleted file mode 100644 index ed4083d95..000000000 --- a/tests/integration/aws/general/nested-handlers/tests.js +++ /dev/null @@ -1,25 +0,0 @@ -'use strict'; - -const path = require('path'); -const expect = require('chai').expect; -const execSync = require('child_process').execSync; - -const Utils = require('../../../../utils/index'); - -describe('AWS - General: Nested handlers test', () => { - beforeAll(() => { - Utils.createTestService('aws-nodejs', path.join(__dirname, 'service')); - Utils.deployService(); - }); - - it('should invoke the nested handler function from AWS', () => { - const invoked = execSync(`${Utils.serverlessExec} invoke --function hello --noGreeting true`); - - const result = JSON.parse(new Buffer(invoked, 'base64').toString()); - expect(result.message).to.be.equal('Go Serverless v1.0! Your function executed successfully!'); - }); - - afterAll(() => { - Utils.removeService(); - }); -}); diff --git a/tests/integration/aws/general/overwrite-resources/service/handler.js b/tests/integration/aws/general/overwrite-resources/service/handler.js deleted file mode 100644 index c45399eaa..000000000 --- a/tests/integration/aws/general/overwrite-resources/service/handler.js +++ /dev/null @@ -1,10 +0,0 @@ -'use strict'; - -module.exports.hello = (event, context, callback) => { - callback(null, { message: 'Go Serverless v1.0! Your function executed successfully!', event }); -}; - -module.exports.world = (event, context, callback) => { - callback(null, { message: 'Go Serverless v1.0! Your function executed successfully!', event }); -}; - diff --git a/tests/integration/aws/general/overwrite-resources/service/serverless.yml b/tests/integration/aws/general/overwrite-resources/service/serverless.yml deleted file mode 100644 index 6f255c362..000000000 --- a/tests/integration/aws/general/overwrite-resources/service/serverless.yml +++ /dev/null @@ -1,18 +0,0 @@ -service: aws-nodejs # NOTE: update this with your service name - -provider: - name: aws - runtime: nodejs6.10 - -functions: - hello: - handler: handler.hello - world: - handler: handler.world - -resources: - Resources: - HelloLambdaFunction: - Type: "AWS::Lambda::Function" - Properties: - Timeout: 10 diff --git a/tests/integration/aws/general/overwrite-resources/tests.js b/tests/integration/aws/general/overwrite-resources/tests.js deleted file mode 100644 index 06e615005..000000000 --- a/tests/integration/aws/general/overwrite-resources/tests.js +++ /dev/null @@ -1,42 +0,0 @@ -'use strict'; - -const path = require('path'); -const expect = require('chai').expect; -const AWS = require('aws-sdk'); -const BbPromise = require('bluebird'); - -const Utils = require('../../../../utils/index'); - -const Lambda = new AWS.Lambda({ region: 'us-east-1' }); -BbPromise.promisifyAll(Lambda, { suffix: 'Promised' }); - -describe('AWS - General: Overwrite resources test', () => { - let stackName; - - beforeAll(() => { - stackName = Utils.createTestService('aws-nodejs', path.join(__dirname, 'service')); - Utils.deployService(); - }); - - it('should overwrite timeout config for hello function', () => { - const helloFunctionName = `${stackName}-hello`; - return Lambda.getFunctionPromised({ FunctionName: helloFunctionName }) - .then(data => { - const timeout = data.Configuration.Timeout; - expect(timeout).to.equal(10); - }); - }); - - it('should NOT overwrite timeout config for world function', () => { - const worldFunctionName = `${stackName}-world`; - return Lambda.getFunctionPromised({ FunctionName: worldFunctionName }) - .then(data => { - const timeout = data.Configuration.Timeout; - expect(timeout).to.equal(6); - }); - }); - - afterAll(() => { - Utils.removeService(); - }); -}); diff --git a/tests/integration/aws/general/package/service/handler.js b/tests/integration/aws/general/package/service/handler.js deleted file mode 100644 index 57b45f44f..000000000 --- a/tests/integration/aws/general/package/service/handler.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -module.exports.hello = (event, context, callback) => { - callback(null, { message: 'Go Serverless v1.0! Your function executed successfully!', event }); -}; diff --git a/tests/integration/aws/general/package/service/serverless.yml b/tests/integration/aws/general/package/service/serverless.yml deleted file mode 100644 index cb030f421..000000000 --- a/tests/integration/aws/general/package/service/serverless.yml +++ /dev/null @@ -1,9 +0,0 @@ -service: aws-nodejs # NOTE: update this with your service name - -provider: - name: aws - runtime: nodejs6.10 - -functions: - hello: - handler: handler.hello diff --git a/tests/integration/aws/general/package/tests.js b/tests/integration/aws/general/package/tests.js deleted file mode 100644 index 2e466cd7a..000000000 --- a/tests/integration/aws/general/package/tests.js +++ /dev/null @@ -1,35 +0,0 @@ -'use strict'; - -const path = require('path'); -const expect = require('chai').expect; -const execSync = require('child_process').execSync; -const AWS = require('aws-sdk'); -const fs = require('fs'); - -const CF = new AWS.CloudFormation({ region: 'us-east-1' }); -const Utils = require('../../../../utils/index'); - -describe('AWS - General: Package', () => { - let serviceName; - - beforeAll(() => { - serviceName = Utils.createTestService('aws-nodejs', path.join(__dirname, 'service')); - execSync(`${Utils.serverlessExec} package`); - }); - - it('should have create cloudformation files and functions zip', () => { - const deployedFiles = fs.readdirSync(path.join(process.cwd(), '.serverless')); - expect(deployedFiles[0]).to.equal('cloudformation-template-create-stack.json'); - expect(deployedFiles[1]).to.equal('cloudformation-template-update-stack.json'); - expect(deployedFiles[2]).to.equal('serverless-state.json'); - // Note: noticed the seconds section can vary a lot - expect(deployedFiles[3]).to.match(/test-[0-9]{1,}-[0-9]{1,}.zip/); - }); - - it('should not found stack from AWS', (done) => { - CF.describeStackResources({ StackName: serviceName }, (error) => { - expect(error.message).to.equal(`Stack with id ${serviceName} does not exist`); - done(); - }); - }); -}); diff --git a/tests/integration/aws/iot/multiple-rules-multiple-functions/service/handler.js b/tests/integration/aws/iot/multiple-rules-multiple-functions/service/handler.js deleted file mode 100644 index cdfd5c5b9..000000000 --- a/tests/integration/aws/iot/multiple-rules-multiple-functions/service/handler.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; - -module.exports.iot1 = (event, context, callback) => { - process.stdout.write(JSON.stringify(event)); - callback(null, {}); -}; - -module.exports.iot2 = (event, context, callback) => { - process.stdout.write(JSON.stringify(event)); - callback(null, {}); -}; diff --git a/tests/integration/aws/iot/multiple-rules-multiple-functions/service/serverless.yml b/tests/integration/aws/iot/multiple-rules-multiple-functions/service/serverless.yml deleted file mode 100644 index 0b1188863..000000000 --- a/tests/integration/aws/iot/multiple-rules-multiple-functions/service/serverless.yml +++ /dev/null @@ -1,17 +0,0 @@ -service: aws-nodejs - -provider: - name: aws - runtime: nodejs6.10 - -functions: - iot1: - handler: handler.iot1 - events: - - iot: - sql: "SELECT * FROM 'mybutton'" - iot2: - handler: handler.iot2 - events: - - iot: - sql: "SELECT * FROM 'weather'" diff --git a/tests/integration/aws/iot/multiple-rules-multiple-functions/tests.js b/tests/integration/aws/iot/multiple-rules-multiple-functions/tests.js deleted file mode 100644 index a54ace65c..000000000 --- a/tests/integration/aws/iot/multiple-rules-multiple-functions/tests.js +++ /dev/null @@ -1,28 +0,0 @@ -'use strict'; - -const path = require('path'); -const expect = require('chai').expect; -const Utils = require('../../../../utils/index'); - -describe('AWS - IoT: Multiple rules with multiple functions', () => { - beforeAll(() => { - Utils.createTestService('aws-nodejs', path.join(__dirname, 'service')); - Utils.deployService(); - }); - - it('should trigger function when ruletopic message is published', () => Utils - .publishIotData('mybutton', '{"message":"hello serverless"}') - .then(() => Utils.publishIotData('weather', '{"message":"sunny today"}')) - .delay(60000) - .then(() => { - const iot1Logs = Utils.getFunctionLogs('iot1'); - const iot2Logs = Utils.getFunctionLogs('iot2'); - expect(/{"message":"hello serverless"}/g.test(iot1Logs)).to.equal(true); - expect(/{"message":"sunny today"}/g.test(iot2Logs)).to.equal(true); - }) - ); - - afterAll(() => { - Utils.removeService(); - }); -}); diff --git a/tests/integration/aws/iot/multiple-rules-single-function/service/handler.js b/tests/integration/aws/iot/multiple-rules-single-function/service/handler.js deleted file mode 100644 index 0f770c560..000000000 --- a/tests/integration/aws/iot/multiple-rules-single-function/service/handler.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict'; - -module.exports.iot1 = (event, context, callback) => { - process.stdout.write(JSON.stringify(event)); - callback(null, {}); -}; diff --git a/tests/integration/aws/iot/multiple-rules-single-function/service/serverless.yml b/tests/integration/aws/iot/multiple-rules-single-function/service/serverless.yml deleted file mode 100644 index ac4825ba3..000000000 --- a/tests/integration/aws/iot/multiple-rules-single-function/service/serverless.yml +++ /dev/null @@ -1,14 +0,0 @@ -service: aws-nodejs - -provider: - name: aws - runtime: nodejs6.10 - -functions: - iot1: - handler: handler.iot1 - events: - - iot: - sql: "SELECT * FROM 'mybutton'" - - iot: - sql: "SELECT * FROM 'weather'" diff --git a/tests/integration/aws/iot/multiple-rules-single-function/tests.js b/tests/integration/aws/iot/multiple-rules-single-function/tests.js deleted file mode 100644 index 11065591f..000000000 --- a/tests/integration/aws/iot/multiple-rules-single-function/tests.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict'; - -const path = require('path'); -const expect = require('chai').expect; -const Utils = require('../../../../utils/index'); - -describe('AWS - IoT: Multiple rules with single function', () => { - beforeAll(() => { - Utils.createTestService('aws-nodejs', path.join(__dirname, 'service')); - Utils.deployService(); - }); - - it('should trigger function when ruletopic message is published', () => Utils - .publishIotData('mybutton', '{"message":"hello serverless"}') - .then(() => Utils.publishIotData('weather', '{"message":"sunny today"}')) - .delay(60000) - .then(() => { - const logs = Utils.getFunctionLogs('iot1'); - expect(/{"message":"hello serverless"}/g.test(logs)).to.equal(true); - expect(/{"message":"sunny today"}/g.test(logs)).to.equal(true); - }) - ); - - afterAll(() => { - Utils.removeService(); - }); -}); diff --git a/tests/integration/aws/iot/single-rule-multiple-functions/service/handler.js b/tests/integration/aws/iot/single-rule-multiple-functions/service/handler.js deleted file mode 100644 index cdfd5c5b9..000000000 --- a/tests/integration/aws/iot/single-rule-multiple-functions/service/handler.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; - -module.exports.iot1 = (event, context, callback) => { - process.stdout.write(JSON.stringify(event)); - callback(null, {}); -}; - -module.exports.iot2 = (event, context, callback) => { - process.stdout.write(JSON.stringify(event)); - callback(null, {}); -}; diff --git a/tests/integration/aws/iot/single-rule-multiple-functions/service/serverless.yml b/tests/integration/aws/iot/single-rule-multiple-functions/service/serverless.yml deleted file mode 100644 index b41a289c6..000000000 --- a/tests/integration/aws/iot/single-rule-multiple-functions/service/serverless.yml +++ /dev/null @@ -1,17 +0,0 @@ -service: aws-nodejs - -provider: - name: aws - runtime: nodejs6.10 - -functions: - iot1: - handler: handler.iot1 - events: - - iot: - sql: "SELECT * FROM 'mybutton'" - iot2: - handler: handler.iot2 - events: - - iot: - sql: "SELECT * FROM 'mybutton'" diff --git a/tests/integration/aws/iot/single-rule-multiple-functions/tests.js b/tests/integration/aws/iot/single-rule-multiple-functions/tests.js deleted file mode 100644 index cddc707f0..000000000 --- a/tests/integration/aws/iot/single-rule-multiple-functions/tests.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict'; - -const path = require('path'); -const expect = require('chai').expect; -const Utils = require('../../../../utils/index'); - -describe('AWS - IoT: Single rule with multiple functions', () => { - beforeAll(() => { - Utils.createTestService('aws-nodejs', path.join(__dirname, 'service')); - Utils.deployService(); - }); - - it('should trigger function when ruletopic message is published', () => Utils - .publishIotData('mybutton', '{"message":"hello serverless"}') - .delay(60000) - .then(() => { - const iot1Logs = Utils.getFunctionLogs('iot1'); - const iot2Logs = Utils.getFunctionLogs('iot2'); - expect(/{"message":"hello serverless"}/g.test(iot1Logs)).to.equal(true); - expect(/{"message":"hello serverless"}/g.test(iot2Logs)).to.equal(true); - }) - ); - - afterAll(() => { - Utils.removeService(); - }); -}); diff --git a/tests/integration/aws/iot/single-rule-single-function/service/handler.js b/tests/integration/aws/iot/single-rule-single-function/service/handler.js deleted file mode 100644 index 0f770c560..000000000 --- a/tests/integration/aws/iot/single-rule-single-function/service/handler.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict'; - -module.exports.iot1 = (event, context, callback) => { - process.stdout.write(JSON.stringify(event)); - callback(null, {}); -}; diff --git a/tests/integration/aws/iot/single-rule-single-function/service/serverless.yml b/tests/integration/aws/iot/single-rule-single-function/service/serverless.yml deleted file mode 100644 index 4235c46b0..000000000 --- a/tests/integration/aws/iot/single-rule-single-function/service/serverless.yml +++ /dev/null @@ -1,12 +0,0 @@ -service: aws-nodejs - -provider: - name: aws - runtime: nodejs6.10 - -functions: - iot1: - handler: handler.iot1 - events: - - iot: - sql: "SELECT * FROM 'mybutton'" diff --git a/tests/integration/aws/iot/single-rule-single-function/tests.js b/tests/integration/aws/iot/single-rule-single-function/tests.js deleted file mode 100644 index eaf3cb11f..000000000 --- a/tests/integration/aws/iot/single-rule-single-function/tests.js +++ /dev/null @@ -1,25 +0,0 @@ -'use strict'; - -const path = require('path'); -const expect = require('chai').expect; -const Utils = require('../../../../utils/index'); - -describe('AWS - IoT: Single rule with single function', () => { - beforeAll(() => { - Utils.createTestService('aws-nodejs', path.join(__dirname, 'service')); - Utils.deployService(); - }); - - it('should trigger function when ruletopic message is published', () => Utils - .publishIotData('mybutton', '{"message":"hello serverless"}') - .delay(60000) - .then(() => { - const logs = Utils.getFunctionLogs('iot1'); - expect(/{"message":"hello serverless"}/g.test(logs)).to.equal(true); - }) - ); - - afterAll(() => { - Utils.removeService(); - }); -}); diff --git a/tests/integration/aws/s3/multiple-events-multiple-functions-multiple-buckets/service/handler.js b/tests/integration/aws/s3/multiple-events-multiple-functions-multiple-buckets/service/handler.js deleted file mode 100644 index 5a96f3a31..000000000 --- a/tests/integration/aws/s3/multiple-events-multiple-functions-multiple-buckets/service/handler.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; - -module.exports.hello = (event, context, callback) => { - process.stdout.write(event.Records[0].eventSource); - process.stdout.write(event.Records[0].eventName); - callback(null, { message: 'Hello from S3!', event }); -}; - -module.exports.world = (event, context, callback) => { - process.stdout.write(event.Records[0].eventSource); - process.stdout.write(event.Records[0].eventName); - callback(null, { message: 'Hello from S3!', event }); -}; diff --git a/tests/integration/aws/s3/multiple-events-multiple-functions-multiple-buckets/service/serverless.yml b/tests/integration/aws/s3/multiple-events-multiple-functions-multiple-buckets/service/serverless.yml deleted file mode 100644 index 49dce85ce..000000000 --- a/tests/integration/aws/s3/multiple-events-multiple-functions-multiple-buckets/service/serverless.yml +++ /dev/null @@ -1,21 +0,0 @@ -service: aws-nodejs - -provider: - name: aws - runtime: nodejs6.10 - -functions: - hello: - handler: handler.hello - events: - - s3: ${env:BUCKET_1} - - s3: - bucket: ${env:BUCKET_1} - event: s3:ObjectRemoved:* - world: - handler: handler.world - events: - - s3: ${env:BUCKET_2} - - s3: - bucket: ${env:BUCKET_2} - event: s3:ObjectRemoved:* diff --git a/tests/integration/aws/s3/multiple-events-multiple-functions-multiple-buckets/tests.js b/tests/integration/aws/s3/multiple-events-multiple-functions-multiple-buckets/tests.js deleted file mode 100644 index 1b667c068..000000000 --- a/tests/integration/aws/s3/multiple-events-multiple-functions-multiple-buckets/tests.js +++ /dev/null @@ -1,34 +0,0 @@ -'use strict'; - -const path = require('path'); -const expect = require('chai').expect; -const Utils = require('../../../../utils/index'); - -describe('AWS - S3: Multiple events in multiple functions with multiple buckets', () => { - beforeAll(() => { - Utils.createTestService('aws-nodejs', path.join(__dirname, 'service')); - Utils.deployService(); - }); - - it('should trigger functions when object created or deleted in buckets', () => Utils - .createAndRemoveInBucket(process.env.BUCKET_1) - .then(() => Utils.createAndRemoveInBucket(process.env.BUCKET_2)) - .delay(60000) - .then(() => { - const helloLogs = Utils.getFunctionLogs('hello'); - const worldLogs = Utils.getFunctionLogs('world'); - - expect(/aws:s3/g.test(helloLogs)).to.equal(true); - expect(/ObjectCreated:Put/g.test(helloLogs)).to.equal(true); - expect(/ObjectRemoved:Delete/g.test(helloLogs)).to.equal(true); - - expect(/aws:s3/g.test(worldLogs)).to.equal(true); - expect(/ObjectCreated:Put/g.test(worldLogs)).to.equal(true); - expect(/ObjectRemoved:Delete/g.test(worldLogs)).to.equal(true); - }) - ); - - afterAll(() => { - Utils.removeService(); - }); -}); diff --git a/tests/integration/aws/s3/multiple-events-multiple-functions-single-bucket/service/handler.js b/tests/integration/aws/s3/multiple-events-multiple-functions-single-bucket/service/handler.js deleted file mode 100644 index d742ed551..000000000 --- a/tests/integration/aws/s3/multiple-events-multiple-functions-single-bucket/service/handler.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; - -module.exports.create = (event, context, callback) => { - process.stdout.write(event.Records[0].eventSource); - process.stdout.write(event.Records[0].eventName); - callback(null, { message: 'Hello from S3!', event }); -}; - -module.exports.remove = (event, context, callback) => { - process.stdout.write(event.Records[0].eventSource); - process.stdout.write(event.Records[0].eventName); - callback(null, { message: 'Hello from S3!', event }); -}; diff --git a/tests/integration/aws/s3/multiple-events-multiple-functions-single-bucket/service/serverless.yml b/tests/integration/aws/s3/multiple-events-multiple-functions-single-bucket/service/serverless.yml deleted file mode 100644 index 3c046a71e..000000000 --- a/tests/integration/aws/s3/multiple-events-multiple-functions-single-bucket/service/serverless.yml +++ /dev/null @@ -1,17 +0,0 @@ -service: aws-nodejs - -provider: - name: aws - runtime: nodejs6.10 - -functions: - create: - handler: handler.create - events: - - s3: ${env:BUCKET_1} - remove: - handler: handler.remove - events: - - s3: - bucket: ${env:BUCKET_1} - event: s3:ObjectRemoved:* diff --git a/tests/integration/aws/s3/multiple-events-multiple-functions-single-bucket/tests.js b/tests/integration/aws/s3/multiple-events-multiple-functions-single-bucket/tests.js deleted file mode 100644 index 03a0aab68..000000000 --- a/tests/integration/aws/s3/multiple-events-multiple-functions-single-bucket/tests.js +++ /dev/null @@ -1,30 +0,0 @@ -'use strict'; - -const path = require('path'); -const expect = require('chai').expect; -const Utils = require('../../../../utils/index'); - -describe('AWS - S3: Multiple events in multiple functions with a single bucket', () => { - beforeAll(() => { - Utils.createTestService('aws-nodejs', path.join(__dirname, 'service')); - Utils.deployService(); - }); - - it('should trigger create/remove functions on object create / remove', () => Utils - .createAndRemoveInBucket(process.env.BUCKET_1) - .delay(60000) - .then(() => { - const createLogs = Utils.getFunctionLogs('create'); - const removeLogs = Utils.getFunctionLogs('remove'); - - expect(/aws:s3/g.test(createLogs)).to.equal(true); - expect(/aws:s3/g.test(removeLogs)).to.equal(true); - expect(/ObjectCreated:Put/g.test(createLogs)).to.equal(true); - expect(/ObjectRemoved:Delete/g.test(removeLogs)).to.equal(true); - }) - ); - - afterAll(() => { - Utils.removeService(); - }); -}); diff --git a/tests/integration/aws/s3/multiple-events-single-function-single-bucket/service/handler.js b/tests/integration/aws/s3/multiple-events-single-function-single-bucket/service/handler.js deleted file mode 100644 index aaa093a8c..000000000 --- a/tests/integration/aws/s3/multiple-events-single-function-single-bucket/service/handler.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -module.exports.hello = (event, context, callback) => { - process.stdout.write(event.Records[0].eventSource); - process.stdout.write(event.Records[0].eventName); - callback(null, { message: 'Hello from S3!', event }); -}; diff --git a/tests/integration/aws/s3/multiple-events-single-function-single-bucket/service/serverless.yml b/tests/integration/aws/s3/multiple-events-single-function-single-bucket/service/serverless.yml deleted file mode 100644 index 3d14e700a..000000000 --- a/tests/integration/aws/s3/multiple-events-single-function-single-bucket/service/serverless.yml +++ /dev/null @@ -1,14 +0,0 @@ -service: aws-nodejs - -provider: - name: aws - runtime: nodejs6.10 - -functions: - hello: - handler: handler.hello - events: - - s3: ${env:BUCKET_1} - - s3: - bucket: ${env:BUCKET_1} - event: s3:ObjectRemoved:* diff --git a/tests/integration/aws/s3/multiple-events-single-function-single-bucket/tests.js b/tests/integration/aws/s3/multiple-events-single-function-single-bucket/tests.js deleted file mode 100644 index e7db28191..000000000 --- a/tests/integration/aws/s3/multiple-events-single-function-single-bucket/tests.js +++ /dev/null @@ -1,28 +0,0 @@ -'use strict'; - -const path = require('path'); -const expect = require('chai').expect; -const Utils = require('../../../../utils/index'); - -describe('AWS - S3: Multiple events in a single function with a single bucket', () => { - beforeAll(() => { - Utils.createTestService('aws-nodejs', path.join(__dirname, 'service')); - Utils.deployService(); - }); - - it('should trigger function when object created or deleted in bucket', () => Utils - .createAndRemoveInBucket(process.env.BUCKET_1) - .delay(60000) - .then(() => { - const logs = Utils.getFunctionLogs('hello'); - - expect(/aws:s3/g.test(logs)).to.equal(true); - expect(/ObjectCreated:Put/g.test(logs)).to.equal(true); - expect(/ObjectRemoved:Delete/g.test(logs)).to.equal(true); - }) - ); - - afterAll(() => { - Utils.removeService(); - }); -}); diff --git a/tests/integration/aws/s3/single-event-single-function-single-bucket/service/handler.js b/tests/integration/aws/s3/single-event-single-function-single-bucket/service/handler.js deleted file mode 100644 index aaa093a8c..000000000 --- a/tests/integration/aws/s3/single-event-single-function-single-bucket/service/handler.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -module.exports.hello = (event, context, callback) => { - process.stdout.write(event.Records[0].eventSource); - process.stdout.write(event.Records[0].eventName); - callback(null, { message: 'Hello from S3!', event }); -}; diff --git a/tests/integration/aws/s3/single-event-single-function-single-bucket/tests.js b/tests/integration/aws/s3/single-event-single-function-single-bucket/tests.js deleted file mode 100644 index 7c09075a2..000000000 --- a/tests/integration/aws/s3/single-event-single-function-single-bucket/tests.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict'; - -const path = require('path'); -const expect = require('chai').expect; -const Utils = require('../../../../utils/index'); - -describe('AWS - S3: Single event in a single function with a single bucket', () => { - beforeAll(() => { - Utils.createTestService('aws-nodejs', path.join(__dirname, 'service')); - Utils.deployService(); - }); - - it('should trigger function when object created in bucket', () => Utils - .createAndRemoveInBucket(process.env.BUCKET_1) - .delay(60000) - .then(() => { - const logs = Utils.getFunctionLogs('hello'); - expect(/aws:s3/g.test(logs)).to.equal(true); - expect(/ObjectCreated:Put/g.test(logs)).to.equal(true); - }) - ); - - afterAll(() => { - Utils.removeService(); - }); -}); diff --git a/tests/integration/aws/schedule/multiple-schedules-multiple-functions/service/handler.js b/tests/integration/aws/schedule/multiple-schedules-multiple-functions/service/handler.js deleted file mode 100644 index 424b93300..000000000 --- a/tests/integration/aws/schedule/multiple-schedules-multiple-functions/service/handler.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; - -module.exports.hello = (event, context, callback) => { - process.stdout.write(event.source); - process.stdout.write(event['detail-type']); - callback(null, { message: 'Hello from Schedule!', event }); -}; - -module.exports.world = (event, context, callback) => { - process.stdout.write(event.source); - process.stdout.write(event['detail-type']); - callback(null, { message: 'Hello from Schedule!', event }); -}; diff --git a/tests/integration/aws/schedule/multiple-schedules-multiple-functions/service/serverless.yml b/tests/integration/aws/schedule/multiple-schedules-multiple-functions/service/serverless.yml deleted file mode 100644 index 4ed8d4953..000000000 --- a/tests/integration/aws/schedule/multiple-schedules-multiple-functions/service/serverless.yml +++ /dev/null @@ -1,15 +0,0 @@ -service: aws-nodejs - -provider: - name: aws - runtime: nodejs6.10 - -functions: - hello: - handler: handler.hello - events: - - schedule: rate(1 minute) - world: - handler: handler.world - events: - - schedule: rate(1 minute) diff --git a/tests/integration/aws/schedule/multiple-schedules-multiple-functions/tests.js b/tests/integration/aws/schedule/multiple-schedules-multiple-functions/tests.js deleted file mode 100644 index 51ae9cb57..000000000 --- a/tests/integration/aws/schedule/multiple-schedules-multiple-functions/tests.js +++ /dev/null @@ -1,30 +0,0 @@ -'use strict'; - -const path = require('path'); -const expect = require('chai').expect; -const Utils = require('../../../../utils/index'); -const BbPromise = require('bluebird'); - -describe('AWS - Schedule: Multiple schedules with multiple functions', () => { - beforeAll(() => { - Utils.createTestService('aws-nodejs', path.join(__dirname, 'service')); - Utils.deployService(); - }); - - it('should trigger functions every minute', () => BbPromise.resolve() - .delay(100000) - .then(() => { - const helloLogs = Utils.getFunctionLogs('hello'); - const worldLogs = Utils.getFunctionLogs('world'); - - expect(/Scheduled Event/g.test(helloLogs)).to.equal(true); - expect(/aws\.events/g.test(helloLogs)).to.equal(true); - expect(/Scheduled Event/g.test(worldLogs)).to.equal(true); - expect(/aws\.events/g.test(worldLogs)).to.equal(true); - }) - ); - - afterAll(() => { - Utils.removeService(); - }); -}); diff --git a/tests/integration/aws/sns/existing-topic/service/handler.js b/tests/integration/aws/sns/existing-topic/service/handler.js deleted file mode 100644 index f3f54b9ab..000000000 --- a/tests/integration/aws/sns/existing-topic/service/handler.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -module.exports.hello = (event, context, callback) => { - process.stdout.write(event.Records[0].EventSource); - process.stdout.write(event.Records[0].Sns.Message); - callback(null, { message: 'Hello from SNS!', event }); -}; diff --git a/tests/integration/aws/sns/existing-topic/service/serverless.yml b/tests/integration/aws/sns/existing-topic/service/serverless.yml deleted file mode 100644 index 472535142..000000000 --- a/tests/integration/aws/sns/existing-topic/service/serverless.yml +++ /dev/null @@ -1,20 +0,0 @@ -service: aws-nodejs - -provider: - name: aws - runtime: nodejs6.10 - -functions: - hello: - handler: handler.hello - events: - - sns: - arn: - Fn::Join: - - ':' - - - - 'arn:aws:sns' - - ${env:EXISTING_TOPIC_REGION} - - ${env:EXISTING_TOPIC_ACCOUNT} - - ${env:EXISTING_TOPIC_NAME} - topicName: ${env:EXISTING_TOPIC_NAME} diff --git a/tests/integration/aws/sns/existing-topic/tests.js b/tests/integration/aws/sns/existing-topic/tests.js deleted file mode 100644 index 414bd3e76..000000000 --- a/tests/integration/aws/sns/existing-topic/tests.js +++ /dev/null @@ -1,38 +0,0 @@ -'use strict'; - -const path = require('path'); -const expect = require('chai').expect; -const Utils = require('../../../../utils/index'); -const uuid = require('uuid'); - -describe('AWS - SNS: Existing topic with single function', () => { - const snsTopic = uuid.v4(); - - beforeAll(() => Utils.createSnsTopic(snsTopic) - .then((result) => { - const splitTopicArn = result.topicArn.split(':'); - process.env.EXISTING_TOPIC_REGION = splitTopicArn[3]; - process.env.EXISTING_TOPIC_ACCOUNT = splitTopicArn[4]; - process.env.EXISTING_TOPIC_NAME = splitTopicArn[5]; - }) - .then(() => { - Utils.createTestService('aws-nodejs', path.join(__dirname, 'service')); - Utils.deployService(); - }) - ); - - it('should trigger function when new message is published', () => Utils - .publishSnsMessage(snsTopic, 'hello world') - .delay(60000) - .then(() => { - const logs = Utils.getFunctionLogs('hello'); - expect(/aws:sns/g.test(logs)).to.equal(true); - expect(/hello world/g.test(logs)).to.equal(true); - }) - ); - - afterAll(() => { - Utils.removeService(); - Utils.removeSnsTopic(snsTopic); - }); -}); diff --git a/tests/integration/aws/sns/multiple-topics-multiple-functions/service/handler.js b/tests/integration/aws/sns/multiple-topics-multiple-functions/service/handler.js deleted file mode 100644 index 9d0517bea..000000000 --- a/tests/integration/aws/sns/multiple-topics-multiple-functions/service/handler.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; - -module.exports.hello = (event, context, callback) => { - process.stdout.write(event.Records[0].EventSource); - process.stdout.write(event.Records[0].Sns.Message); - callback(null, { message: 'Hello from SNS!', event }); -}; - -module.exports.world = (event, context, callback) => { - process.stdout.write(event.Records[0].EventSource); - process.stdout.write(event.Records[0].Sns.Message); - callback(null, { message: 'Hello from SNS!', event }); -}; diff --git a/tests/integration/aws/sns/multiple-topics-multiple-functions/service/serverless.yml b/tests/integration/aws/sns/multiple-topics-multiple-functions/service/serverless.yml deleted file mode 100644 index 6e35cd187..000000000 --- a/tests/integration/aws/sns/multiple-topics-multiple-functions/service/serverless.yml +++ /dev/null @@ -1,15 +0,0 @@ -service: aws-nodejs - -provider: - name: aws - runtime: nodejs6.10 - -functions: - hello: - handler: handler.hello - events: - - sns: ${env:TOPIC_1} - world: - handler: handler.world - events: - - sns: ${env:TOPIC_2} diff --git a/tests/integration/aws/sns/multiple-topics-multiple-functions/tests.js b/tests/integration/aws/sns/multiple-topics-multiple-functions/tests.js deleted file mode 100644 index df3420898..000000000 --- a/tests/integration/aws/sns/multiple-topics-multiple-functions/tests.js +++ /dev/null @@ -1,31 +0,0 @@ -'use strict'; - -const path = require('path'); -const expect = require('chai').expect; -const Utils = require('../../../../utils/index'); - -describe('AWS - SNS: Multiple topics with multiple functions', () => { - beforeAll(() => { - Utils.createTestService('aws-nodejs', path.join(__dirname, 'service')); - Utils.deployService(); - }); - - it('should trigger function when new message is published', () => Utils - .publishSnsMessage(process.env.TOPIC_1, 'topic1') - .then(() => Utils.publishSnsMessage(process.env.TOPIC_2, 'topic2')) - .delay(60000) - .then(() => { - const helloLogs = Utils.getFunctionLogs('hello'); - const worldLogs = Utils.getFunctionLogs('world'); - - expect(/aws:sns/g.test(helloLogs)).to.equal(true); - expect(/topic1/g.test(helloLogs)).to.equal(true); - expect(/aws:sns/g.test(worldLogs)).to.equal(true); - expect(/topic2/g.test(worldLogs)).to.equal(true); - }) - ); - - afterAll(() => { - Utils.removeService(); - }); -}); diff --git a/tests/integration/aws/sns/multiple-topics-single-function/service/handler.js b/tests/integration/aws/sns/multiple-topics-single-function/service/handler.js deleted file mode 100644 index f3f54b9ab..000000000 --- a/tests/integration/aws/sns/multiple-topics-single-function/service/handler.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -module.exports.hello = (event, context, callback) => { - process.stdout.write(event.Records[0].EventSource); - process.stdout.write(event.Records[0].Sns.Message); - callback(null, { message: 'Hello from SNS!', event }); -}; diff --git a/tests/integration/aws/sns/multiple-topics-single-function/service/serverless.yml b/tests/integration/aws/sns/multiple-topics-single-function/service/serverless.yml deleted file mode 100644 index 3a29ef219..000000000 --- a/tests/integration/aws/sns/multiple-topics-single-function/service/serverless.yml +++ /dev/null @@ -1,12 +0,0 @@ -service: aws-nodejs - -provider: - name: aws - runtime: nodejs6.10 - -functions: - hello: - handler: handler.hello - events: - - sns: ${env:TOPIC_1} - - sns: ${env:TOPIC_2} diff --git a/tests/integration/aws/sns/multiple-topics-single-function/tests.js b/tests/integration/aws/sns/multiple-topics-single-function/tests.js deleted file mode 100644 index 6c6122cce..000000000 --- a/tests/integration/aws/sns/multiple-topics-single-function/tests.js +++ /dev/null @@ -1,28 +0,0 @@ -'use strict'; - -const path = require('path'); -const expect = require('chai').expect; -const Utils = require('../../../../utils/index'); - -describe('AWS - SNS: Multiple topics single function', () => { - beforeAll(() => { - Utils.createTestService('aws-nodejs', path.join(__dirname, 'service')); - Utils.deployService(); - }); - - it('should trigger function when new message is published', () => Utils - .publishSnsMessage(process.env.TOPIC_1, 'topic1') - .then(() => Utils.publishSnsMessage(process.env.TOPIC_2, 'topic2')) - .delay(60000) - .then(() => { - const logs = Utils.getFunctionLogs('hello'); - expect(/aws:sns/g.test(logs)).to.equal(true); - expect(/topic1/g.test(logs)).to.equal(true); - expect(/topic2/g.test(logs)).to.equal(true); - }) - ); - - afterAll(() => { - Utils.removeService(); - }); -}); diff --git a/tests/integration/aws/sns/single-topic-multiple-functions/service/handler.js b/tests/integration/aws/sns/single-topic-multiple-functions/service/handler.js deleted file mode 100644 index 9d0517bea..000000000 --- a/tests/integration/aws/sns/single-topic-multiple-functions/service/handler.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; - -module.exports.hello = (event, context, callback) => { - process.stdout.write(event.Records[0].EventSource); - process.stdout.write(event.Records[0].Sns.Message); - callback(null, { message: 'Hello from SNS!', event }); -}; - -module.exports.world = (event, context, callback) => { - process.stdout.write(event.Records[0].EventSource); - process.stdout.write(event.Records[0].Sns.Message); - callback(null, { message: 'Hello from SNS!', event }); -}; diff --git a/tests/integration/aws/sns/single-topic-multiple-functions/service/serverless.yml b/tests/integration/aws/sns/single-topic-multiple-functions/service/serverless.yml deleted file mode 100644 index 28ec23690..000000000 --- a/tests/integration/aws/sns/single-topic-multiple-functions/service/serverless.yml +++ /dev/null @@ -1,15 +0,0 @@ -service: aws-nodejs - -provider: - name: aws - runtime: nodejs6.10 - -functions: - hello: - handler: handler.hello - events: - - sns: ${env:TOPIC_1} - world: - handler: handler.world - events: - - sns: ${env:TOPIC_1} diff --git a/tests/integration/aws/sns/single-topic-multiple-functions/tests.js b/tests/integration/aws/sns/single-topic-multiple-functions/tests.js deleted file mode 100644 index 03b4977ca..000000000 --- a/tests/integration/aws/sns/single-topic-multiple-functions/tests.js +++ /dev/null @@ -1,30 +0,0 @@ -'use strict'; - -const path = require('path'); -const expect = require('chai').expect; -const Utils = require('../../../../utils/index'); - -describe('AWS - SNS: Single topic with multiple functions', () => { - beforeAll(() => { - Utils.createTestService('aws-nodejs', path.join(__dirname, 'service')); - Utils.deployService(); - }); - - it('should trigger function when new message is published', () => Utils - .publishSnsMessage(process.env.TOPIC_1, 'hello world') - .delay(60000) - .then(() => { - const helloLogs = Utils.getFunctionLogs('hello'); - const worldLogs = Utils.getFunctionLogs('world'); - - expect(/aws:sns/g.test(helloLogs)).to.equal(true); - expect(/hello world/g.test(helloLogs)).to.equal(true); - expect(/aws:sns/g.test(worldLogs)).to.equal(true); - expect(/hello world/g.test(worldLogs)).to.equal(true); - }) - ); - - afterAll(() => { - Utils.removeService(); - }); -}); diff --git a/tests/integration/aws/sns/single-topic-single-function/service/handler.js b/tests/integration/aws/sns/single-topic-single-function/service/handler.js deleted file mode 100644 index f3f54b9ab..000000000 --- a/tests/integration/aws/sns/single-topic-single-function/service/handler.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -module.exports.hello = (event, context, callback) => { - process.stdout.write(event.Records[0].EventSource); - process.stdout.write(event.Records[0].Sns.Message); - callback(null, { message: 'Hello from SNS!', event }); -}; diff --git a/tests/integration/aws/sns/single-topic-single-function/service/serverless.yml b/tests/integration/aws/sns/single-topic-single-function/service/serverless.yml deleted file mode 100644 index 2168713c2..000000000 --- a/tests/integration/aws/sns/single-topic-single-function/service/serverless.yml +++ /dev/null @@ -1,11 +0,0 @@ -service: aws-nodejs - -provider: - name: aws - runtime: nodejs6.10 - -functions: - hello: - handler: handler.hello - events: - - sns: ${env:TOPIC_1} diff --git a/tests/integration/aws/sns/single-topic-single-function/tests.js b/tests/integration/aws/sns/single-topic-single-function/tests.js deleted file mode 100644 index fcf655144..000000000 --- a/tests/integration/aws/sns/single-topic-single-function/tests.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict'; - -const path = require('path'); -const expect = require('chai').expect; -const Utils = require('../../../../utils/index'); - -describe('AWS - SNS: Single topic with single function', () => { - beforeAll(() => { - Utils.createTestService('aws-nodejs', path.join(__dirname, 'service')); - Utils.deployService(); - }); - - it('should trigger function when new message is published', () => Utils - .publishSnsMessage(process.env.TOPIC_1, 'hello world') - .delay(60000) - .then(() => { - const logs = Utils.getFunctionLogs('hello'); - expect(/aws:sns/g.test(logs)).to.equal(true); - expect(/hello world/g.test(logs)).to.equal(true); - }) - ); - - afterAll(() => { - Utils.removeService(); - }); -}); diff --git a/tests/integration/general/custom-plugins/service/handler.js b/tests/integration/general/custom-plugins/service/handler.js deleted file mode 100644 index 6313e306f..000000000 --- a/tests/integration/general/custom-plugins/service/handler.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict'; - -// Your first function handler -module.exports.hello = (event, context, callback) => { - callback(null, { message: 'Go Serverless v1.0! Your function executed successfully!', event }); -}; diff --git a/tests/integration/general/custom-plugins/service/package.json b/tests/integration/general/custom-plugins/service/package.json deleted file mode 100644 index 2970090d2..000000000 --- a/tests/integration/general/custom-plugins/service/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "test-service", - "version": "0.1.0", - "dependencies": {} -} diff --git a/tests/integration/general/custom-plugins/service/serverless-plugin-greeter/main.js b/tests/integration/general/custom-plugins/service/serverless-plugin-greeter/main.js deleted file mode 100644 index 0d028622a..000000000 --- a/tests/integration/general/custom-plugins/service/serverless-plugin-greeter/main.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict'; - -class Greeter { - constructor(serverless, options) { - this.serverless = serverless; - this.options = options; - - this.commands = { - greet: { - lifecycleEvents: [ - 'greet', - ], - }, - }; - - this.hooks = { - 'greet:greet': this.greet.bind(this), - }; - } - - greet() { - process.stdout.write('Hello from the greeter plugin!'); - } -} - -module.exports = Greeter; diff --git a/tests/integration/general/custom-plugins/service/serverless-plugin-greeter/package.json b/tests/integration/general/custom-plugins/service/serverless-plugin-greeter/package.json deleted file mode 100644 index ff4faea7e..000000000 --- a/tests/integration/general/custom-plugins/service/serverless-plugin-greeter/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "serverless-plugin-greeter", - "version": "0.1.0", - "main": "main.js" -} diff --git a/tests/integration/general/custom-plugins/service/serverless.yml b/tests/integration/general/custom-plugins/service/serverless.yml deleted file mode 100644 index 9357321be..000000000 --- a/tests/integration/general/custom-plugins/service/serverless.yml +++ /dev/null @@ -1,12 +0,0 @@ -service: aws-nodejs # NOTE: update this with your service name - -provider: - name: aws - runtime: nodejs6.10 - -functions: - hello: - handler: handler.hello - -plugins: - - serverless-plugin-greeter diff --git a/tests/integration/general/custom-plugins/tests.js b/tests/integration/general/custom-plugins/tests.js deleted file mode 100644 index 6da6213f6..000000000 --- a/tests/integration/general/custom-plugins/tests.js +++ /dev/null @@ -1,36 +0,0 @@ -'use strict'; - -const path = require('path'); -const expect = require('chai').expect; -const execSync = require('child_process').execSync; - -const Utils = require('../../../utils/index'); - -describe('General: Custom plugins test', () => { - beforeAll(() => { - Utils.createTestService('aws-nodejs', path.join(__dirname, 'service')); - - // cd into the plugins directory - execSync('cd serverless-plugin-greeter'); - - // link and install the npm package / plugin - execSync('npm link serverless-plugin-greeter && npm install --save serverless-plugin-greeter'); - - // cd back into the service directory - execSync('cd ..'); - }); - - it('should successfully run the greet command of the custom plugin', () => { - const pluginExecution = execSync(`${Utils.serverlessExec} greet`); - - // note: the result will return a newline at the end - const result = new Buffer(pluginExecution, 'base64').toString(); - - expect(result).to.equal('Hello from the greeter plugin!'); - }); - - afterAll(() => { - // unlink the npm package - execSync('npm r serverless-plugin-greeter -g'); - }); -}); diff --git a/tests/integration/general/local-plugins/service/.serverless_plugins/one.js b/tests/integration/general/local-plugins/service/.serverless_plugins/one.js deleted file mode 100644 index 016b2416e..000000000 --- a/tests/integration/general/local-plugins/service/.serverless_plugins/one.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict'; - -class ServerlessPlugin { - constructor(serverless, options) { - this.serverless = serverless; - this.options = options; - - this.commands = { - one: { - usage: 'test plugin', - lifecycleEvents: [ - 'hello', - ], - }, - }; - - this.hooks = { - 'before:one:hello': this.beforeWelcome.bind(this), - }; - } - - beforeWelcome() { - this.serverless.cli.log('plugin one ran successfully!'); - } -} - -module.exports = ServerlessPlugin; diff --git a/tests/integration/general/local-plugins/service/.serverless_plugins/two/index.js b/tests/integration/general/local-plugins/service/.serverless_plugins/two/index.js deleted file mode 100644 index 566e6647a..000000000 --- a/tests/integration/general/local-plugins/service/.serverless_plugins/two/index.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict'; - -class ServerlessPlugin { - constructor(serverless, options) { - this.serverless = serverless; - this.options = options; - - this.commands = { - two: { - usage: 'test plugin', - lifecycleEvents: [ - 'hello', - ], - }, - }; - - this.hooks = { - 'before:two:hello': this.beforeWelcome.bind(this), - }; - } - - beforeWelcome() { - this.serverless.cli.log('plugin two ran successfully!'); - } -} - -module.exports = ServerlessPlugin; diff --git a/tests/integration/general/local-plugins/service/handler.js b/tests/integration/general/local-plugins/service/handler.js deleted file mode 100644 index 6313e306f..000000000 --- a/tests/integration/general/local-plugins/service/handler.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict'; - -// Your first function handler -module.exports.hello = (event, context, callback) => { - callback(null, { message: 'Go Serverless v1.0! Your function executed successfully!', event }); -}; diff --git a/tests/integration/general/local-plugins/service/serverless.yml b/tests/integration/general/local-plugins/service/serverless.yml deleted file mode 100644 index 0a155c126..000000000 --- a/tests/integration/general/local-plugins/service/serverless.yml +++ /dev/null @@ -1,13 +0,0 @@ -service: aws-nodejs # NOTE: update this with your service name - -provider: - name: aws - runtime: nodejs6.10 - -functions: - hello: - handler: handler.hello - -plugins: - - one - - two diff --git a/tests/integration/general/local-plugins/tests.js b/tests/integration/general/local-plugins/tests.js deleted file mode 100644 index 54594cf22..000000000 --- a/tests/integration/general/local-plugins/tests.js +++ /dev/null @@ -1,25 +0,0 @@ -'use strict'; - -const path = require('path'); -const expect = require('chai').expect; -const execSync = require('child_process').execSync; - -const Utils = require('../../../utils/index'); - -describe('General: Local plugins test', () => { - beforeAll(() => { - Utils.createTestService('aws-nodejs', path.join(__dirname, 'service')); - }); - - it('should successfully run the one command', () => { - const pluginExecution = execSync(`${Utils.serverlessExec} one`); - const result = new Buffer(pluginExecution, 'base64').toString(); - expect(/plugin one ran successfully/g.test(result)).to.equal(true); - }); - - it('should successfully run the two command', () => { - const pluginExecution = execSync(`${Utils.serverlessExec} two`); - const result = new Buffer(pluginExecution, 'base64').toString(); - expect(/plugin two ran successfully/g.test(result)).to.equal(true); - }); -}); diff --git a/tests/mocha-reporter.js b/tests/mocha-reporter.js new file mode 100644 index 000000000..689814889 --- /dev/null +++ b/tests/mocha-reporter.js @@ -0,0 +1,130 @@ +'use strict'; + +// Unhandled rejections are not exposed in Mocha, enforce it +// https://github.com/mochajs/mocha/issues/2640 +process.on('unhandledRejection', err => { + throw err; +}); + +const { join } = require('path'); +const os = require('os'); +const Spec = require('mocha/lib/reporters/spec'); +const Runner = require('mocha/lib/runner'); +const { ensureDirSync, removeSync } = require('fs-extra'); +const chalk = require('chalk'); +const { tmpDirCommonPath } = require('../tests/utils/fs'); +const { skippedWithNotice } = require('../tests/utils/misc'); + +// Ensure faster tests propagation +// It's to expose errors otherwise hidden by race conditions +// Reported to Mocha with: https://github.com/mochajs/mocha/issues/3920 +Runner.immediately = process.nextTick; + +// Speed up Bluebird's unhandled rejection notifications so it's on par with timing +// we observe with native promises, and so they do not interfere with an async leaks detector +const BbPromise = require('bluebird'); +/* eslint-disable no-underscore-dangle */ +BbPromise.prototype._ensurePossibleRejectionHandled = function() { + if ((this._bitField & 524288) !== 0) return; + this._setRejectionIsUnhandled(); + process.nextTick(() => this._notifyUnhandledRejection()); +}; +/* eslint-enable */ + +// Ensure to not mess with real homedir +// Tests do not mock config handling, which during tests generates and edits user's serverlessrc +// By overriding homedir resolution we prevent updates to real ~/.serverlessrc +os.homedir = () => tmpDirCommonPath; +if (process.env.USERPROFILE) process.env.USERPROFILE = tmpDirCommonPath; +if (process.env.HOME) process.env.HOME = tmpDirCommonPath; + +ensureDirSync(tmpDirCommonPath); // Ensure temporary homedir exists + +module.exports = class ServerlessSpec extends Spec { + constructor(runner) { + super(runner); + + process.on('uncaughtException', err => { + if (!process.listenerCount('exit')) { + if (process.listenerCount('uncaughtException') === 1) { + // Mocha didn't setup listeners yet, ensure error is exposed + throw err; + } + + // Mocha ignores uncaught exceptions if they happen in conext of skipped test, expose them + // https://github.com/mochajs/mocha/issues/3938 + if (runner.currentRunnable.isPending()) throw err; + return; + } + // If there's an uncaught exception after rest runner wraps up + // Mocha reports it with success exit code: https://github.com/mochajs/mocha/issues/3917 + // Workaround that (otherwise we may end with green CI for failed builds): + process.removeAllListeners('exit'); + throw err; + }); + + // After test run for given file finalizes: + // - Enforce eventual current directory change was reverted + // - Ensure to reset eventually created user config file + const startCwd = process.cwd(); + const userConfig = join(tmpDirCommonPath, '.serverlessrc'); + runner.on('suite end', suite => { + if (!suite.parent || !suite.parent.root) return; // Apply just on top level suites + try { + removeSync(userConfig); + } catch (error) { + if (error.code !== 'ENOENT') throw error; + } + if (process.cwd() !== startCwd) { + runner._abort = true; // eslint-disable-line no-underscore-dangle,no-param-reassign + throw new Error( + `Tests in ${suite.file.slice(startCwd.length + 1)} didn't revert ` + + 'current directory change. This may affect resuls of upcoming tests.' + ); + } + }); + + runner.on('end', () => { + // Output eventual skip notices + if (skippedWithNotice.length) { + const resolveTestName = test => { + const names = [test.title]; + let parent = test.parent; + while (parent) { + if (parent.title) names.push(parent.title); + parent = parent.parent; + } + return `${chalk.cyan(names.reverse().join(': '))} (in: ${chalk.grey( + test.file.slice(process.cwd().length + 1) + )})`; + }; + process.stdout.write( + ' Notice: Some tests were skipped due to following environment issues:' + + `\n\n - ${skippedWithNotice + .map( + meta => `${resolveTestName(meta.context.test)}\n\n ${chalk.red(meta.reason)}\n` + ) + .join('\n - ')}\n\n` + ); + } + + // Cleanup temporary homedir + try { + removeSync(tmpDirCommonPath); + } catch (error) { + // Safe to ignore + } + + if (process.version.match(/\d+/)[0] < 8) return; // Async leaks detector is not reliable in Node.js v6 + + // Async leaks detection + setTimeout(() => { + // If tests end with any orphaned async call then this callback will be invoked + // It's a signal there's some promise chain (or in general async flow) miconfiguration + throw new Error('Test ended with unfinished async jobs'); + // Timeout '2' to ensure no false positives, with '1' there are observable rare scenarios + // of possibly a garbage collector delaying process exit being picked up + }, 2).unref(); + }); + } +}; diff --git a/tests/setup-tests.js b/tests/setup-tests.js new file mode 100644 index 000000000..32c0f7f96 --- /dev/null +++ b/tests/setup-tests.js @@ -0,0 +1,3 @@ +'use strict'; + +jest.setTimeout(300000); diff --git a/tests/setupTests.js b/tests/setupTests.js deleted file mode 100644 index 4da8b0798..000000000 --- a/tests/setupTests.js +++ /dev/null @@ -1,3 +0,0 @@ -// timeout is set to 5 minutes -// eslint-disable-next-line no-undef -jasmine.DEFAULT_TIMEOUT_INTERVAL = 300000; diff --git a/tests/templates/test_all_templates b/tests/templates/test-all-templates similarity index 100% rename from tests/templates/test_all_templates rename to tests/templates/test-all-templates diff --git a/tests/utils/api-gateway/index.js b/tests/utils/api-gateway/index.js new file mode 100644 index 000000000..07b07b902 --- /dev/null +++ b/tests/utils/api-gateway/index.js @@ -0,0 +1,68 @@ +'use strict'; + +const AWS = require('aws-sdk'); +const _ = require('lodash'); +const { region, persistentRequest } = require('../misc'); + +function createRestApi(name) { + const APIG = new AWS.APIGateway({ region }); + + const params = { + name, + }; + + return APIG.createRestApi(params).promise(); +} + +function deleteRestApi(restApiId) { + const APIG = new AWS.APIGateway({ region }); + + const params = { + restApiId, + }; + + return APIG.deleteRestApi(params).promise(); +} + +function getResources(restApiId) { + const APIG = new AWS.APIGateway({ region }); + + const params = { + restApiId, + }; + + return APIG.getResources(params) + .promise() + .then(data => data.items); +} + +function findRestApis(name) { + const APIG = new AWS.APIGateway({ region }); + + const params = { + limit: 500, + }; + + function recursiveFind(found, position) { + if (position) params.position = position; + return APIG.getRestApis(params) + .promise() + .then(result => { + const matches = result.items.filter(restApi => restApi.name.match(name)); + if (matches.length) { + _.merge(found, matches); + } + if (result.position) return recursiveFind(found, result.position); + return found; + }); + } + + return recursiveFind([]); +} + +module.exports = { + createRestApi: persistentRequest.bind(this, createRestApi), + deleteRestApi: persistentRequest.bind(this, deleteRestApi), + getResources: persistentRequest.bind(this, getResources), + findRestApis: persistentRequest.bind(this, findRestApis), +}; diff --git a/tests/utils/aws-cleanup.js b/tests/utils/aws-cleanup.js new file mode 100644 index 000000000..98f100315 --- /dev/null +++ b/tests/utils/aws-cleanup.js @@ -0,0 +1,88 @@ +'use strict'; + +// NOTE: This script requires Node.js > 8 to run since it uses +// modern Node.js / JavaScript features such as async / await + +const { logger, testServiceIdentifier } = require('./misc'); +const { findStacks, deleteStack, listStackResources } = require('./cloudformation'); +const { findRestApis, deleteRestApi } = require('./api-gateway'); +const { deleteBucket } = require('./s3'); + +async function findDeploymentBuckets(stacks) { + const buckets = []; + for (const stack of stacks) { + const stackResources = await listStackResources(stack.StackId); + const bucket = stackResources.filter(resource => { + return resource.LogicalResourceId === 'ServerlessDeploymentBucket'; + }); + buckets.push(...bucket); + } + return buckets; +} + +async function cleanup() { + const date = new Date(); + const yesterday = date.setDate(date.getDate() - 1); + + const status = [ + 'CREATE_FAILED', + 'CREATE_COMPLETE', + 'UPDATE_COMPLETE', + 'ROLLBACK_FAILED', + 'ROLLBACK_COMPLETE', + 'DELETE_FAILED', + 'UPDATE_ROLLBACK_FAILED', + 'UPDATE_ROLLBACK_COMPLETE', + ]; + + // find all the resources + const stacks = await findStacks(testServiceIdentifier, status); + const apis = await findRestApis(testServiceIdentifier); + + let bucketsToRemove = []; + const stacksToRemove = stacks.filter(stack => +new Date(stack.CreationTime) < yesterday); + const apisToRemove = apis.filter(api => +new Date(api.createdDate) < yesterday); + if (stacksToRemove) { + bucketsToRemove = await findDeploymentBuckets(stacksToRemove); + } + + logger.log(`${bucketsToRemove.length} Buckets to remove...`); + logger.log(`${stacksToRemove.length} Stacks to remove...`); + logger.log(`${apisToRemove.length} APIs to remove...`); + + if (bucketsToRemove.length) { + logger.log('Removing Buckets...'); + const promises = bucketsToRemove.map(bucket => deleteBucket(bucket.PhysicalResourceId)); + try { + await Promise.all(promises); + } catch (error) { + // do nothing... try to continue with cleanup + } + } + + if (stacksToRemove.length) { + logger.log('Removing Stacks...'); + const promises = stacksToRemove.map(stack => deleteStack(stack.StackName)); + try { + await Promise.all(promises); + } catch (error) { + // do nothing... try to continue with cleanup + } + } + + if (apisToRemove.length) { + logger.log('Removing APIs...'); + const promises = apisToRemove.map(api => deleteRestApi(api.id)); + try { + await Promise.all(promises); + } catch (error) { + // do nothing... try to continue with cleanup + } + } +} + +cleanup().catch(error => { + // eslint-disable-next-line no-console + console.error(error); + process.exit(1); +}); diff --git a/tests/utils/child-process.js b/tests/utils/child-process.js new file mode 100644 index 000000000..93f2a716a --- /dev/null +++ b/tests/utils/child-process.js @@ -0,0 +1,16 @@ +'use strict'; + +const { execSync: originalExecSync } = require('child_process'); + +function execSync(command, options = null) { + // Same as native but outputs std in case of error + try { + return originalExecSync(command, options); + } catch (error) { + if (error.stdout) process.stdout.write(error.stdout); + if (error.stderr) process.stderr.write(error.stderr); + throw error; + } +} + +module.exports = { execSync }; diff --git a/tests/utils/cloudformation/index.js b/tests/utils/cloudformation/index.js new file mode 100644 index 000000000..8955e56cb --- /dev/null +++ b/tests/utils/cloudformation/index.js @@ -0,0 +1,78 @@ +'use strict'; + +const AWS = require('aws-sdk'); +const { region, persistentRequest } = require('../misc'); + +function findStacks(name, status) { + const CF = new AWS.CloudFormation({ region }); + + const params = {}; + if (status) { + params.StackStatusFilter = status; + } + + function recursiveFind(found, token) { + if (token) params.NextToken = token; + return CF.listStacks(params) + .promise() + .then(result => { + const matches = result.StackSummaries.filter(stack => stack.StackName.match(name)); + if (matches.length) { + found.push(...matches); + } + if (result.NextToken) return recursiveFind(found, result.NextToken); + return found; + }); + } + + return recursiveFind([]); +} + +function deleteStack(stack) { + const CF = new AWS.CloudFormation({ region }); + + const params = { + StackName: stack, + }; + + return CF.deleteStack(params).promise(); +} + +function listStackResources(stack) { + const CF = new AWS.CloudFormation({ region }); + + const params = { + StackName: stack, + }; + + function recursiveFind(resources, token) { + if (token) params.NextToken = token; + return CF.listStackResources(params) + .promise() + .then(result => { + resources.push(...result.StackResourceSummaries); + if (result.NextToken) return recursiveFind(resources, result.NextToken); + return resources; + }); + } + + return recursiveFind([]); +} + +function listStacks(status) { + const CF = new AWS.CloudFormation({ region }); + + const params = {}; + if (status) { + params.StackStatusFilter = status; + } + + return CF.listStacks(params).promise(); +} + +module.exports = { + findStacks: persistentRequest.bind(this, findStacks), + deleteStack: persistentRequest.bind(this, deleteStack), + listStackResources: persistentRequest.bind(this, listStackResources), + listStacks: persistentRequest.bind(this, listStacks), +}; diff --git a/tests/utils/cloudwatch/index.js b/tests/utils/cloudwatch/index.js new file mode 100644 index 000000000..d9150ad25 --- /dev/null +++ b/tests/utils/cloudwatch/index.js @@ -0,0 +1,22 @@ +'use strict'; + +const AWS = require('aws-sdk'); +const { region, persistentRequest } = require('../misc'); + +function putCloudWatchEvents(sources) { + const cwe = new AWS.CloudWatchEvents({ region }); + + const entries = sources.map(source => ({ + Source: source, + DetailType: 'serverlessDetailType', + Detail: '{ "key1": "value1" }', + })); + const params = { + Entries: entries, + }; + return cwe.putEvents(params).promise(); +} + +module.exports = { + putCloudWatchEvents: persistentRequest.bind(this, putCloudWatchEvents), +}; diff --git a/tests/utils/cognito/index.js b/tests/utils/cognito/index.js new file mode 100644 index 000000000..2c6dca017 --- /dev/null +++ b/tests/utils/cognito/index.js @@ -0,0 +1,35 @@ +'use strict'; + +const AWS = require('aws-sdk'); +const { region, persistentRequest } = require('../misc'); + +function getCognitoUserPoolId(userPoolName) { + const cisp = new AWS.CognitoIdentityServiceProvider({ region }); + + const params = { + MaxResults: 50, + }; + + return cisp + .listUserPools(params) + .promise() + .then( + data => data.UserPools.find(userPool => RegExp(userPoolName, 'g').test(userPool.Name)).Id + ); +} + +function createCognitoUser(userPoolId, username, password) { + const cisp = new AWS.CognitoIdentityServiceProvider({ region }); + + const params = { + UserPoolId: userPoolId, + Username: username, + TemporaryPassword: password, + }; + return cisp.adminCreateUser(params).promise(); +} + +module.exports = { + getCognitoUserPoolId: persistentRequest.bind(this, getCognitoUserPoolId), + createCognitoUser: persistentRequest.bind(this, createCognitoUser), +}; diff --git a/tests/utils/fs/index.js b/tests/utils/fs/index.js new file mode 100644 index 000000000..53220f4b8 --- /dev/null +++ b/tests/utils/fs/index.js @@ -0,0 +1,67 @@ +'use strict'; + +const os = require('os'); +const path = require('path'); +const fs = require('fs'); +const fse = require('fs-extra'); +const crypto = require('crypto'); +const YAML = require('js-yaml'); +const JSZip = require('jszip'); + +const tmpDirCommonPath = path.join( + os.tmpdir(), + 'tmpdirs-serverless', + crypto.randomBytes(2).toString('hex') +); + +function getTmpDirPath() { + return path.join(tmpDirCommonPath, crypto.randomBytes(8).toString('hex')); +} + +function getTmpFilePath(fileName) { + return path.join(getTmpDirPath(), fileName); +} + +function createTmpDir() { + const dirPath = getTmpDirPath(); + fse.ensureDirSync(dirPath); + return dirPath; +} + +function createTmpFile(name) { + const filePath = getTmpFilePath(name); + fse.ensureFileSync(filePath); + return filePath; +} + +function replaceTextInFile(filePath, subString, newSubString) { + const fileContent = fs.readFileSync(filePath).toString(); + fs.writeFileSync(filePath, fileContent.replace(subString, newSubString)); +} + +function readYamlFile(filePath) { + const content = fs.readFileSync(filePath, 'utf8'); + return YAML.safeLoad(content); +} + +function writeYamlFile(filePath, content) { + const yaml = YAML.safeDump(content); + fs.writeFileSync(filePath, yaml); + return yaml; +} + +function listZipFiles(filename) { + return new JSZip().loadAsync(fs.readFileSync(filename)).then(zip => Object.keys(zip.files)); +} + +module.exports = { + tmpDirCommonPath, + getTmpDirPath, + getTmpFilePath, + createTmpDir, + createTmpFile, + replaceTextInFile, + readYamlFile, + writeYamlFile, + listZipFiles, +}; diff --git a/tests/utils/index.js b/tests/utils/index.js deleted file mode 100644 index 687760759..000000000 --- a/tests/utils/index.js +++ /dev/null @@ -1,239 +0,0 @@ -'use strict'; - -const fs = require('fs'); -const os = require('os'); -const path = require('path'); -const crypto = require('crypto'); -const BbPromise = require('bluebird'); -const fse = require('fs-extra'); -const execSync = require('child_process').execSync; -const AWS = require('aws-sdk'); - -// mock to test functionality bound to a serverless plugin -class ServerlessPlugin { - constructor(serverless, options, testSubject) { - this.options = options; - this.serverless = serverless; - - Object.assign( - this, - testSubject - ); - } -} - -const serverlessExec = path.join(__dirname, '..', '..', 'bin', 'serverless'); - -const getTmpDirPath = () => path.join(os.tmpdir(), - 'tmpdirs-serverless', 'serverless', crypto.randomBytes(8).toString('hex')); - -const getTmpFilePath = (fileName) => path.join(getTmpDirPath(), fileName); - -const replaceTextInFile = (filePath, subString, newSubString) => { - const fileContent = fs.readFileSync(filePath).toString(); - fs.writeFileSync(filePath, fileContent.replace(subString, newSubString)); -}; - -module.exports = { - serverlessExec, - getTmpDirPath, - getTmpFilePath, - replaceTextInFile, - ServerlessPlugin, - - createTestService: (templateName, testServiceDir) => { - const hrtime = process.hrtime(); - const serviceName = `test-${hrtime[0]}-${hrtime[1]}`; - const tmpDir = path.join(os.tmpdir(), - 'tmpdirs-serverless', - 'integration-test-suite', - crypto.randomBytes(8).toString('hex')); - - fse.mkdirsSync(tmpDir); - process.chdir(tmpDir); - - // create a new Serverless service - execSync(`${serverlessExec} create --template ${templateName}`, { stdio: 'inherit' }); - - if (testServiceDir) { - fse.copySync(testServiceDir, tmpDir, { clobber: true, preserveTimestamps: true }); - } - - replaceTextInFile('serverless.yml', templateName, serviceName); - - process.env.TOPIC_1 = `${serviceName}-1`; - process.env.TOPIC_2 = `${serviceName}-1`; - process.env.BUCKET_1 = `${serviceName}-1`; - process.env.BUCKET_2 = `${serviceName}-2`; - process.env.COGNITO_USER_POOL_1 = `${serviceName}-1`; - process.env.COGNITO_USER_POOL_2 = `${serviceName}-2`; - - // return the name of the CloudFormation stack - return `${serviceName}-dev`; - }, - - createAndRemoveInBucket(bucketName) { - const S3 = new AWS.S3({ region: 'us-east-1' }); - BbPromise.promisifyAll(S3, { suffix: 'Promised' }); - - const params = { - Bucket: bucketName, - Key: 'object', - Body: 'hello world', - }; - - return S3.putObjectPromised(params) - .then(() => { - delete params.Body; - return S3.deleteObjectPromised(params); - }); - }, - - createSnsTopic(topicName) { - const SNS = new AWS.SNS({ region: 'us-east-1' }); - BbPromise.promisifyAll(SNS, { suffix: 'Promised' }); - - const params = { - Name: topicName, - }; - - return SNS.createTopicPromised(params); - }, - - installPlugin: (installDir, PluginClass) => { - const pluginPkg = { name: path.basename(installDir), version: '0.0.0' }; - const className = (new PluginClass()).constructor.name; - fse.outputFileSync(path.join(installDir, 'package.json'), JSON.stringify(pluginPkg), 'utf8'); - fse.outputFileSync(path.join(installDir, 'index.js'), - `"use strict";\n${PluginClass.toString()}\nmodule.exports = ${className}`, 'utf8'); - }, - - removeSnsTopic(topicName) { - const SNS = new AWS.SNS({ region: 'us-east-1' }); - BbPromise.promisifyAll(SNS, { suffix: 'Promised' }); - - return SNS.listTopicsPromised() - .then(data => { - const topicArn = data.Topics.find(topic => RegExp(topicName, 'g') - .test(topic.TopicArn)).TopicArn; - - const params = { - TopicArn: topicArn, - }; - - return SNS.deleteTopicPromised(params); - }); - }, - - publishSnsMessage(topicName, message) { - const SNS = new AWS.SNS({ region: 'us-east-1' }); - BbPromise.promisifyAll(SNS, { suffix: 'Promised' }); - - return SNS.listTopicsPromised() - .then(data => { - const topicArn = data.Topics.find(topic => RegExp(topicName, 'g') - .test(topic.TopicArn)).TopicArn; - - const params = { - Message: message, - TopicArn: topicArn, - }; - - return SNS.publishPromised(params); - }); - }, - - publishIotData(topic, message) { - const Iot = new AWS.Iot({ region: 'us-east-1' }); - BbPromise.promisifyAll(Iot, { suffix: 'Promised' }); - - return Iot.describeEndpointPromised() - .then(data => { - const IotData = new AWS.IotData({ region: 'us-east-1', endpoint: data.endpointAddress }); - BbPromise.promisifyAll(IotData, { suffix: 'Promised' }); - - const params = { - topic, - payload: new Buffer(message), - }; - - return IotData.publishPromised(params); - }); - }, - - putCloudWatchEvents(sources) { - const cwe = new AWS.CloudWatchEvents({ region: 'us-east-1' }); - BbPromise.promisifyAll(cwe, { suffix: 'Promised' }); - - const entries = []; - sources.forEach(source => { - entries.push({ - Source: source, - DetailType: 'serverlessDetailType', - Detail: '{ "key1": "value1" }', - }); - }); - const params = { - Entries: entries, - }; - return cwe.putEventsPromised(params); - }, - - getCognitoUserPoolId(userPoolName) { - const cisp = new AWS.CognitoIdentityServiceProvider({ region: 'us-east-1' }); - BbPromise.promisifyAll(cisp, { suffix: 'Promised' }); - - const params = { - MaxResults: 50, - }; - - return cisp.listUserPoolsPromised(params) - .then((data) => data.UserPools.find((userPool) => - RegExp(userPoolName, 'g').test(userPool.Name)).Id - ); - }, - - createCognitoUser(userPoolId, username, password) { - const cisp = new AWS.CognitoIdentityServiceProvider({ region: 'us-east-1' }); - BbPromise.promisifyAll(cisp, { suffix: 'Promised' }); - - const params = { - UserPoolId: userPoolId, - Username: username, - TemporaryPassword: password, - }; - return cisp.adminCreateUserPromised(params); - }, - - getFunctionLogs(functionName) { - const logs = execSync(`${serverlessExec} logs --function ${functionName} --noGreeting true`); - const logsString = new Buffer(logs, 'base64').toString(); - process.stdout.write(logsString); - return logsString; - }, - - deployService() { - execSync(`${serverlessExec} deploy`, { stdio: 'inherit' }); - }, - - removeService() { - execSync(`${serverlessExec} remove`, { stdio: 'inherit' }); - }, - - replaceEnv(values) { - const originals = {}; - for (const key of Object.keys(values)) { - if (process.env[key]) { - originals[key] = process.env[key]; - } else { - originals[key] = 'undefined'; - } - if (values[key] === 'undefined') { - delete process.env[key]; - } else { - process.env[key] = values[key]; - } - } - return originals; - }, -}; diff --git a/tests/utils/index.test.js b/tests/utils/index.test.js deleted file mode 100644 index 16efb247e..000000000 --- a/tests/utils/index.test.js +++ /dev/null @@ -1,47 +0,0 @@ -'use strict'; - -const BbPromise = require('bluebird'); -const Serverless = require('../../lib/Serverless'); -const expect = require('chai').expect; -const testUtils = require('./index'); - -describe('Test utils', () => { - describe('#getTmpDirPath()', () => { - it('should return a valid tmpDir path', () => { - const tmpDirPath = testUtils.getTmpDirPath(); - - expect(tmpDirPath).to.match(/.+.{16}/); - }); - }); - - describe('#getTmpFilePath()', () => { - it('should return a valid tmpFile path', () => { - const fileName = 'foo.bar'; - const tmpFilePath = testUtils.getTmpFilePath(fileName); - - expect(tmpFilePath).to.match(/.+.{16}.{1}foo\.bar/); - }); - }); - - describe('ServerlessPlugin', () => { - it('should create a new ServerlessPlugin mock instance', () => { - const ServerlessPlugin = testUtils.ServerlessPlugin; - - const serverless = new Serverless(); - const options = { - stage: 'production', - region: 'my-test-region', - }; - const functionUnderTest = () => BbPromise.resolve('function under test'); - - const serverlessPlugin = new ServerlessPlugin( - serverless, - options, - functionUnderTest - ); - - expect(serverlessPlugin.serverless).to.be.instanceof(Serverless); - expect(serverlessPlugin.options).to.deep.equal(options); - }); - }); -}); diff --git a/tests/utils/iot/index.js b/tests/utils/iot/index.js new file mode 100644 index 000000000..8ab68f841 --- /dev/null +++ b/tests/utils/iot/index.js @@ -0,0 +1,25 @@ +'use strict'; + +const AWS = require('aws-sdk'); +const { region, persistentRequest } = require('../misc'); + +function publishIotData(topic, message) { + const Iot = new AWS.Iot({ region }); + + return Iot.describeEndpoint() + .promise() + .then(data => { + const IotData = new AWS.IotData({ region, endpoint: data.endpointAddress }); + + const params = { + topic, + payload: Buffer.from(message), + }; + + return IotData.publish(params).promise(); + }); +} + +module.exports = { + publishIotData: persistentRequest.bind(this, publishIotData), +}; diff --git a/tests/utils/misc/index.js b/tests/utils/misc/index.js new file mode 100644 index 000000000..0f787ee87 --- /dev/null +++ b/tests/utils/misc/index.js @@ -0,0 +1,181 @@ +'use strict'; + +const path = require('path'); +const fse = require('fs-extra'); +const BbPromise = require('bluebird'); +const chalk = require('chalk'); +const { execSync } = require('../child-process'); +const { readYamlFile, writeYamlFile } = require('../fs'); + +const logger = console; + +const region = 'us-east-1'; + +const testServiceIdentifier = 'integ-test'; + +const serverlessExec = path.resolve(__dirname, '..', '..', '..', 'bin', 'serverless'); + +const serviceNameRegex = new RegExp(`${testServiceIdentifier}-d+`); + +function getServiceName() { + const hrtime = process.hrtime(); + return `${testServiceIdentifier}-${hrtime[1]}`; +} + +function deployService() { + execSync(`${serverlessExec} deploy`); +} + +function removeService() { + execSync(`${serverlessExec} remove`); +} + +function replaceEnv(values) { + const originals = {}; + for (const key of Object.keys(values)) { + if (process.env[key]) { + originals[key] = process.env[key]; + } else { + originals[key] = 'undefined'; + } + if (values[key] === 'undefined') { + delete process.env[key]; + } else { + process.env[key] = values[key]; + } + } + return originals; +} + +function createTestService( + tmpDir, + options = { + // Either templateName or templateDir have to be provided + templateName: null, // Generic template to use (e.g. 'aws-nodejs') + templateDir: null, // Path to custom pre-prepared service template + serverlessConfigHook: null, // Eventual hook that allows to customize serverless config + } +) { + const serviceName = getServiceName(); + + fse.mkdirsSync(tmpDir); + process.chdir(tmpDir); + + if (options.templateName) { + // create a new Serverless service + execSync(`${serverlessExec} create --template ${options.templateName}`); + } else if (options.templateDir) { + fse.copySync(options.templateDir, tmpDir, { clobber: true, preserveTimestamps: true }); + } else { + throw new Error("Either 'templateName' or 'templateDir' options have to be provided"); + } + + const serverlessFilePath = path.join(tmpDir, 'serverless.yml'); + const serverlessConfig = readYamlFile(serverlessFilePath); + // Ensure unique service name + serverlessConfig.service = serviceName; + if (options.serverlessConfigHook) options.serverlessConfigHook(serverlessConfig); + writeYamlFile(serverlessFilePath, serverlessConfig); + + process.env.TOPIC_1 = `${serviceName}-1`; + process.env.TOPIC_2 = `${serviceName}-1`; + process.env.BUCKET_1 = `${serviceName}-1`; + process.env.BUCKET_2 = `${serviceName}-2`; + process.env.COGNITO_USER_POOL_1 = `${serviceName}-1`; + process.env.COGNITO_USER_POOL_2 = `${serviceName}-2`; + + return serverlessConfig; +} + +function getFunctionLogs(functionName) { + const logs = execSync(`${serverlessExec} logs --function ${functionName} --noGreeting true`); + const logsString = Buffer.from(logs, 'base64').toString(); + process.stdout.write(logsString); + return logsString; +} + +function waitForFunctionLogs(functionName, startMarker, endMarker) { + let logs; + return new BbPromise(resolve => { + const interval = setInterval(() => { + logs = getFunctionLogs(functionName); + if (logs && logs.includes(startMarker) && logs.includes(endMarker)) { + clearInterval(interval); + return resolve(logs); + } + return null; + }, 2000); + }); +} + +function persistentRequest(...args) { + const func = args[0]; + const funcArgs = args.slice(1); + const MAX_TRIES = 5; + return new BbPromise((resolve, reject) => { + const doCall = numTry => { + return func.apply(this, funcArgs).then(resolve, e => { + if ( + numTry < MAX_TRIES && + ((e.providerError && e.providerError.retryable) || e.statusCode === 429) + ) { + logger.log( + [ + `Recoverable error occurred (${e.message}), sleeping for 5 seconds.`, + `Try ${numTry + 1} of ${MAX_TRIES}`, + ].join(' ') + ); + setTimeout(doCall, 5000, numTry + 1); + } else { + reject(e); + } + }); + }; + return doCall(0); + }); +} + +const skippedWithNotice = []; + +function skipWithNotice(context, reason, afterCallback) { + if (!context || typeof context.skip !== 'function') { + throw new TypeError('Passed context is not a valid mocha suite'); + } + if (process.env.CI) return; // Do not tolerate skips in CI environment + skippedWithNotice.push({ context, reason }); + process.stdout.write(chalk.yellow(`\n Skipped due to: ${chalk.red(reason)}\n\n`)); + if (afterCallback) { + try { + // Ensure teardown is called + // (Mocha fails to do it -> https://github.com/mochajs/mocha/issues/3740) + afterCallback(); + } catch (error) { + process.stdout.write(chalk.error(`after callback crashed with: ${error.stack}\n`)); + } + } + context.skip(); +} + +function skipOnWindowsDisabledSymlinks(error, context, afterCallback) { + if (error.code !== 'EPERM' || process.platform !== 'win32') return; + skipWithNotice(context, 'Missing admin rights to create symlinks', afterCallback); +} + +module.exports = { + logger, + region, + testServiceIdentifier, + serverlessExec, + serviceNameRegex, + getServiceName, + deployService, + removeService, + replaceEnv, + createTestService, + getFunctionLogs, + waitForFunctionLogs, + persistentRequest, + skippedWithNotice, + skipWithNotice, + skipOnWindowsDisabledSymlinks, +}; diff --git a/tests/utils/plugins/index.js b/tests/utils/plugins/index.js new file mode 100644 index 000000000..32ca791f4 --- /dev/null +++ b/tests/utils/plugins/index.js @@ -0,0 +1,30 @@ +'use strict'; + +const path = require('path'); +const fse = require('fs-extra'); + +// mock to test functionality bound to a serverless plugin +class ServerlessPlugin { + constructor(serverless, options, testSubject) { + this.options = options; + this.serverless = serverless; + + Object.assign(this, testSubject); + } +} + +function installPlugin(installDir, PluginClass) { + const pluginPkg = { name: path.basename(installDir), version: '0.0.0' }; + const className = new PluginClass().constructor.name; + fse.outputFileSync(path.join(installDir, 'package.json'), JSON.stringify(pluginPkg), 'utf8'); + fse.outputFileSync( + path.join(installDir, 'index.js'), + `"use strict";\n${PluginClass.toString()}\nmodule.exports = ${className}`, + 'utf8' + ); +} + +module.exports = { + ServerlessPlugin, + installPlugin, +}; diff --git a/tests/utils/s3/index.js b/tests/utils/s3/index.js new file mode 100644 index 000000000..4768e50fd --- /dev/null +++ b/tests/utils/s3/index.js @@ -0,0 +1,65 @@ +'use strict'; + +const AWS = require('aws-sdk'); +const { region, persistentRequest } = require('../misc'); + +function createBucket(bucket) { + const S3 = new AWS.S3({ region }); + + return S3.createBucket({ Bucket: bucket }).promise(); +} + +function createAndRemoveInBucket(bucket, opts = {}) { + const S3 = new AWS.S3({ region }); + + const prefix = opts.prefix || ''; + const suffix = opts.suffix || ''; + const fileName = opts.fileName || 'object'; + + const params = { + Bucket: bucket, + Key: `${prefix}${fileName}${suffix}`, + Body: 'hello world', + }; + + return S3.putObject(params) + .promise() + .then(() => { + delete params.Body; + return S3.deleteObject(params).promise(); + }); +} + +function emptyBucket(bucket) { + const S3 = new AWS.S3({ region }); + + return S3.listObjects({ Bucket: bucket }) + .promise() + .then(data => { + const items = data.Contents; + const numItems = items.length; + if (numItems) { + const keys = items.map(item => Object.assign({}, { Key: item.Key })); + return S3.deleteObjects({ + Bucket: bucket, + Delete: { + Objects: keys, + }, + }).promise(); + } + return null; + }); +} + +function deleteBucket(bucket) { + const S3 = new AWS.S3({ region }); + + return emptyBucket(bucket).then(() => S3.deleteBucket({ Bucket: bucket }).promise()); +} + +module.exports = { + createBucket: persistentRequest.bind(this, createBucket), + createAndRemoveInBucket: persistentRequest.bind(this, createAndRemoveInBucket), + emptyBucket: persistentRequest.bind(this, emptyBucket), + deleteBucket: persistentRequest.bind(this, deleteBucket), +}; diff --git a/tests/utils/sns/index.js b/tests/utils/sns/index.js new file mode 100644 index 000000000..a899982bb --- /dev/null +++ b/tests/utils/sns/index.js @@ -0,0 +1,55 @@ +'use strict'; + +const AWS = require('aws-sdk'); +const { region, persistentRequest } = require('../misc'); + +function createSnsTopic(topicName) { + const SNS = new AWS.SNS({ region }); + + const params = { + Name: topicName, + }; + + return SNS.createTopic(params).promise(); +} + +function removeSnsTopic(topicName) { + const SNS = new AWS.SNS({ region }); + + return SNS.listTopics() + .promise() + .then(data => { + const topicArn = data.Topics.find(topic => RegExp(topicName, 'g').test(topic.TopicArn)) + .TopicArn; + + const params = { + TopicArn: topicArn, + }; + + return SNS.deleteTopic(params).promise(); + }); +} + +function publishSnsMessage(topicName, message) { + const SNS = new AWS.SNS({ region }); + + return SNS.listTopics() + .promise() + .then(data => { + const topicArn = data.Topics.find(topic => RegExp(topicName, 'g').test(topic.TopicArn)) + .TopicArn; + + const params = { + Message: message, + TopicArn: topicArn, + }; + + return SNS.publish(params).promise(); + }); +} + +module.exports = { + createSnsTopic: persistentRequest.bind(this, createSnsTopic), + removeSnsTopic: persistentRequest.bind(this, removeSnsTopic), + publishSnsMessage: persistentRequest.bind(this, publishSnsMessage), +};