From 6425e4ae49d6b1e26aeb34f4dd3cb76807ee7e65 Mon Sep 17 00:00:00 2001 From: Mariusz Nowak Date: Thu, 22 Apr 2021 16:15:53 +0200 Subject: [PATCH 0001/1101] refactor(CLI): Improve inner flow documentation --- scripts/serverless.js | 36 +++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/scripts/serverless.js b/scripts/serverless.js index c99975b64b2..44ffa54e85c 100755 --- a/scripts/serverless.js +++ b/scripts/serverless.js @@ -45,6 +45,8 @@ const processSpanPromise = (async () => { } const ServerlessError = require('../lib/serverless-error'); + + // Abort if command is not supported in this environment if (commandSchema && commandSchema.isHidden && commandSchema.noSupportNotice) { throw new ServerlessError( `Cannot run \`${command}\` command: ${commandSchema.noSupportNotice}` @@ -95,6 +97,8 @@ const processSpanPromise = (async () => { }; if (!commandSchema || commandSchema.serviceDependencyMode) { + // Command is potentially service specific, follow up with resolution of service config + const resolveConfigurationPath = require('../lib/cli/resolve-configuration-path'); const readConfiguration = require('../lib/configuration/read'); @@ -120,7 +124,7 @@ const processSpanPromise = (async () => { serviceDir = process.cwd(); if (!commandSchema) { // If command was not recognized in first resolution phase - // Parse args again also against schemas commands which require service to be run + // parse args again also against schemas of commands which require service context resolveInput.clear(); ({ command, commands, options, isHelpRequest, commandSchema } = resolveInput( require('../lib/cli/commands-schema/service') @@ -130,7 +134,8 @@ const processSpanPromise = (async () => { // IIFE for maintanance convenience await (async () => { if (_.get(configuration.provider, 'variableSyntax')) { - // Request to rely on old variables resolver explictly, abort + // Request to rely on old variables resolver explictly + // abort (fallback to legacy internal resolution) if (isHelpRequest) return; if (configuration.variablesResolutionMode) { throw new ServerlessError( @@ -181,8 +186,8 @@ const processSpanPromise = (async () => { } } if (!commandSchema && providerName === 'aws') { - // If command was not recognized in first resolution phase - // Parse args again also against schemas commands which require AWS service to be run + // If command was not recognized in previous resolution phases + // parse args again also against schemas commands which require AWS service context resolveInput.clear(); ({ command, commands, options, isHelpRequest, commandSchema } = resolveInput( require('../lib/cli/commands-schema/aws-service') @@ -274,7 +279,6 @@ const processSpanPromise = (async () => { if (!variablesMeta.size) return; // No properties configured with variables - // Resolve all unresolved configuration properties resolverConfiguration.fulfilledSources.add('env'); if (isHelpRequest || commands[0] === 'plugin') { @@ -352,6 +356,8 @@ const processSpanPromise = (async () => { ({ command, commands, options, isHelpRequest, commandSchema } = resolveInput( require('../lib/cli/commands-schema/aws-service') )); + + // Validate result command and options require('../lib/cli/ensure-supported-command')(); } } @@ -372,6 +378,8 @@ const processSpanPromise = (async () => { serverless.invocationId = uuid.v4(); await serverless.init(); if (serverless.invokedInstance) { + // Local (in service) installation was found and initialized internally, + // From now on refer to it only serverless.invokedInstance.invocationId = serverless.invocationId; serverless = serverless.invokedInstance; } @@ -381,11 +389,12 @@ const processSpanPromise = (async () => { if (!configuration) return; let hasFinalCommandSchema = false; if (configuration.plugins) { + // After plugins are loaded, re-resolve CLI command and options schema as plugin + // might have defined extra commands and options + // TODO: Remove "serverless.pluginManager.externalPlugins" check with next major if (serverless.pluginManager.externalPlugins) { if (serverless.pluginManager.externalPlugins.size) { - // After plugins are loaded, re-resolve CLI command and options schema as plugin - // might have defined extra commands and options const commandsSchema = require('../lib/cli/commands-schema/resolve-final')( serverless.pluginManager.externalPlugins, { providerName: providerName || 'aws', configuration } @@ -412,11 +421,15 @@ const processSpanPromise = (async () => { )); } + // Validate result command and options require('../lib/cli/ensure-supported-command')(configuration); if (isHelpRequest) return; if (!_.get(variablesMeta, 'size')) return; + // Resolve remaininig service configuration variables if (providerName === 'aws') { + // Ensure properties which are crucial to some variable source resolvers + // are actually resolved. if ( !ensureResolvedProperty('provider\0credentials', { shouldSilentlyReturnIfLegacyMode: true, @@ -445,6 +458,8 @@ const processSpanPromise = (async () => { // we have full "opt" source data if user didn't explicitly switch to new resolver resolverConfiguration.fulfilledSources.add('opt'); } + + // Register serverless instance and AWS provider specific variable sources resolverConfiguration.sources.sls = require('../lib/configuration/variables/sources/instance-dependent/get-sls')( serverless ); @@ -465,6 +480,7 @@ const processSpanPromise = (async () => { resolverConfiguration.fulfilledSources.add('cf').add('s3').add('ssm'); } + // Register dashboard specific variable source resolvers if (configuration.org && serverless.pluginManager.dashboardPlugin) { for (const [sourceName, sourceConfig] of Object.entries( serverless.pluginManager.dashboardPlugin.configurationVariablesSources @@ -477,6 +493,7 @@ const processSpanPromise = (async () => { const ensurePlainFunction = require('type/plain-function/ensure'); const ensurePlainObject = require('type/plain-object/ensure'); + // Register variable source resolvers provided by external plugins for (const externalPlugin of serverless.pluginManager.externalPlugins) { const pluginName = externalPlugin.constructor.name; if (externalPlugin.configurationVariablesSources != null) { @@ -531,6 +548,7 @@ const processSpanPromise = (async () => { } } + // Having all source resolvers configured, resolve variables await resolveVariables(resolverConfiguration); if (!variablesMeta.size) { serverless.isConfigurationInputResolved = true; @@ -541,6 +559,8 @@ const processSpanPromise = (async () => { ) { return; } + + // Report unrecognized variable sources found in variables configured in service config const unresolvedSources = require('../lib/configuration/variables/resolve-unresolved-source-types')( variablesMeta ); @@ -607,8 +627,10 @@ const processSpanPromise = (async () => { })(); if (isHelpRequest && serverless.pluginManager.externalPlugins) { + // Show help require('../lib/cli/render-help')(serverless.pluginManager.externalPlugins); } else { + // Run command await serverless.run(); } } catch (error) { From 53a7872f78f51938b409925e052d34f2dc85abbd Mon Sep 17 00:00:00 2001 From: Mariusz Nowak Date: Fri, 23 Apr 2021 09:53:00 +0200 Subject: [PATCH 0002/1101] fix(Variables): Fix unresolved sources resolver --- .../resolve-unresolved-source-types.js | 28 ++++--- .../resolve-unresolved-source-types.test.js | 75 ++++++++++++++----- 2 files changed, 73 insertions(+), 30 deletions(-) diff --git a/lib/configuration/variables/resolve-unresolved-source-types.js b/lib/configuration/variables/resolve-unresolved-source-types.js index 2130c2546cc..bab1a8fec1b 100644 --- a/lib/configuration/variables/resolve-unresolved-source-types.js +++ b/lib/configuration/variables/resolve-unresolved-source-types.js @@ -1,21 +1,29 @@ 'use strict'; const processVariables = (propertyPath, variablesMeta, resultMap) => { - if (!variablesMeta.variables) return; + let hasUnresolvedSources = false; + if (!variablesMeta.variables) return hasUnresolvedSources; for (const variableMeta of variablesMeta.variables) { if (!variableMeta.sources) continue; - for (const sourceData of variableMeta.sources) { - if (!sourceData.type) continue; - if (!resultMap.has(sourceData.type)) resultMap.set(sourceData.type, new Set()); - resultMap.get(sourceData.type).add(propertyPath); - if (sourceData.params) { - for (const paramData of sourceData.params) { - processVariables(propertyPath, paramData, resultMap); - } + const sourceData = variableMeta.sources[0]; + if (!sourceData.type) continue; + if (sourceData.params) { + for (const paramData of sourceData.params) { + if (processVariables(propertyPath, paramData, resultMap)) hasUnresolvedSources = true; } - if (sourceData.address) processVariables(propertyPath, sourceData.address, resultMap); + if (hasUnresolvedSources) continue; } + if (sourceData.address) { + if (processVariables(propertyPath, sourceData.address, resultMap)) { + hasUnresolvedSources = true; + continue; + } + } + hasUnresolvedSources = true; + if (!resultMap.has(sourceData.type)) resultMap.set(sourceData.type, new Set()); + resultMap.get(sourceData.type).add(propertyPath); } + return hasUnresolvedSources; }; module.exports = (propertiesVariablesMeta) => { diff --git a/test/unit/lib/configuration/variables/resolve-unresolved-source-types.test.js b/test/unit/lib/configuration/variables/resolve-unresolved-source-types.test.js index ab694ea8d64..83318f32055 100644 --- a/test/unit/lib/configuration/variables/resolve-unresolved-source-types.test.js +++ b/test/unit/lib/configuration/variables/resolve-unresolved-source-types.test.js @@ -3,44 +3,79 @@ const { expect } = require('chai'); const resolveMeta = require('../../../../../lib/configuration/variables/resolve-meta'); +const resolve = require('../../../../../lib/configuration/variables/resolve'); const resolveUnresolvedSourceTypes = require('../../../../../lib/configuration/variables/resolve-unresolved-source-types'); describe('test/unit/lib/configuration/variables/resolve-unresolved-source-types.test.js', () => { const configuration = { - foo: { - params: '${sourceParam(param1, param2)}', - varParam: '${sourceParam(${sourceDirect:})}', + resolved: 'foo${recognized:}', + unrecognized: 'foo${unrecognized:}', + unrecognizedInParens: 'foo${recognized(${unrecognized:}, ${unrecognized2:})}', + unrecognizedInAddress: 'foo${recognized:${unrecognized:}}', + unrecognizedInParensAndAddress: + 'foo${recognized(${unrecognized:}, ${unrecognized2:}):${unrecognized3:}}', + unrecognizedFallback: 'foo${recognized:, unrecognized:}', + otherUnrecognizedFallback: 'foo${unrecognized:, unrecognized4:}', + deep: { + resolved: 'foo${recognized:}', + unrecognized: 'foo${unrecognized:}', + unrecognizedInParens: 'foo${recognized(${unrecognized:}, ${unrecognized2:})}', + unrecognizedInAddress: 'foo${recognized:${unrecognized:}}', + unrecognizedInParensAndAddress: + 'foo${recognized(${unrecognized:}, ${unrecognized2:}):${unrecognized3:}}', + unrecognizedFallback: 'foo${recognized:, unrecognized:}', + otherUnrecognizedFallback: 'foo${unrecognized:, unrecognized4:}', }, - static: true, - address: 'foo${sourceAddress:address-result}', - varAddress: 'foo${sourceAddress:${sourceDirect:}}', + }; - nonStringStringPart: 'elo${sourceMissing:, null}', - notExistingProperty: "${sourceProperty(not, existing), 'notExistingFallback'}", - nestUnrecognized: { - unrecognized: - '${sourceDirect:}|${sourceUnrecognized:}|${sourceDirect(${sourceUnrecognized:})}' + - '${sourceDirect:${sourceUnrecognized:}}', + const sources = { + recognized: { + resolve: () => ({ value: 234 }), }, }; let resultMap; before(async () => { - resultMap = resolveUnresolvedSourceTypes(resolveMeta(configuration)); + const variablesMeta = resolveMeta(configuration); + await resolve({ + serviceDir: process.cwd(), + configuration, + variablesMeta, + sources, + options: {}, + fulfilledSources: new Set('recognized'), + }); + + resultMap = resolveUnresolvedSourceTypes(variablesMeta); }); it('should resolve all not resolved sources', () => { expect(resultMap).to.deep.equal( new Map([ - ['sourceParam', new Set(['foo\0params', 'foo\0varParam'])], [ - 'sourceDirect', - new Set(['foo\0varParam', 'varAddress', 'nestUnrecognized\0unrecognized']), + 'unrecognized', + new Set([ + 'unrecognized', + 'unrecognizedInParens', + 'unrecognizedInAddress', + 'unrecognizedInParensAndAddress', + 'otherUnrecognizedFallback', + 'deep\0unrecognized', + 'deep\0unrecognizedInParens', + 'deep\0unrecognizedInAddress', + 'deep\0unrecognizedInParensAndAddress', + 'deep\0otherUnrecognizedFallback', + ]), + ], + [ + 'unrecognized2', + new Set([ + 'unrecognizedInParens', + 'unrecognizedInParensAndAddress', + 'deep\0unrecognizedInParens', + 'deep\0unrecognizedInParensAndAddress', + ]), ], - ['sourceAddress', new Set(['address', 'varAddress'])], - ['sourceMissing', new Set(['nonStringStringPart'])], - ['sourceProperty', new Set(['notExistingProperty'])], - ['sourceUnrecognized', new Set(['nestUnrecognized\0unrecognized'])], ]) ); }); From 9e3399e2bc4a2f8fd5c97bd4f5fd02b564ce1cb0 Mon Sep 17 00:00:00 2001 From: Mariusz Nowak Date: Fri, 23 Apr 2021 10:26:42 +0200 Subject: [PATCH 0003/1101] chore: Bump dependencies --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 609cb1b3b46..2a251dc8559 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "ajv": "^6.12.6", "ajv-keywords": "^3.5.2", "archiver": "^5.3.0", - "aws-sdk": "^2.890.0", + "aws-sdk": "^2.891.0", "bluebird": "^3.7.2", "boxen": "^5.0.1", "cachedir": "^2.3.0", From 1626761b31655b459b05759fca17c279c61b41a2 Mon Sep 17 00:00:00 2001 From: Mariusz Nowak Date: Fri, 23 Apr 2021 10:30:33 +0200 Subject: [PATCH 0004/1101] chore: Release v2.38.0 --- CHANGELOG.md | 11 +++++++++++ package.json | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f02d391e7e..8dee4facf5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [2.38.0](https://github.com/serverless/serverless/compare/v2.37.2...v2.38.0) (2021-04-23) + +### Features + +- **AWS CloudFormation:** Add default export names to outputs ([#9313](https://github.com/serverless/serverless/issues/9313)) ([7e139bb](https://github.com/serverless/serverless/commit/7e139bb0136e0d053f4f6f8cb2876480bb2a485e)) ([Joseph Cha](https://github.com/js-cha)) + +### Bug Fixes + +- **AWS API Gateway:** Create one request validator and reuse ([#9319](https://github.com/serverless/serverless/issues/9319)) ([154351f](https://github.com/serverless/serverless/commit/154351f1a5925a745873895014ed31f03b2842b3)) ([Jacques](https://github.com/gambit66)) +- **Variables:** Fix unresolved sources notifications ([#9356](https://github.com/serverless/serverless/issues/9356)) ([53a7872](https://github.com/serverless/serverless/commit/53a7872f78f51938b409925e052d34f2dc85abbd)) ([Mariusz Nowak](https://github.com/medikoo)) + ### [2.37.2](https://github.com/serverless/serverless/compare/v2.37.1...v2.37.2) (2021-04-22) ### Bug Fixes diff --git a/package.json b/package.json index 2a251dc8559..a1aed905698 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "serverless", - "version": "2.37.2", + "version": "2.38.0", "description": "Serverless Framework - Build web, mobile and IoT applications with serverless architectures using AWS Lambda, Azure Functions, Google CloudFunctions & more", "preferGlobal": true, "homepage": "https://serverless.com/framework/docs/", From 28c5af47f4ae4cf0957390712f72353bf7af3c68 Mon Sep 17 00:00:00 2001 From: Piotr Grzesik Date: Thu, 22 Apr 2021 14:48:03 +0200 Subject: [PATCH 0005/1101] refactor: Add `code` to `ServerlessError` in `./Serverless.js` --- lib/Serverless.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/Serverless.js b/lib/Serverless.js index 0864eededb9..defd63c6dfc 100644 --- a/lib/Serverless.js +++ b/lib/Serverless.js @@ -37,20 +37,24 @@ class Serverless { ensureString(configObject.serviceDir, { name: 'config.serviceDir', Error: ServerlessError, + errorCode: 'INVALID_NON_STRING_SERVICE_DIR', }) ); this.configurationFilename = ensureString(configObject.configurationFilename, { name: 'config.configurationFilename', Error: ServerlessError, + errorCode: 'INVALID_NON_STRING_CONFIGURATION_FILENAME', }); if (path.isAbsolute(this.configurationFilename)) { throw new ServerlessError( - `"config.configurationFilename" cannot be absolute path. Received: ${configObject.configurationFilename}` + `"config.configurationFilename" cannot be absolute path. Received: ${configObject.configurationFilename}`, + 'INVALID_ABSOLUTE_PATH_CONFIGURATION_FILENAME' ); } this.configurationInput = ensurePlainObject(configObject.configuration, { name: 'config.configuration', Error: ServerlessError, + errorCode: 'INVALID_NON_OBJECT_CONFIGURATION', }); this.isConfigurationInputResolved = Boolean(configObject.isConfigurationResolved); } else if (configObject.configurationPath != null) { @@ -59,6 +63,7 @@ class Serverless { ensureString(configObject.configurationPath, { name: 'config.configurationPath', Error: ServerlessError, + errorCode: 'INVALID_NON_STRING_CONFIGURATION_PATH', }) ); this.serviceDir = process.cwd(); @@ -67,6 +72,7 @@ class Serverless { isOptional: true, name: 'config.configuration', Error: ServerlessError, + errorCode: 'INVALID_NON_OBJECT_CONFIGURATION', }); if (this.configurationInput) { this.isConfigurationInputResolved = Boolean(configObject.isConfigurationResolved); From 395bdc8b23b0fd5a94fee3584df89f30232e6d74 Mon Sep 17 00:00:00 2001 From: Piotr Grzesik Date: Thu, 22 Apr 2021 15:24:10 +0200 Subject: [PATCH 0006/1101] refactor: Add `code` to `ServerlessError` in `lib/configuration` --- lib/configuration/read.js | 2 +- lib/configuration/resolve-provider-name.js | 1 + lib/configuration/variables/sources/env.js | 1 + lib/configuration/variables/sources/file.js | 2 ++ .../sources/instance-dependent/get-cf.js | 1 + .../sources/instance-dependent/get-s3.js | 1 + .../sources/instance-dependent/get-sls.js | 1 + .../sources/instance-dependent/get-ssm.js | 1 + lib/configuration/variables/sources/opt.js | 1 + .../variables/sources/str-to-bool.js | 1 + .../configuration/resolve-provider-name.test.js | 17 +++++++++++------ 11 files changed, 22 insertions(+), 7 deletions(-) diff --git a/lib/configuration/read.js b/lib/configuration/read.js index aaaaa632226..89fe28dc6be 100644 --- a/lib/configuration/read.js +++ b/lib/configuration/read.js @@ -37,7 +37,7 @@ const resolveTsNode = async (serviceDir) => { return require.resolve(`${String(stdoutBuffer).trim()}/ts-node`); } catch (globalDepError) { if (globalDepError.code !== 'MODULE_NOT_FOUND') throw globalDepError; - throw new ServerlessError('"ts-node" not found'); + throw new ServerlessError('"ts-node" not found', 'TS_NODE_NOT_FOUND'); } } } diff --git a/lib/configuration/resolve-provider-name.js b/lib/configuration/resolve-provider-name.js index d0ec19dd4c2..0a5d1022c73 100644 --- a/lib/configuration/resolve-provider-name.js +++ b/lib/configuration/resolve-provider-name.js @@ -12,6 +12,7 @@ module.exports = (configuration) => { { Error: ServerlessError, errorMessage: 'Invalid service configuration: "provider.name" property is missing', + errorCode: 'INVALID_CONFIGURATION_PROVIDER_NAME_MISSING', } ); } catch (error) { diff --git a/lib/configuration/variables/sources/env.js b/lib/configuration/variables/sources/env.js index 9e535c4103d..9f1f15549ea 100644 --- a/lib/configuration/variables/sources/env.js +++ b/lib/configuration/variables/sources/env.js @@ -14,6 +14,7 @@ module.exports = { address = ensureString(address, { Error: ServerlessError, errorMessage: 'Non-string address argument in variable "env" source: %v', + errorCode: 'INVALID_ENV_SOURCE_ADDRESS_ARGUMENT', }); return { value: process.env[address] || null, isPending: !isSourceFulfilled }; diff --git a/lib/configuration/variables/sources/file.js b/lib/configuration/variables/sources/file.js index 9e2b217fe71..4e6b697a9ce 100644 --- a/lib/configuration/variables/sources/file.js +++ b/lib/configuration/variables/sources/file.js @@ -33,6 +33,7 @@ module.exports = { ensureString(params[0], { Error: ServerlessError, errorMessage: 'Non-string path argument in variable "file" source: %v', + errorCode: 'INVALID_FILE_SOURCE_PATH_ARGUMENT', }) ); if (!filePath.startsWith(`${serviceDir}${path.sep}`)) { @@ -45,6 +46,7 @@ module.exports = { address = ensureString(address, { Error: ServerlessError, errorMessage: 'Non-string address argument for variable "file" source: %v', + errorCode: 'INVALID_FILE_SOURCE_ADDRESS_ARGUMENT', }); } diff --git a/lib/configuration/variables/sources/instance-dependent/get-cf.js b/lib/configuration/variables/sources/instance-dependent/get-cf.js index 2507012b845..382518b4a8b 100644 --- a/lib/configuration/variables/sources/instance-dependent/get-cf.js +++ b/lib/configuration/variables/sources/instance-dependent/get-cf.js @@ -17,6 +17,7 @@ module.exports = (serverlessInstance) => { address = ensureString(address, { Error: ServerlessError, errorMessage: 'Non-string address argument in variable "cf" source: %v', + errorCode: 'INVALID_CF_SOURCE_ADDRESS_ARGUMENT', }); const separatorIndex = address.indexOf('.'); if (separatorIndex === -1) { diff --git a/lib/configuration/variables/sources/instance-dependent/get-s3.js b/lib/configuration/variables/sources/instance-dependent/get-s3.js index 8da1a033800..e48facee732 100644 --- a/lib/configuration/variables/sources/instance-dependent/get-s3.js +++ b/lib/configuration/variables/sources/instance-dependent/get-s3.js @@ -16,6 +16,7 @@ module.exports = (serverlessInstance) => { address = ensureString(address, { Error: ServerlessError, errorMessage: 'Non-string address argument in variable "s3" source: %v', + errorCode: 'INVALID_S3_SOURCE_ADDRESS_ARGUMENT', }); const separatorIndex = address.indexOf('/'); if (separatorIndex === -1) { diff --git a/lib/configuration/variables/sources/instance-dependent/get-sls.js b/lib/configuration/variables/sources/instance-dependent/get-sls.js index 127ad6f4d5e..74fdceaea26 100644 --- a/lib/configuration/variables/sources/instance-dependent/get-sls.js +++ b/lib/configuration/variables/sources/instance-dependent/get-sls.js @@ -15,6 +15,7 @@ module.exports = (serverlessInstance) => { address = ensureString(address, { Error: ServerlessError, errorMessage: 'Non-string address argument in variable "sls" source: %v', + errorCode: 'INVALID_SLS_SOURCE_ADDRESS_ARGUMENT', }); switch (address) { diff --git a/lib/configuration/variables/sources/instance-dependent/get-ssm.js b/lib/configuration/variables/sources/instance-dependent/get-ssm.js index 36f7b424e3b..bf2623542d6 100644 --- a/lib/configuration/variables/sources/instance-dependent/get-ssm.js +++ b/lib/configuration/variables/sources/instance-dependent/get-ssm.js @@ -19,6 +19,7 @@ module.exports = (serverlessInstance) => { address = ensureString(address, { Error: ServerlessError, errorMessage: 'Non-string address argument in variable "ssm" source: %v', + errorCode: 'INVALID_SSM_SOURCE_ADDRESS_ARGUMENT', }); const region = !params || !params[0] || params[0] === 'raw' ? undefined : params[0]; const shouldReturnRawValue = params && (params[0] === 'raw' || params[1] === 'raw'); diff --git a/lib/configuration/variables/sources/opt.js b/lib/configuration/variables/sources/opt.js index c6dd838f4d6..e543444f8e9 100644 --- a/lib/configuration/variables/sources/opt.js +++ b/lib/configuration/variables/sources/opt.js @@ -9,6 +9,7 @@ module.exports = { isOptional: true, Error: ServerlessError, errorMessage: 'Non-string address argument in variable "opt" source: %v', + errorCode: 'INVALID_OPT_SOURCE_ADDRESS_ARGUMENT', }); if (!isSourceFulfilled) { if (address == null) return { value: null, isPending: true }; diff --git a/lib/configuration/variables/sources/str-to-bool.js b/lib/configuration/variables/sources/str-to-bool.js index b9ab92b6c81..9935ad785fb 100644 --- a/lib/configuration/variables/sources/str-to-bool.js +++ b/lib/configuration/variables/sources/str-to-bool.js @@ -15,6 +15,7 @@ module.exports = { const stringValue = ensureString(params[0], { Error: ServerlessError, errorMessage: 'Non-string "strToBool" input:. Received: %v', + errorCode: 'INVALID_STR_TO_BOOL_SOURCE_VALUE', }).trim(); if (trueStrings.has(stringValue)) return { value: true }; diff --git a/test/unit/lib/configuration/resolve-provider-name.test.js b/test/unit/lib/configuration/resolve-provider-name.test.js index ee24c19e8a2..10288b07310 100644 --- a/test/unit/lib/configuration/resolve-provider-name.test.js +++ b/test/unit/lib/configuration/resolve-provider-name.test.js @@ -3,8 +3,9 @@ const { expect } = require('chai'); const resolveProviderName = require('../../../../lib/configuration/resolve-provider-name'); +const ServerlessError = require('../../../../lib/serverless-error'); -describe('test/unit/lib/configuration/resolve-provider.name.test.js', () => { +describe('test/unit/lib/configuration/resolve-provider-name.test.js', () => { it('should read name from "provider"', () => { expect(resolveProviderName({ provider: 'foo' })).to.equal('foo'); }); @@ -12,14 +13,18 @@ describe('test/unit/lib/configuration/resolve-provider.name.test.js', () => { expect(resolveProviderName({ provider: { name: 'foo' } })).to.equal('foo'); }); it('should reject missing "provider.name"', () => { - expect(() => resolveProviderName({ provider: {} })).to.throw('Invalid service configuration'); + expect(() => resolveProviderName({ provider: {} })) + .to.throw(ServerlessError) + .with.property('code', 'INVALID_CONFIGURATION_PROVIDER_NAME_MISSING'); }); it('should reject invalid "provider.name"', () => { - expect(() => resolveProviderName({ provider: { name: {} } })).to.throw( - 'Invalid service configuration' - ); + expect(() => resolveProviderName({ provider: { name: {} } })) + .to.throw(ServerlessError) + .with.property('code', 'INVALID_CONFIGURATION_PROVIDER_NAME_MISSING'); }); it('should reject missing "provider"', () => { - expect(() => resolveProviderName({})).to.throw('Invalid service configuration'); + expect(() => resolveProviderName({})) + .to.throw(ServerlessError) + .with.property('code', 'INVALID_CONFIGURATION_PROVIDER_NAME_MISSING'); }); }); From 82040b427e4c292bf9287a668fa46856ba5204e2 Mon Sep 17 00:00:00 2001 From: Piotr Grzesik Date: Thu, 22 Apr 2021 15:39:37 +0200 Subject: [PATCH 0007/1101] refactor: Add `code` to `ServerlessError` in `lib/classes` --- lib/classes/ConfigSchemaHandler/index.js | 2 +- lib/classes/PluginManager.js | 32 ++++++++++++++++-------- lib/classes/Service.js | 15 ++++++++--- lib/classes/Variables.js | 23 +++++++++++------ 4 files changed, 50 insertions(+), 22 deletions(-) diff --git a/lib/classes/ConfigSchemaHandler/index.js b/lib/classes/ConfigSchemaHandler/index.js index 7faa3da1ec6..abf7ba36931 100644 --- a/lib/classes/ConfigSchemaHandler/index.js +++ b/lib/classes/ConfigSchemaHandler/index.js @@ -122,7 +122,7 @@ class ConfigSchemaHandler { ? `${ERROR_PREFIX}: \n ${messages.join('\n ')}` : `${ERROR_PREFIX} ${messages[0]}`; - throw new ServerlessError(errorMessage); + throw new ServerlessError(errorMessage, 'SCHEMA_VALIDATION'); } else { if (messages.length === 1) { this.serverless.cli.log(`${WARNING_PREFIX} ${messages[0]}`, 'Serverless', { diff --git a/lib/classes/PluginManager.js b/lib/classes/PluginManager.js index 2197891aac5..efdf0ee5dc9 100644 --- a/lib/classes/PluginManager.js +++ b/lib/classes/PluginManager.js @@ -188,12 +188,14 @@ class PluginManager { `Serverless plugin "${name}" not found.`, ' Make sure it\'s installed and listed in the "plugins" section', ' of your serverless config file.', - ].join('') + ].join(''), + 'PLUGIN_NOT_FOUND' ); } if (!Plugin) { throw new ServerlessError( - `Serverless plugin "${name}", didn't export Plugin constructor.` + `Serverless plugin "${name}", didn't export Plugin constructor.`, + 'MISSING_PLUGIN_NAME' ); } return Plugin; @@ -231,7 +233,10 @@ class PluginManager { createCommandAlias(alias, command) { // Deny self overrides if (command.startsWith(alias)) { - throw new ServerlessError(`Command "${alias}" cannot be overriden by an alias`); + throw new ServerlessError( + `Command "${alias}" cannot be overriden by an alias`, + 'INVALID_COMMAND_ALIAS' + ); } const splitAlias = alias.split(':'); @@ -245,7 +250,8 @@ class PluginManager { // Check if the alias is already defined if (aliasTarget.command) { throw new ServerlessError( - `Alias "${alias}" is already defined for command ${aliasTarget.command}` + `Alias "${alias}" is already defined for command ${aliasTarget.command}`, + 'COMMAND_ALIAS_ALREADY_DEFINED' ); } // Check if the alias would overwrite an exiting command @@ -257,7 +263,10 @@ class PluginManager { return __.commands[aliasPath]; }, this) ) { - throw new ServerlessError(`Command "${alias}" cannot be overriden by an alias`); + throw new ServerlessError( + `Command "${alias}" cannot be overriden by an alias`, + 'INVALID_COMMAND_ALIAS' + ); } aliasTarget.command = command; } @@ -270,7 +279,10 @@ class PluginManager { // Check if there is already an alias for the same path as the command const aliasCommand = this.getAliasCommandTarget(key.split(':')); if (aliasCommand) { - throw new ServerlessError(`Command "${key}" cannot override an existing alias`); + throw new ServerlessError( + `Command "${key}" cannot override an existing alias`, + 'INVALID_COMMAND_OVERRIDE_EXISTING_ALIAS' + ); } // Load the command const commands = _.mapValues(details.commands, (subDetails, subKey) => @@ -488,7 +500,7 @@ class PluginManager { } } errorMessage.push('" to see a more helpful error message for this command.'); - throw new ServerlessError(errorMessage.join('')); + throw new ServerlessError(errorMessage.join(''), 'SUBCOMMAND_NOT_FOUND'); } // top level command isn't valid. give a suggestion @@ -502,7 +514,7 @@ class PluginManager { `Serverless command "${commandName}" not found. Did you mean "${suggestedCommand}"?`, ' Run "serverless help" for a list of all available commands.', ].join(''); - throw new ServerlessError(errorMessage); + throw new ServerlessError(errorMessage, 'COMMAND_NOT_FOUND'); }, { commands: this.commands } ); @@ -622,7 +634,7 @@ class PluginManager { 'This command can only be run in a Serverless service directory. ', "Make sure to reference a valid config file in the current working directory if you're using a custom config file", ].join(''); - throw new ServerlessError(msg); + throw new ServerlessError(msg, 'INVALID_COMMAND_MISSING_SERVICE_DIRECTORY'); } } } @@ -652,7 +664,7 @@ class PluginManager { typeof value.customValidation.errorMessage === 'string' && !value.customValidation.regularExpression.test(this.cliOptions[key]) ) { - throw new ServerlessError(value.customValidation.errorMessage); + throw new ServerlessError(value.customValidation.errorMessage, 'INVALID_CLI_OPTION'); } }); } diff --git a/lib/classes/Service.js b/lib/classes/Service.js index 51c23ee9793..bb7fddfc340 100644 --- a/lib/classes/Service.js +++ b/lib/classes/Service.js @@ -256,14 +256,20 @@ class Service { if (functionName in this.functions) { return this.functions[functionName]; } - throw new ServerlessError(`Function "${functionName}" doesn't exist in this Service`); + throw new ServerlessError( + `Function "${functionName}" doesn't exist in this Service`, + 'FUNCTION_MISSING_IN_SERVICE' + ); } getLayer(layerName) { if (layerName in this.layers) { return this.layers[layerName]; } - throw new ServerlessError(`Layer "${layerName}" doesn't exist in this Service`); + throw new ServerlessError( + `Layer "${layerName}" doesn't exist in this Service`, + 'LAYER_MISSING_IN_SERVICE' + ); } getEventInFunction(eventName, functionName) { @@ -273,7 +279,10 @@ class Service { if (event) { return event; } - throw new ServerlessError(`Event "${eventName}" doesn't exist in function "${functionName}"`); + throw new ServerlessError( + `Event "${eventName}" doesn't exist in function "${functionName}"`, + 'EVENT_MISSING_FOR_FUNCTION' + ); } getAllEventsInFunction(functionName) { diff --git a/lib/classes/Variables.js b/lib/classes/Variables.js index 6e276ad8175..9e353dcb1d5 100644 --- a/lib/classes/Variables.js +++ b/lib/classes/Variables.js @@ -451,7 +451,7 @@ class Variables { ` a string for variable ${matchedString}.`, ' Please make sure the value of the property is a string.', ].join(''); - throw new ServerlessError(errorMessage); + throw new ServerlessError(errorMessage, 'INVALID_NON_STRING_PROPERTY'); } return property; } @@ -565,7 +565,9 @@ class Variables { ' You can only reference env vars, options, & files.', ' You can check our docs for more info.', ].join(''); - ret = BbPromise.reject(new ServerlessError(errorMessage)); + ret = BbPromise.reject( + new ServerlessError(errorMessage, 'INVALID_VARIABLE_REFERENCE_SYNTAX') + ); } ret = this.tracker.add(variableString, ret, propertyString); } @@ -708,7 +710,9 @@ class Variables { ` file "${referencedFileRelativePath}".`, ' Check if your javascript is returning the correct data.', ].join(''); - return BbPromise.reject(new ServerlessError(errorMessage)); + return BbPromise.reject( + new ServerlessError(errorMessage, 'INVALID_FILE_VARIABLE_SYNTAX') + ); } return BbPromise.resolve(deepValueToPopulateResolved); } @@ -727,7 +731,9 @@ class Variables { ` file "${referencedFileRelativePath}" sub properties`, ' Please use ":" to reference sub properties.', ].join(''); - return BbPromise.reject(new ServerlessError(errorMessage)); + return BbPromise.reject( + new ServerlessError(errorMessage, 'INVALID_FILE_VARIABLE_SYNTAX') + ); } deepProperties = deepProperties.slice(1).split('.'); return this.getDeeperValue(deepProperties, valueToPopulate); @@ -767,7 +773,7 @@ class Variables { ` Stack name: "${stackName}"`, ` Requested variable: "${outputLogicalId}".`, ].join(''); - return BbPromise.reject(new ServerlessError(errorMessage)); + return BbPromise.reject(new ServerlessError(errorMessage, 'INVALID_CF_VARIABLE')); } return BbPromise.resolve(output.OutputValue); }); @@ -791,7 +797,7 @@ class Variables { .then((response) => BbPromise.resolve(response.Body.toString())) .catch((err) => { const errorMessage = `Error getting value for ${variableString}. ${err.message}`; - return BbPromise.reject(new ServerlessError(errorMessage)); + return BbPromise.reject(new ServerlessError(errorMessage, 'INVALID_S3_VARIABLE')); }); } @@ -836,7 +842,7 @@ class Variables { }, (err) => { if (!err.providerError || err.providerError.statusCode !== 400) { - throw new ServerlessError(err.message); + throw new ServerlessError(err.message, 'INVALID_SSM_VARIABLE'); } } ); @@ -855,7 +861,8 @@ class Variables { return true; } throw new ServerlessError( - 'Unexpected strToBool input; expected either "true", "false", "0", or "1".' + 'Unexpected strToBool input; expected either "true", "false", "0", or "1".', + 'INVALID_STR_TO_BOOL_SOURCE_VALUE' ); } // Cast non-string inputs From 8748a783ae4e0ca25507c45e1a5619fc7c1520c8 Mon Sep 17 00:00:00 2001 From: Piotr Grzesik Date: Thu, 22 Apr 2021 16:02:00 +0200 Subject: [PATCH 0008/1101] refactor: Add `code` to `ServerlessError` in `lib/aws` --- lib/aws/request.js | 2 +- test/unit/lib/aws/request.test.js | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/aws/request.js b/lib/aws/request.js index 211e7ed5d83..b45d1fb7f66 100644 --- a/lib/aws/request.js +++ b/lib/aws/request.js @@ -194,7 +194,7 @@ async function awsRequest(service, method, ...args) { : bottomError.message; message = errorMessage; // We do not want to trigger the retry mechanism for credential errors - throw Object.assign(new ServerlessError(errorMessage), { + throw Object.assign(new ServerlessError(errorMessage, 'AWS_CREDENTIALS_NOT_FOUND'), { providerError: Object.assign({}, err, { retryable: false }), }); } diff --git a/test/unit/lib/aws/request.test.js b/test/unit/lib/aws/request.test.js index 737904163d2..641f94c03b4 100644 --- a/test/unit/lib/aws/request.test.js +++ b/test/unit/lib/aws/request.test.js @@ -53,9 +53,7 @@ describe('#request', () => { Key: 'test-key', } ) - ).to.be.rejectedWith( - 'AWS provider credentials not found. Learn how to set up AWS provider credentials in our docs here: <\u001b[32mhttp://slss.io/aws-creds-setup\u001b[39m>.' - ); + ).to.be.eventually.rejected.and.have.property('code', 'AWS_CREDENTIALS_NOT_FOUND'); }); it('should support passing params without credentials', async () => { From 822a7cf9f527514b53fd8cfc5c172ec5dc53f4ce Mon Sep 17 00:00:00 2001 From: Piotr Grzesik Date: Thu, 22 Apr 2021 16:19:35 +0200 Subject: [PATCH 0009/1101] refactor: Add `code` to `ServerlessError` in `lib/utils` --- lib/utils/downloadTemplateFromRepo.js | 10 +++---- lib/utils/renameService.js | 2 +- .../utils/downloadTemplateFromRepo.test.js | 28 +++++++++++++------ test/unit/lib/utils/renameService.test.js | 4 ++- 4 files changed, 28 insertions(+), 16 deletions(-) diff --git a/lib/utils/downloadTemplateFromRepo.js b/lib/utils/downloadTemplateFromRepo.js index e82fb55eb74..63816ad8d1c 100644 --- a/lib/utils/downloadTemplateFromRepo.js +++ b/lib/utils/downloadTemplateFromRepo.js @@ -40,7 +40,7 @@ function validateUrl({ url, hostname, service, owner, repo }) { // 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`; - throw new ServerlessError(errorMessage); + throw new ServerlessError(errorMessage, 'INVALID_TEMPLATE_URL'); } } @@ -211,14 +211,14 @@ function parsePlainGitURL(url) { function parseRepoURL(inputUrl) { return new BbPromise((resolve, reject) => { if (!inputUrl) { - return reject(new ServerlessError('URL is required')); + return reject(new ServerlessError('URL is required', 'MISSING_TEMPLATE_URL')); } const url = URL.parse(inputUrl.replace(/\/$/, '')); // check if url parameter is a valid url if (!url.host && !url.href.startsWith('git@')) { - return reject(new ServerlessError('The URL you passed is not valid')); + return reject(new ServerlessError('The URL you passed is not valid', 'INVALID_TEMPLATE_URL')); } if (isPlainGitURL(url.href)) { @@ -233,7 +233,7 @@ function parseRepoURL(inputUrl) { const msg = 'The URL you passed is not one of the valid providers: "GitHub", "GitHub Entreprise", "Bitbucket", "Bitbucket Server" or "GitLab".'; - const err = new ServerlessError(msg); + const err = new ServerlessError(msg, 'INVALID_TEMPLATE_PROVIDER'); // test if it's a private bitbucket server return retrieveBitbucketServerInfo(url) .then((isBitbucket) => { @@ -277,7 +277,7 @@ function downloadTemplateFromRepo(inputUrl, templateName, downloadPath) { if (dirExistsSync(path.join(process.cwd(), dirName))) { const errorMessage = `A folder named "${dirName}" already exists.`; - throw new ServerlessError(errorMessage); + throw new ServerlessError(errorMessage, 'TARGET_FOLDER_ALREADY_EXISTS'); } log(`Downloading and installing "${serviceName}"...`); diff --git a/lib/utils/renameService.js b/lib/utils/renameService.js index 0a97f3074b5..a48ed24dce4 100644 --- a/lib/utils/renameService.js +++ b/lib/utils/renameService.js @@ -57,7 +57,7 @@ function renameService(name, serviceDir) { } const errorMessage = ['serverless.yml or serverlss.ts not found in', ` ${serviceDir}`].join(''); - throw new ServerlessError(errorMessage); + throw new ServerlessError(errorMessage, 'MISSING_SERVICE_FILE'); } module.exports.renameService = renameService; diff --git a/test/unit/lib/utils/downloadTemplateFromRepo.test.js b/test/unit/lib/utils/downloadTemplateFromRepo.test.js index fd67ee46765..80bd4b88733 100644 --- a/test/unit/lib/utils/downloadTemplateFromRepo.test.js +++ b/test/unit/lib/utils/downloadTemplateFromRepo.test.js @@ -73,13 +73,15 @@ describe('downloadTemplateFromRepo', () => { describe('downloadTemplateFromRepo', () => { it('should reject an error if the passed URL option is not a valid URL', () => { - return expect(downloadTemplateFromRepo('invalidUrl')).to.be.rejectedWith(Error); + return expect( + downloadTemplateFromRepo('invalidUrl') + ).to.be.eventually.rejected.and.have.property('code', 'INVALID_TEMPLATE_URL'); }); it('should reject an error if the passed URL is not a valid GitHub URL', () => { return expect( downloadTemplateFromRepo('http://no-git-hub-url.com/foo/bar') - ).to.be.rejectedWith(Error); + ).to.be.eventually.rejected.and.have.property('code', 'INVALID_TEMPLATE_PROVIDER'); }); it('should reject an error if a directory with the same service name is already present', () => { @@ -88,7 +90,7 @@ describe('downloadTemplateFromRepo', () => { return expect( downloadTemplateFromRepo('https://github.com/johndoe/existing-service') - ).to.be.rejectedWith(Error); + ).to.be.eventually.rejected.and.have.property('code', 'TARGET_FOLDER_ALREADY_EXISTS'); }); it('should download the service based on a regular .git URL', () => { @@ -223,23 +225,31 @@ describe('downloadTemplateFromRepo', () => { const serviceDirName = path.join(serviceDir, 'rest-api-with-dynamodb'); fse.mkdirsSync(serviceDirName); - return expect(downloadTemplateFromRepo(null, url)).to.be.rejectedWith(Error); + return expect( + downloadTemplateFromRepo(null, url) + ).to.be.eventually.rejected.and.have.property('code', 'MISSING_TEMPLATE_URL'); }); }); describe('parseRepoURL', () => { it('should reject an error if no URL is provided', () => { - return expect(parseRepoURL()).to.be.rejectedWith(Error); + return expect(parseRepoURL()).to.be.eventually.rejected.and.have.property( + 'code', + 'MISSING_TEMPLATE_URL' + ); }); it('should reject an error if URL is not valid', () => { - return expect(parseRepoURL('non_valid_url')).to.be.rejectedWith(Error); + return expect(parseRepoURL('non_valid_url')).to.be.eventually.rejected.and.have.property( + 'code', + 'INVALID_TEMPLATE_URL' + ); }); it('should throw an error if URL is not of valid provider', () => { - return expect(parseRepoURL('https://kostasbariotis.com/repo/owner')).to.be.rejectedWith( - Error - ); + return expect( + parseRepoURL('https://kostasbariotis.com/repo/owner') + ).to.be.eventually.rejected.and.have.property('code', 'INVALID_TEMPLATE_PROVIDER'); }); it('should parse a valid GitHub URL', () => { diff --git a/test/unit/lib/utils/renameService.test.js b/test/unit/lib/utils/renameService.test.js index 45cda8431da..b5c758539fe 100644 --- a/test/unit/lib/utils/renameService.test.js +++ b/test/unit/lib/utils/renameService.test.js @@ -201,6 +201,8 @@ describe('renameService', () => { }); it('should fail to set new service name in serverless.yml', () => { - expect(() => renameService('new-service-name', serviceDir)).to.throw(Error); + expect(() => renameService('new-service-name', serviceDir)) + .to.throw() + .and.have.property('code', 'MISSING_SERVICE_FILE'); }); }); From fbf0ed30e933755ae86c3d31050cdb0d3b952b82 Mon Sep 17 00:00:00 2001 From: Piotr Grzesik Date: Fri, 23 Apr 2021 09:59:57 +0200 Subject: [PATCH 0010/1101] refactor: Add `code` to `ServerlessError` in `lib/plugins/aws` --- lib/plugins/aws/configCredentials.js | 10 ++++++++-- lib/plugins/aws/customResources/index.js | 5 ++++- lib/plugins/aws/deploy/lib/checkForChanges.js | 3 ++- .../aws/deploy/lib/existsDeploymentBucket.js | 8 ++++++-- lib/plugins/aws/deploy/lib/extendedValidate.js | 6 ++++-- lib/plugins/aws/deployFunction.js | 12 ++++++++---- lib/plugins/aws/info/getStackInfo.js | 3 ++- lib/plugins/aws/invokeLocal/index.js | 11 ++++++++--- lib/plugins/aws/lib/monitorStack.js | 4 ++-- lib/plugins/aws/lib/validate.js | 5 ++++- lib/plugins/aws/logs.js | 2 +- .../package/compile/events/alb/lib/validate.js | 12 ++++++++---- .../events/apiGateway/lib/hack/updateStage.js | 2 +- .../compile/events/apiGateway/lib/restApi.js | 5 ++++- .../events/apiGateway/lib/usagePlanKeys.js | 5 ++++- .../compile/events/apiGateway/lib/validate.js | 17 +++++++++++------ .../aws/package/compile/events/cloudFront.js | 17 ++++++++++++----- .../package/compile/events/cloudWatchEvent.js | 3 ++- .../aws/package/compile/events/cloudWatchLog.js | 2 +- .../package/compile/events/cognitoUserPool.js | 2 +- .../package/compile/events/eventBridge/index.js | 3 ++- .../aws/package/compile/events/s3/index.js | 4 ++-- lib/plugins/aws/package/compile/events/sns.js | 3 ++- .../compile/events/websockets/lib/stage.js | 3 ++- .../compile/events/websockets/lib/validate.js | 3 ++- lib/plugins/aws/package/compile/functions.js | 11 ++++++++--- .../aws/package/lib/generateCoreTemplate.js | 2 +- lib/plugins/aws/utils/resolveCfImportValue.js | 3 ++- lib/plugins/aws/utils/resolveCfRefValue.js | 3 ++- .../unit/lib/plugins/aws/deployFunction.test.js | 2 +- 30 files changed, 117 insertions(+), 54 deletions(-) diff --git a/lib/plugins/aws/configCredentials.js b/lib/plugins/aws/configCredentials.js index 8996e291473..45489c3dbf1 100644 --- a/lib/plugins/aws/configCredentials.js +++ b/lib/plugins/aws/configCredentials.js @@ -24,7 +24,10 @@ class AwsConfigCredentials { }; if (!os.homedir()) { - throw new ServerlessError("Can't find home directory on your local file system."); + throw new ServerlessError( + "Can't find home directory on your local file system.", + 'MISSING_HOME_DIRECTORY' + ); } this.hooks = { @@ -42,7 +45,10 @@ class AwsConfigCredentials { // validate if (!this.options.key || !this.options.secret) { - throw new ServerlessError('Please include --key and --secret options for AWS.'); + throw new ServerlessError( + 'Please include --key and --secret options for AWS.', + 'MISSING_KEY_AND_SECRET_CLI_OPTIONS' + ); } this.serverless.cli.log('Setting up AWS...'); diff --git a/lib/plugins/aws/customResources/index.js b/lib/plugins/aws/customResources/index.js index b95f57811b3..ebdf1f1302c 100644 --- a/lib/plugins/aws/customResources/index.js +++ b/lib/plugins/aws/customResources/index.js @@ -50,7 +50,10 @@ async function addCustomResourceToService(awsProvider, resourceName, iamRoleStat Handler = 'apiGatewayCloudWatchRole/handler.handler'; customResourceFunctionLogicalId = awsProvider.naming.getCustomResourceApiGatewayAccountCloudWatchRoleHandlerFunctionLogicalId(); } else { - throw new ServerlessError(`No implementation found for Custom Resource "${resourceName}"`); + throw new ServerlessError( + `No implementation found for Custom Resource "${resourceName}"`, + 'MISSING_CUSTOM_RESOURCE_IMPLEMENTATION' + ); } absoluteFunctionName = `${funcPrefix}-${functionName}`; if (absoluteFunctionName.length > 64) { diff --git a/lib/plugins/aws/deploy/lib/checkForChanges.js b/lib/plugins/aws/deploy/lib/checkForChanges.js index 4fd4ad11cc8..3e651ca9556 100644 --- a/lib/plugins/aws/deploy/lib/checkForChanges.js +++ b/lib/plugins/aws/deploy/lib/checkForChanges.js @@ -59,7 +59,8 @@ module.exports = { `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(' ') + ].join(' '), + 'DEPLOYMENT_BUCKET_DOES_NOT_EXIST' ) ); }) diff --git a/lib/plugins/aws/deploy/lib/existsDeploymentBucket.js b/lib/plugins/aws/deploy/lib/existsDeploymentBucket.js index af3cd33bbe6..dbc71104c81 100644 --- a/lib/plugins/aws/deploy/lib/existsDeploymentBucket.js +++ b/lib/plugins/aws/deploy/lib/existsDeploymentBucket.js @@ -17,13 +17,17 @@ module.exports = { if (result.LocationConstraint === 'EU') result.LocationConstraint = 'eu-west-1'; if (result.LocationConstraint !== this.provider.getRegion()) { throw new ServerlessError( - 'Deployment bucket is not in the same region as the lambda function' + 'Deployment bucket is not in the same region as the lambda function', + 'DEPLOYMENT_BUCKET_INVALID_REGION' ); } return BbPromise.resolve(); }) .catch((err) => { - throw new ServerlessError(`Could not locate deployment bucket. Error: ${err.message}`); + throw new ServerlessError( + `Could not locate deployment bucket. Error: ${err.message}`, + 'DEPLOYMENT_BUCKET_NOT_FOUND' + ); }); }, }; diff --git a/lib/plugins/aws/deploy/lib/extendedValidate.js b/lib/plugins/aws/deploy/lib/extendedValidate.js index d4b2ffc8661..928e0585127 100644 --- a/lib/plugins/aws/deploy/lib/extendedValidate.js +++ b/lib/plugins/aws/deploy/lib/extendedValidate.js @@ -16,7 +16,8 @@ module.exports = { ); if (!this.serverless.utils.fileExistsSync(serviceStateFilePath)) { throw new ServerlessError( - `No ${serviceStateFileName} file found in the package path you provided.` + `No ${serviceStateFileName} file found in the package path you provided.`, + 'MISSING_SERVICE_STATE_FILE' ); } const state = this.serverless.utils.readFileSync(serviceStateFilePath); @@ -86,7 +87,8 @@ module.exports = { if (!this.serverless.utils.fileExistsSync(artifactFilePath)) { throw new ServerlessError( - `No ${artifactFileName} file found in the package path you provided.` + `No ${artifactFileName} file found in the package path you provided.`, + 'MISSING_ARTIFACT_FILE' ); } }); diff --git a/lib/plugins/aws/deployFunction.js b/lib/plugins/aws/deployFunction.js index 2a204d29f68..4757c332c43 100644 --- a/lib/plugins/aws/deployFunction.js +++ b/lib/plugins/aws/deployFunction.js @@ -78,7 +78,7 @@ class AwsDeployFunction { ' After that you can redeploy your services functions with the', ' "serverless deploy function" command.', ].join(''); - throw new ServerlessError(errorMessage); + throw new ServerlessError(errorMessage, 'FUNCTION_NOT_YET_DEPLOYED'); } throw error; } @@ -120,7 +120,10 @@ class AwsDeployFunction { } const roleProperties = roleResource.Properties; if (!roleProperties.RoleName) { - throw new ServerlessError('Role resource missing RoleName property'); + throw new ServerlessError( + 'Role resource missing RoleName property', + 'MISSING_ROLENAME_FOR_ROLE' + ); } const compiledFullRoleName = `${roleProperties.Path || '/'}${roleProperties.RoleName}`; @@ -146,7 +149,8 @@ class AwsDeployFunction { if (err.providerError && err.providerError.code === 'ResourceConflictException') { if (didOneMinutePass) { throw new ServerlessError( - 'Retry timed out. Please try to deploy your function once again.' + 'Retry timed out. Please try to deploy your function once again.', + 'DEPLOY_FUNCTION_CONFIGURATION_UPDATE_TIMED_OUT' ); } this.serverless.cli.log( @@ -258,7 +262,7 @@ class AwsDeployFunction { // taken from the bash man pages if (!key.match(/^[A-Za-z_][a-zA-Z0-9_]*$/)) { const errorMessage = 'Invalid characters in environment variable'; - throw new ServerlessError(errorMessage); + throw new ServerlessError(errorMessage, 'DEPLOY_FUNCTION_INVALID_ENV_VARIABLE'); } if (params.Environment.Variables[key] != null) { diff --git a/lib/plugins/aws/info/getStackInfo.js b/lib/plugins/aws/info/getStackInfo.js index 73706e45700..bd208a55661 100644 --- a/lib/plugins/aws/info/getStackInfo.js +++ b/lib/plugins/aws/info/getStackInfo.js @@ -47,7 +47,8 @@ module.exports = { }, (error) => { throw new ServerlessError( - `Could not resolve provider.httpApi.id parameter. ${error.message}` + `Could not resolve provider.httpApi.id parameter. ${error.message}`, + 'UNABLE_TO_RESOLVE_HTTP_API_ID' ); } ) diff --git a/lib/plugins/aws/invokeLocal/index.js b/lib/plugins/aws/invokeLocal/index.js index 84aabe565c6..11f38f1e454 100644 --- a/lib/plugins/aws/invokeLocal/index.js +++ b/lib/plugins/aws/invokeLocal/index.js @@ -67,7 +67,10 @@ class AwsInvokeLocal { const exists = await fileExists(absolutePath); if (!exists) { - throw new ServerlessError(`The file you provided does not exist: ${absolutePath}`); + throw new ServerlessError( + `The file you provided does not exist: ${absolutePath}`, + 'INVOKE_LOCAL_MISSING_FILE' + ); } if (absolutePath.endsWith('.js')) { // to support js - export as an input data @@ -86,7 +89,8 @@ class AwsInvokeLocal { if (this.options.functionObj.image) { throw new ServerlessError( - 'Local invocation of lambdas pointing AWS ECR images is not supported' + 'Local invocation of lambdas pointing AWS ECR images is not supported', + 'INVOKE_LOCAL_IMAGE_BACKED_FUNCTIONS_NOT_SUPPORTED' ); } if (this.options.contextPath) { @@ -198,7 +202,8 @@ class AwsInvokeLocal { } } catch (error) { throw new ServerlessError( - `Could not resolve "${name}" environment variable: ${error.message}` + `Could not resolve "${name}" environment variable: ${error.message}`, + 'INVOKE_LOCAL_INVALID_ENV_VARIABLE' ); } }) diff --git a/lib/plugins/aws/lib/monitorStack.js b/lib/plugins/aws/lib/monitorStack.js index 7794d393063..d044471ae27 100644 --- a/lib/plugins/aws/lib/monitorStack.js +++ b/lib/plugins/aws/lib/monitorStack.js @@ -91,7 +91,7 @@ module.exports = { let errorMessage = 'An error occurred: '; errorMessage += `${stackLatestError.LogicalResourceId} - `; errorMessage += `${stackLatestError.ResourceStatusReason}.`; - throw new ServerlessError(errorMessage); + throw new ServerlessError(errorMessage, 'STACK_MONITORING_FAILURE'); } }, (e) => { @@ -102,7 +102,7 @@ module.exports = { stackStatus = 'DELETE_COMPLETE'; return; } - throw new ServerlessError(e.message); + throw new ServerlessError(e.message, 'STACK_MONITORING_FAILURE'); } ) ) diff --git a/lib/plugins/aws/lib/validate.js b/lib/plugins/aws/lib/validate.js index 53f0b8d1c39..68dca8f3d19 100644 --- a/lib/plugins/aws/lib/validate.js +++ b/lib/plugins/aws/lib/validate.js @@ -5,7 +5,10 @@ const ServerlessError = require('../../../serverless-error'); module.exports = { validate() { if (!this.serverless.serviceDir) { - throw new ServerlessError('This command can only be run inside a service directory'); + throw new ServerlessError( + 'This command can only be run inside a service directory', + 'MISSING_SERVICE_DIRECTORY' + ); } this.options.stage = this.provider.getStage(); diff --git a/lib/plugins/aws/logs.js b/lib/plugins/aws/logs.js index 6820d264be1..2dfbea9a5a9 100644 --- a/lib/plugins/aws/logs.js +++ b/lib/plugins/aws/logs.js @@ -47,7 +47,7 @@ class AwsLogs { const reply = await this.provider.request('CloudWatchLogs', 'describeLogStreams', params); if (!reply || reply.logStreams.length === 0) { - throw new ServerlessError('No existing streams for the function'); + throw new ServerlessError('No existing streams for the function', 'NO_EXISTING_LOG_STREAMS'); } return reply.logStreams.map((logStream) => logStream.logStreamName); diff --git a/lib/plugins/aws/package/compile/events/alb/lib/validate.js b/lib/plugins/aws/package/compile/events/alb/lib/validate.js index de4aec4de63..81d01b21067 100644 --- a/lib/plugins/aws/package/compile/events/alb/lib/validate.js +++ b/lib/plugins/aws/package/compile/events/alb/lib/validate.js @@ -115,7 +115,10 @@ module.exports = { } const matches = listenerArn.match(this.ALB_LISTENER_REGEXP); if (!matches) { - throw new ServerlessError(`Invalid ALB listenerArn in function "${functionName}".`); + throw new ServerlessError( + `Invalid ALB listenerArn in function "${functionName}".`, + 'ALB_INVALID_LISTENER_ARN' + ); } return { albId: matches[1], listenerId: matches[2] }; }, @@ -138,7 +141,7 @@ module.exports = { ' Events in the same function cannot use the same priority even if they have a different listenerArn.\n', ' This is a Serverless limitation that will be fixed in the next major release.', ].join(''); - throw new ServerlessError(errorMessage); + throw new ServerlessError(errorMessage, 'ALB_PRIORITY_ALREADY_IN_USE'); } comparator = (e1, e2) => samePriority(e1, e2) && sameListener(e1, e2) && !sameFunction(e1, e2); @@ -148,7 +151,7 @@ module.exports = { `ALB event in function "${duplicates[0].functionName}"`, ` cannot use priority "${duplicates[0].priority}" because it is already in use.`, ].join(''); - throw new ServerlessError(errorMessage); + throw new ServerlessError(errorMessage, 'ALB_PRIORITY_ALREADY_IN_USE'); } }, @@ -159,7 +162,8 @@ module.exports = { for (const auth of eventAuthorizers) { if (!authorizers[auth]) { throw new ServerlessError( - `No match for "${auth}" in function "${functionName}" found in registered ALB authorizers` + `No match for "${auth}" in function "${functionName}" found in registered ALB authorizers`, + 'ALB_MISSING_AUTHORIZER' ); } } 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 ba06d897124..21483c3450b 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 @@ -81,7 +81,7 @@ module.exports = { "that there's a way to support your setup).", ].join(''); - throw new ServerlessError(errorMessage); + throw new ServerlessError(errorMessage, 'API_GATEWAY_REST_API_ID_NOT_RESOLVED'); } return resolveStage .call(this) 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 0247669b747..e42dd6aa152 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/restApi.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/restApi.js @@ -28,7 +28,10 @@ module.exports = { vpcEndpointIds = this.serverless.service.provider.vpcEndpointIds; if (endpointType !== 'PRIVATE') { - throw new ServerlessError('VPC endpoint IDs are only available for private APIs'); + throw new ServerlessError( + 'VPC endpoint IDs are only available for private APIs', + 'API_GATEWAY_INVALID_VPC_ENDPOINT_IDS_CONFIG' + ); } } } 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 d88c6ebd97a..d017514661d 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/usagePlanKeys.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/usagePlanKeys.js @@ -42,7 +42,10 @@ module.exports = { !usagePlansIncludeName && _.isObject(value) ) { - throw new ServerlessError(`API key "${name}" has no usage plan defined`); + throw new ServerlessError( + `API key "${name}" has no usage plan defined`, + 'API_GATEWAY_KEY_WITHOUT_USAGE_PLAN' + ); } if (_.isObject(apiKeyDefinition) && usagePlansIncludeName) { keyNumber = 0; 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 1e68f4c474a..6695ed7ac9e 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/validate.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/validate.js @@ -83,7 +83,7 @@ module.exports = { const errorMessage = [ `You need to set the request uri when using the ${http.integration} integration.`, ]; - throw new ServerlessError(errorMessage); + throw new ServerlessError(errorMessage, 'API_GATEWAY_MISSING_REQUEST_URI'); } http.connectionType = this.getConnectionType(http); @@ -92,7 +92,7 @@ module.exports = { const errorMessage = [ `You need to set connectionId when using ${http.connectionType} connectionType.`, ]; - throw new ServerlessError(errorMessage); + throw new ServerlessError(errorMessage, 'API_GATEWAY_MISSING_CONNECTION_ID'); } } @@ -158,7 +158,8 @@ module.exports = { `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(' ') + ].join(' '), + 'API_GATEWAY_INVALID_STAGE_NAME' ); } @@ -235,7 +236,8 @@ module.exports = { _.isObject(authorizer.arn) ) { throw new ServerlessError( - 'Please provide an authorizer name for authorizers of type COGNITO_USER_POOLS' + 'Please provide an authorizer name for authorizers of type COGNITO_USER_POOLS', + 'API_GATEWAY_MISSING_AUTHORIZER_NAME' ); } else { name = this.provider.naming.extractAuthorizerNameFromArn(arn); @@ -243,7 +245,10 @@ module.exports = { } else if (authorizer.name) { authorizerFunctionName = name = authorizer.name; } else { - throw new ServerlessError('Please provide either an authorizer name or ARN'); + throw new ServerlessError( + 'Please provide either an authorizer name or ARN', + 'API_GATEWAY_MISSING_AUTHORIZER_NAME_OR_ARN' + ); } if (!type) { @@ -289,7 +294,7 @@ module.exports = { const errorMessage = [ 'Cognito claims can only be filtered when using the lambda integration type', ]; - throw new ServerlessError(errorMessage); + throw new ServerlessError(errorMessage, 'API_GATEWAY_INVALID_COGNITO_CLAIMS'); } return { diff --git a/lib/plugins/aws/package/compile/events/cloudFront.js b/lib/plugins/aws/package/compile/events/cloudFront.js index ce5c44cab3d..bd51da252bc 100644 --- a/lib/plugins/aws/package/compile/events/cloudFront.js +++ b/lib/plugins/aws/package/compile/events/cloudFront.js @@ -416,7 +416,8 @@ class AwsCompileCloudFrontEvents { if (event.cloudFront.isDefaultOrigin) { if (defaultOrigin && defaultOrigin !== origin) { throw new ServerlessError( - 'Found more than one cloudfront event with "isDefaultOrigin" defined' + 'Found more than one cloudfront event with "isDefaultOrigin" defined', + 'CLOUDFRONT_MULTIPLE_DEFAULT_ORIGIN_EVENTS' ); } defaultOrigin = origin; @@ -534,13 +535,17 @@ class AwsCompileCloudFrontEvents { if (lambdaAtEdgeFunctions.length) { if (this.provider.getRegion() !== 'us-east-1') { throw new ServerlessError( - 'CloudFront associated functions have to be deployed to the us-east-1 region.' + 'CloudFront associated functions have to be deployed to the us-east-1 region.', + 'CLOUDFRONT_INVALID_REGION' ); } // Check if all behaviors got unique pathPatterns if (behaviors.length !== _.uniqBy(behaviors, 'PathPattern').length) { - throw new ServerlessError('Found more than one behavior with the same PathPattern'); + throw new ServerlessError( + 'Found more than one behavior with the same PathPattern', + 'CLOUDFRONT_MULTIPLE_BEHAVIORS_FOR_SINGLE_PATH_PATTERN' + ); } // Check if all event types in every behavior is unique @@ -553,7 +558,8 @@ class AwsCompileCloudFrontEvents { }) ) { throw new ServerlessError( - 'The event type of a function association must be unique in the cache behavior' + 'The event type of a function association must be unique in the cache behavior', + 'CLOUDFRONT_EVENT_TYPE_NON_UNIQUE_CACHE_BEHAVIOR' ); } @@ -563,7 +569,8 @@ class AwsCompileCloudFrontEvents { if (!origin) { if (origins.length > 1) { throw new ServerlessError( - 'Found more than one origin but none of the cloudfront event has "isDefaultOrigin" defined' + 'Found more than one origin but none of the cloudfront event has "isDefaultOrigin" defined', + 'CLOUDFRONT_MULTIPLE_DEFAULT_ORIGIN_EVENTS' ); } origin = origins[0]; diff --git a/lib/plugins/aws/package/compile/events/cloudWatchEvent.js b/lib/plugins/aws/package/compile/events/cloudWatchEvent.js index eb37ebf200d..129991cbbcd 100644 --- a/lib/plugins/aws/package/compile/events/cloudWatchEvent.js +++ b/lib/plugins/aws/package/compile/events/cloudWatchEvent.js @@ -70,7 +70,8 @@ class AwsCompileCloudWatchEventEvents { 'You can only set one of input, inputPath, or inputTransformer ', 'properties at the same time for cloudwatch events. ', 'Please check the AWS docs for more info', - ].join('') + ].join(''), + 'CLOUDWATCH_MULTIPLE_INPUT_PROPERTIES' ); } diff --git a/lib/plugins/aws/package/compile/events/cloudWatchLog.js b/lib/plugins/aws/package/compile/events/cloudWatchLog.js index 657fd84427a..d038f772278 100644 --- a/lib/plugins/aws/package/compile/events/cloudWatchLog.js +++ b/lib/plugins/aws/package/compile/events/cloudWatchLog.js @@ -59,7 +59,7 @@ class AwsCompileCloudWatchLogEvents { `"${LogGroupName}" logGroup for cloudwatchLog event is duplicated.`, ' This property can only be set once per CloudFormation stack.', ].join(''); - throw new ServerlessError(errorMessage); + throw new ServerlessError(errorMessage, 'CLOUDWATCHLOG_DUPLICATED_LOG_GROUP_EVENT'); } logGroupNames.push(LogGroupName); logGroupNamesThisFunction.push(LogGroupName); diff --git a/lib/plugins/aws/package/compile/events/cognitoUserPool.js b/lib/plugins/aws/package/compile/events/cognitoUserPool.js index 8dedf86390d..a983fa5b18d 100644 --- a/lib/plugins/aws/package/compile/events/cognitoUserPool.js +++ b/lib/plugins/aws/package/compile/events/cognitoUserPool.js @@ -151,7 +151,7 @@ class AwsCompileCognitoUserPoolEvents { 'Only one Cognito User Pool can be configured per function.', ` In "${FunctionName}" you're attempting to configure "${currentPoolName}" and "${pool}" at the same time.`, ].join(''); - throw new ServerlessError(errorMessage); + throw new ServerlessError(errorMessage, 'COGNITO_MULTIPLE_USER_POOLS_PER_FUNCTION'); } const eventFunctionLogicalId = this.provider.naming.getLambdaLogicalId(functionName); diff --git a/lib/plugins/aws/package/compile/events/eventBridge/index.js b/lib/plugins/aws/package/compile/events/eventBridge/index.js index 7c0e90b384f..8cc5846e929 100644 --- a/lib/plugins/aws/package/compile/events/eventBridge/index.js +++ b/lib/plugins/aws/package/compile/events/eventBridge/index.js @@ -126,7 +126,8 @@ class AwsCompileEventBridgeEvents { [ 'You can only set one of input, inputPath, or inputTransformer ', 'properties for eventBridge events.', - ].join('') + ].join(''), + 'EVENTBRIDGE_MULTIPLE_INPUT_PROPERTIES' ); } diff --git a/lib/plugins/aws/package/compile/events/s3/index.js b/lib/plugins/aws/package/compile/events/s3/index.js index 67f875b563c..b153f32a681 100644 --- a/lib/plugins/aws/package/compile/events/s3/index.js +++ b/lib/plugins/aws/package/compile/events/s3/index.js @@ -102,7 +102,7 @@ class AwsCompileS3Events { `${bucketName} - Bucket name must conform to pattern ${this.serverless.configSchemaHandler.schema.definitions.awsS3BucketName.pattern}.`, `Please check provider.s3.${bucketRef} and/or s3 events of function "${functionName}".`, ].join(' '); - throw new ServerlessError(errorMessage); + throw new ServerlessError(errorMessage, 'S3_INVALID_BUCKET_PATTERN'); } if (bucketsLambdaConfigurations[bucketName]) { @@ -282,7 +282,7 @@ class AwsCompileS3Events { 'Only one S3 Bucket can be configured per function.', ` In "${FunctionName}" you're attempting to configure "${currentBucketName}" and "${bucket}" at the same time.`, ].join(''); - throw new ServerlessError(errorMessage); + throw new ServerlessError(errorMessage, 'S3_MULTIPLE_BUCKETS_PER_FUNCTION'); } rules = (event.s3.rules || []).map((rule) => { diff --git a/lib/plugins/aws/package/compile/events/sns.js b/lib/plugins/aws/package/compile/events/sns.js index 198346e6962..6e38c9a3238 100644 --- a/lib/plugins/aws/package/compile/events/sns.js +++ b/lib/plugins/aws/package/compile/events/sns.js @@ -87,7 +87,8 @@ class AwsCompileSNSEvents { // Only true when providing CFN intrinsic function (not string) to arn property without topicName property if (!topicName) { throw new ServerlessError( - 'Missing "sns.topicName" setting (it needs to be provided if "sns.arn" is not provided as plain ARN string)' + 'Missing "sns.topicName" setting (it needs to be provided if "sns.arn" is not provided as plain ARN string)', + 'SNS_MISSING_TOPIC_NAME' ); } } else { 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 74038bc125b..24db29fcd66 100644 --- a/lib/plugins/aws/package/compile/events/websockets/lib/stage.js +++ b/lib/plugins/aws/package/compile/events/websockets/lib/stage.js @@ -46,7 +46,8 @@ module.exports = { throw new ServerlessError( `provider.logs.websocket.level is set to an invalid value. Support values are ${Array.from( validLogLevels - ).join(', ')}, got ${level}.` + ).join(', ')}, got ${level}.`, + 'WEBSOCKETS_INVALID_LOGS_LEVEL' ); } } 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 b7f253cbdcb..c2b52ea1311 100644 --- a/lib/plugins/aws/package/compile/events/websockets/lib/validate.js +++ b/lib/plugins/aws/package/compile/events/websockets/lib/validate.js @@ -82,7 +82,8 @@ module.exports = { if (_.isObject(event.websocket.authorizer.arn)) { if (!event.websocket.authorizer.name) { throw new ServerlessError( - 'Websocket Authorizer: Non-string "arn" needs to be accompanied with "name"' + 'Websocket Authorizer: Non-string "arn" needs to be accompanied with "name"', + 'WEBSOCKETS_MISSING_AUTHORIZER_NAME' ); } websocketObj.authorizer.name = event.websocket.authorizer.name; diff --git a/lib/plugins/aws/package/compile/functions.js b/lib/plugins/aws/package/compile/functions.js index 8a5807e7029..e259a986e5a 100644 --- a/lib/plugins/aws/package/compile/functions.js +++ b/lib/plugins/aws/package/compile/functions.js @@ -148,12 +148,14 @@ class AwsCompileFunctions { if (!functionObject.handler && !functionObject.image) { throw new ServerlessError( - `Either "handler" or "image" property needs to be set on function "${functionName}"` + `Either "handler" or "image" property needs to be set on function "${functionName}"`, + 'FUNCTION_NEITHER_HANDLER_NOR_IMAGE_DEFINED_ERROR' ); } if (functionObject.handler && functionObject.image) { throw new ServerlessError( - `Either "handler" or "image" property (not both) needs to be set on function "${functionName}".` + `Either "handler" or "image" property (not both) needs to be set on function "${functionName}".`, + 'FUNCTION_BOTH_HANDLER_AND_IMAGE_DEFINED_ERROR' ); } @@ -634,7 +636,10 @@ class AwsCompileFunctions { if (functionAddress.includes(':sqs:')) return 'sqs:SendMessage'; if (functionAddress.includes(':sns:')) return 'sns:Publish'; if (functionAddress.includes(':event-bus/')) return 'events:PutEvents'; - throw new ServerlessError(`Unsupported destination target ${functionAddress}`); + throw new ServerlessError( + `Unsupported destination target ${functionAddress}`, + 'UNSUPPORTED_DESTINATION_TARGET' + ); })(); iamPolicyStatements.push({ Effect: 'Allow', diff --git a/lib/plugins/aws/package/lib/generateCoreTemplate.js b/lib/plugins/aws/package/lib/generateCoreTemplate.js index 9eba01caefa..4991327931e 100644 --- a/lib/plugins/aws/package/lib/generateCoreTemplate.js +++ b/lib/plugins/aws/package/lib/generateCoreTemplate.js @@ -75,7 +75,7 @@ module.exports = { const errorMessage = [ 'You cannot enable and disable S3 Transfer Acceleration at the same time', ].join(''); - throw new ServerlessError(errorMessage); + throw new ServerlessError(errorMessage, 'S3_ACCELERATION_ENABLED_AND_DISABLED'); } if (bucketName) { diff --git a/lib/plugins/aws/utils/resolveCfImportValue.js b/lib/plugins/aws/utils/resolveCfImportValue.js index 5e9b7be00cd..5e9795e8299 100644 --- a/lib/plugins/aws/utils/resolveCfImportValue.js +++ b/lib/plugins/aws/utils/resolveCfImportValue.js @@ -11,7 +11,8 @@ function resolveCfImportValue(provider, name, sdkParams = {}) { } throw new ServerlessError( - `Could not resolve Fn::ImportValue with name ${name}. Are you sure this value is exported ?` + `Could not resolve Fn::ImportValue with name ${name}. Are you sure this value is exported ?`, + 'CF_IMPORT_RESOLUTION' ); }); } diff --git a/lib/plugins/aws/utils/resolveCfRefValue.js b/lib/plugins/aws/utils/resolveCfRefValue.js index 08ef8039e03..928e4f58e02 100644 --- a/lib/plugins/aws/utils/resolveCfRefValue.js +++ b/lib/plugins/aws/utils/resolveCfRefValue.js @@ -19,7 +19,8 @@ function resolveCfRefValue(provider, resourceLogicalId, sdkParams = {}) { } throw new ServerlessError( - `Could not resolve Ref with name ${resourceLogicalId}. Are you sure this value matches one resource logical ID ?` + `Could not resolve Ref with name ${resourceLogicalId}. Are you sure this value matches one resource logical ID ?`, + 'CF_REF_RESOLUTION' ); }); } diff --git a/test/unit/lib/plugins/aws/deployFunction.test.js b/test/unit/lib/plugins/aws/deployFunction.test.js index 04a47af4d98..7960927121e 100644 --- a/test/unit/lib/plugins/aws/deployFunction.test.js +++ b/test/unit/lib/plugins/aws/deployFunction.test.js @@ -968,6 +968,6 @@ describe('test/unit/lib/plugins/aws/deployFunction.test.js', () => { }, }, }) - ).to.be.eventually.rejectedWith('Please run "serverless deploy" to deploy your service'); + ).to.be.eventually.rejected.and.have.property('code', 'FUNCTION_NOT_YET_DEPLOYED'); }); }); From 81f0858cc93dfe7eda795380bc2cca5e54115989 Mon Sep 17 00:00:00 2001 From: Piotr Grzesik Date: Fri, 23 Apr 2021 10:19:44 +0200 Subject: [PATCH 0011/1101] refactor: Add `code` to `ServerlessError` in `lib/plugins` --- lib/plugins/aws/lib/monitorStack.js | 4 ++-- lib/plugins/config.js | 14 ++++++++------ lib/plugins/create/create.js | 14 +++++++------- lib/plugins/deploy.js | 2 +- lib/plugins/package/lib/packageService.js | 2 +- lib/plugins/package/lib/zipService.js | 7 +++++-- lib/plugins/plugin/lib/utils.js | 10 ++++++++-- lib/plugins/print.js | 14 ++++++++++---- lib/plugins/standalone.js | 6 ++++-- 9 files changed, 46 insertions(+), 27 deletions(-) diff --git a/lib/plugins/aws/lib/monitorStack.js b/lib/plugins/aws/lib/monitorStack.js index d044471ae27..9905fdcb53e 100644 --- a/lib/plugins/aws/lib/monitorStack.js +++ b/lib/plugins/aws/lib/monitorStack.js @@ -91,7 +91,7 @@ module.exports = { let errorMessage = 'An error occurred: '; errorMessage += `${stackLatestError.LogicalResourceId} - `; errorMessage += `${stackLatestError.ResourceStatusReason}.`; - throw new ServerlessError(errorMessage, 'STACK_MONITORING_FAILURE'); + throw new ServerlessError(errorMessage, 'STACK_OPERATION_FAILURE'); } }, (e) => { @@ -102,7 +102,7 @@ module.exports = { stackStatus = 'DELETE_COMPLETE'; return; } - throw new ServerlessError(e.message, 'STACK_MONITORING_FAILURE'); + throw new ServerlessError(e.message, 'STACK_OPERATION_FAILURE'); } ) ) diff --git a/lib/plugins/config.js b/lib/plugins/config.js index 2a4d4529726..525c00d13d4 100644 --- a/lib/plugins/config.js +++ b/lib/plugins/config.js @@ -69,7 +69,7 @@ class Config { `Provider "${provider}" is not supported.`, ` Supported providers are: ${humanReadableProvidersList}.`, ].join(''); - throw new ServerlessError(errorMessage); + throw new ServerlessError(errorMessage, 'INVALID_PROVIDER'); } } @@ -84,19 +84,20 @@ class Config { return; } if (this.serverless.isLocallyInstalled) { - throw new ServerlessError(noSupportErrorMessage); + throw new ServerlessError(noSupportErrorMessage, 'AUTO_UPDATE_NOT_SUPPORTED'); } if (this.serverless.isStandaloneExecutable) { if (process.platform === 'win32') { - throw new ServerlessError(noSupportErrorMessage); + throw new ServerlessError(noSupportErrorMessage, 'AUTO_UPDATE_NOT_SUPPORTED'); } } else { if (!(await isNpmGlobalPackage())) { - throw new ServerlessError(noSupportErrorMessage); + throw new ServerlessError(noSupportErrorMessage, 'AUTO_UPDATE_NOT_SUPPORTED'); } if (!(await isNpmPackageWritable(this.serverless))) { throw new ServerlessError( - 'Auto update cannot be set, due to missing write access to npm global installation' + 'Auto update cannot be set, due to missing write access to npm global installation', + 'AUTO_UPDATE_NOT_SET_MISSING_WRITE_ACCESS' ); } } @@ -123,7 +124,8 @@ class Config { if (!validShells.has(shell)) { throw new ServerlessError( - `Shell "${shell}" is not supported. Supported shells: ${Array.from(validShells)}.` + `Shell "${shell}" is not supported. Supported shells: ${Array.from(validShells)}.`, + 'TABCOMPLETION_INVALID_SHELL_ARGUMENT' ); } const location = (() => { diff --git a/lib/plugins/create/create.js b/lib/plugins/create/create.js index 8cc048d147a..099cad8dbfe 100644 --- a/lib/plugins/create/create.js +++ b/lib/plugins/create/create.js @@ -22,7 +22,7 @@ const handleServiceCreationError = (error) => { 'Please check that you have the required permissions to write to the directory', ].join(''); - throw new ServerlessError(errorMessage); + throw new ServerlessError(errorMessage, 'UNABLE_TO_CREATE_SERVICE'); }; class Create { @@ -66,7 +66,7 @@ class Create { this.serverless.cli.log(message); }) .catch((err) => { - throw new ServerlessError(err); + throw new ServerlessError(err, 'BOILERPLATE_GENERATION_ERROR'); }); } else if ('template-path' in this.options) { // Copying template from a local directory @@ -75,7 +75,7 @@ class Create { : path.join(process.cwd(), this.options.name); if (dirExistsSync(serviceDir)) { const errorMessage = `A folder named "${serviceDir}" already exists.`; - throw new ServerlessError(errorMessage); + throw new ServerlessError(errorMessage, 'TARGET_FOLDER_ALREADY_EXISTS'); } copyDirContentsSync(untildify(this.options['template-path']), serviceDir, { noLinks: true, @@ -88,7 +88,7 @@ class Create { 'You must either pass a template name (--template), ', 'a URL (--template-url) or a local path (--template-path).', ].join(''); - throw new ServerlessError(errorMessage); + throw new ServerlessError(errorMessage, 'MISSING_TEMPLATE_CLI_PARAM'); } return BbPromise.resolve(); } @@ -101,7 +101,7 @@ class Create { `Template "${this.options.template}" is not supported.`, ` Supported templates are: ${humanReadableTemplatesList}.`, ].join(''); - throw new ServerlessError(errorMessage); + throw new ServerlessError(errorMessage, 'NOT_SUPPORTED_TEMPLATE'); } // store the custom options for the service if given @@ -125,7 +125,7 @@ class Create { 'Rename or move the directory and try again if you want serverless to create it"', ].join(''); - throw new ServerlessError(errorMessage); + throw new ServerlessError(errorMessage, 'TARGET_FOLDER_ALREADY_EXISTS'); } this.serverless.cli.log(`Generating boilerplate in "${newPath}"`); @@ -144,7 +144,7 @@ class Create { `Move the file and try again if you want serverless to write a new "${filename}"`, ].join(''); - throw new ServerlessError(errorMessage); + throw new ServerlessError(errorMessage, 'TEMPLATE_FILE_ALREADY_EXISTS'); } }); } diff --git a/lib/plugins/deploy.js b/lib/plugins/deploy.js index b676b7f7f1d..ae682625aa7 100644 --- a/lib/plugins/deploy.js +++ b/lib/plugins/deploy.js @@ -33,7 +33,7 @@ class Deploy { const provider = this.serverless.service.provider.name; if (!this.serverless.getProvider(provider)) { const errorMessage = `The specified provider "${provider}" does not exist.`; - throw new ServerlessError(errorMessage); + throw new ServerlessError(errorMessage, 'INVALID_PROVIDER'); } if (this.options.function) { // If the user has given a function parameter, spawn a function deploy diff --git a/lib/plugins/package/lib/packageService.js b/lib/plugins/package/lib/packageService.js index 66ed9f1154f..363ed6849d8 100644 --- a/lib/plugins/package/lib/packageService.js +++ b/lib/plugins/package/lib/packageService.js @@ -260,7 +260,7 @@ module.exports = { .filter((r) => r[1] === true) .map((r) => r[0]); if (filePaths.length !== 0) return filePaths; - throw new ServerlessError('No file matches include / exclude patterns'); + throw new ServerlessError('No file matches include / exclude patterns', 'NO_MATCHED_FILES'); }); }, }; diff --git a/lib/plugins/package/lib/zipService.js b/lib/plugins/package/lib/zipService.js index 8271ab45f68..265c1693065 100644 --- a/lib/plugins/package/lib/zipService.js +++ b/lib/plugins/package/lib/zipService.js @@ -58,7 +58,7 @@ module.exports = { */ zipFiles(files, zipFileName, prefix) { if (files.length === 0) { - const error = new ServerlessError('No files to package'); + const error = new ServerlessError('No files to package', 'NO_FILES_TO_PACKAGE'); return BbPromise.reject(error); } @@ -117,7 +117,10 @@ module.exports = { filePath, }), (error) => { - throw new ServerlessError(`Cannot read file ${filePath} due to: ${error.message}`); + throw new ServerlessError( + `Cannot read file ${filePath} due to: ${error.message}`, + 'CANNOT_READ_FILE' + ); } ); }, diff --git a/lib/plugins/plugin/lib/utils.js b/lib/plugins/plugin/lib/utils.js index 1b86613029c..da2ff6f772e 100644 --- a/lib/plugins/plugin/lib/utils.js +++ b/lib/plugins/plugin/lib/utils.js @@ -12,7 +12,10 @@ const ServerlessError = require('../../../serverless-error'); module.exports = { validate() { if (!this.serverless.serviceDir) { - throw new ServerlessError('This command can only be run inside a service directory'); + throw new ServerlessError( + 'This command can only be run inside a service directory', + 'MISSING_SERVICE_DIRECTORY' + ); } return BbPromise.resolve(); @@ -22,7 +25,10 @@ module.exports = { if (this.serverless.configurationFilename) { return path.resolve(this.serverless.serviceDir, this.serverless.configurationFilename); } - throw new ServerlessError('Could not find any serverless service definition file.'); + throw new ServerlessError( + 'Could not find any serverless service definition file.', + 'MISSING_SERVICE_CONFIGURATION_FILE' + ); }, getPlugins() { diff --git a/lib/plugins/print.js b/lib/plugins/print.js index 0761fed4167..bcce55016d1 100644 --- a/lib/plugins/print.js +++ b/lib/plugins/print.js @@ -100,7 +100,10 @@ class Print { conf = conf[step]; if (!conf) { - throw new ServerlessError(`Path "${this.options.path}" not found`); + throw new ServerlessError( + `Path "${this.options.path}" not found`, + 'INVALID_PATH_ARGUMENT' + ); } } } @@ -110,7 +113,7 @@ class Print { if (this.options.transform === 'keys') { conf = Object.keys(conf); } else { - throw new ServerlessError('Transform can only be "keys"'); + throw new ServerlessError('Transform can only be "keys"', 'INVALID_TRANSFORM'); } } @@ -123,7 +126,10 @@ class Print { out = conf.join(os.EOL); } else { if (_.isObject(conf)) { - throw new ServerlessError('Cannot print an object as "text"'); + throw new ServerlessError( + 'Cannot print an object as "text"', + 'PRINT_INVALID_OBJECT_AS_TEXT' + ); } out = String(conf); @@ -133,7 +139,7 @@ class Print { } else if (format === 'yaml') { out = yaml.dump(JSON.parse(jc.stringify(conf)), { noRefs: true }); } else { - throw new ServerlessError('Format must be "yaml", "json" or "text"'); + throw new ServerlessError('Format must be "yaml", "json" or "text"', 'PRINT_INVALID_FORMAT'); } this.serverless.cli.consoleLog(out); diff --git a/lib/plugins/standalone.js b/lib/plugins/standalone.js index edd9bfbde0d..4acd1aeb9e3 100644 --- a/lib/plugins/standalone.js +++ b/lib/plugins/standalone.js @@ -57,7 +57,8 @@ module.exports = class Standalone { 'Note: Service is safe to upgrade if no deprecations are logged during its deployment.', ` Check https://github.com/serverless/serverless/releases/tag/v${latestMajor}.0.0 ` + 'for list of all breaking changes', - ].join('\n') + ].join('\n'), + 'CANNOT_UPGRADE_MAJOR' ); } this.serverless.cli.log('Downloading new version...'); @@ -66,7 +67,8 @@ module.exports = class Standalone { if (!standaloneResponse.ok) { throw new ServerlessError( 'Sorry unable to `upgrade` at this point ' + - `(server rejected request with ${standaloneResponse.status})` + `(server rejected request with ${standaloneResponse.status})`, + 'UPGRADE_ERROR' ); } await fse.remove(BINARY_TMP_PATH); From 8c37e0ac6df7a1a010e57ef14702185b2ef07dfa Mon Sep 17 00:00:00 2001 From: Piotr Grzesik Date: Fri, 23 Apr 2021 12:47:20 +0200 Subject: [PATCH 0012/1101] refactor: Adjust invalid `code` for `ServerlessError` --- lib/classes/ConfigSchemaHandler/index.js | 2 +- lib/classes/Variables.js | 2 +- lib/configuration/variables/sources/env.js | 2 +- lib/configuration/variables/sources/file.js | 2 +- .../variables/sources/instance-dependent/get-cf.js | 2 +- .../variables/sources/instance-dependent/get-s3.js | 2 +- .../variables/sources/instance-dependent/get-sls.js | 2 +- .../variables/sources/instance-dependent/get-ssm.js | 2 +- lib/configuration/variables/sources/opt.js | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/classes/ConfigSchemaHandler/index.js b/lib/classes/ConfigSchemaHandler/index.js index abf7ba36931..8456310ed51 100644 --- a/lib/classes/ConfigSchemaHandler/index.js +++ b/lib/classes/ConfigSchemaHandler/index.js @@ -122,7 +122,7 @@ class ConfigSchemaHandler { ? `${ERROR_PREFIX}: \n ${messages.join('\n ')}` : `${ERROR_PREFIX} ${messages[0]}`; - throw new ServerlessError(errorMessage, 'SCHEMA_VALIDATION'); + throw new ServerlessError(errorMessage, 'INVALID_NON_SCHEMA_COMPLIANT_CONFIGURATION'); } else { if (messages.length === 1) { this.serverless.cli.log(`${WARNING_PREFIX} ${messages[0]}`, 'Serverless', { diff --git a/lib/classes/Variables.js b/lib/classes/Variables.js index 9e353dcb1d5..8a5ec30c1f5 100644 --- a/lib/classes/Variables.js +++ b/lib/classes/Variables.js @@ -451,7 +451,7 @@ class Variables { ` a string for variable ${matchedString}.`, ' Please make sure the value of the property is a string.', ].join(''); - throw new ServerlessError(errorMessage, 'INVALID_NON_STRING_PROPERTY'); + throw new ServerlessError(errorMessage, 'INVALID_NON_STRING_VARIABLE_RESOLUTION_RESULT'); } return property; } diff --git a/lib/configuration/variables/sources/env.js b/lib/configuration/variables/sources/env.js index 9f1f15549ea..8856947ae9a 100644 --- a/lib/configuration/variables/sources/env.js +++ b/lib/configuration/variables/sources/env.js @@ -14,7 +14,7 @@ module.exports = { address = ensureString(address, { Error: ServerlessError, errorMessage: 'Non-string address argument in variable "env" source: %v', - errorCode: 'INVALID_ENV_SOURCE_ADDRESS_ARGUMENT', + errorCode: 'INVALID_ENV_SOURCE_ADDRESS', }); return { value: process.env[address] || null, isPending: !isSourceFulfilled }; diff --git a/lib/configuration/variables/sources/file.js b/lib/configuration/variables/sources/file.js index 4e6b697a9ce..5075144dc45 100644 --- a/lib/configuration/variables/sources/file.js +++ b/lib/configuration/variables/sources/file.js @@ -46,7 +46,7 @@ module.exports = { address = ensureString(address, { Error: ServerlessError, errorMessage: 'Non-string address argument for variable "file" source: %v', - errorCode: 'INVALID_FILE_SOURCE_ADDRESS_ARGUMENT', + errorCode: 'INVALID_FILE_SOURCE_ADDRESS', }); } diff --git a/lib/configuration/variables/sources/instance-dependent/get-cf.js b/lib/configuration/variables/sources/instance-dependent/get-cf.js index 382518b4a8b..84bf9374bd1 100644 --- a/lib/configuration/variables/sources/instance-dependent/get-cf.js +++ b/lib/configuration/variables/sources/instance-dependent/get-cf.js @@ -17,7 +17,7 @@ module.exports = (serverlessInstance) => { address = ensureString(address, { Error: ServerlessError, errorMessage: 'Non-string address argument in variable "cf" source: %v', - errorCode: 'INVALID_CF_SOURCE_ADDRESS_ARGUMENT', + errorCode: 'INVALID_CF_SOURCE_ADDRESS', }); const separatorIndex = address.indexOf('.'); if (separatorIndex === -1) { diff --git a/lib/configuration/variables/sources/instance-dependent/get-s3.js b/lib/configuration/variables/sources/instance-dependent/get-s3.js index e48facee732..4726c499110 100644 --- a/lib/configuration/variables/sources/instance-dependent/get-s3.js +++ b/lib/configuration/variables/sources/instance-dependent/get-s3.js @@ -16,7 +16,7 @@ module.exports = (serverlessInstance) => { address = ensureString(address, { Error: ServerlessError, errorMessage: 'Non-string address argument in variable "s3" source: %v', - errorCode: 'INVALID_S3_SOURCE_ADDRESS_ARGUMENT', + errorCode: 'INVALID_S3_SOURCE_ADDRESS', }); const separatorIndex = address.indexOf('/'); if (separatorIndex === -1) { diff --git a/lib/configuration/variables/sources/instance-dependent/get-sls.js b/lib/configuration/variables/sources/instance-dependent/get-sls.js index 74fdceaea26..4b7a6780363 100644 --- a/lib/configuration/variables/sources/instance-dependent/get-sls.js +++ b/lib/configuration/variables/sources/instance-dependent/get-sls.js @@ -15,7 +15,7 @@ module.exports = (serverlessInstance) => { address = ensureString(address, { Error: ServerlessError, errorMessage: 'Non-string address argument in variable "sls" source: %v', - errorCode: 'INVALID_SLS_SOURCE_ADDRESS_ARGUMENT', + errorCode: 'INVALID_SLS_SOURCE_ADDRESS', }); switch (address) { diff --git a/lib/configuration/variables/sources/instance-dependent/get-ssm.js b/lib/configuration/variables/sources/instance-dependent/get-ssm.js index bf2623542d6..83ff2ea7ea2 100644 --- a/lib/configuration/variables/sources/instance-dependent/get-ssm.js +++ b/lib/configuration/variables/sources/instance-dependent/get-ssm.js @@ -19,7 +19,7 @@ module.exports = (serverlessInstance) => { address = ensureString(address, { Error: ServerlessError, errorMessage: 'Non-string address argument in variable "ssm" source: %v', - errorCode: 'INVALID_SSM_SOURCE_ADDRESS_ARGUMENT', + errorCode: 'INVALID_SSM_SOURCE_ADDRESS', }); const region = !params || !params[0] || params[0] === 'raw' ? undefined : params[0]; const shouldReturnRawValue = params && (params[0] === 'raw' || params[1] === 'raw'); diff --git a/lib/configuration/variables/sources/opt.js b/lib/configuration/variables/sources/opt.js index e543444f8e9..6a7af5c4aac 100644 --- a/lib/configuration/variables/sources/opt.js +++ b/lib/configuration/variables/sources/opt.js @@ -9,7 +9,7 @@ module.exports = { isOptional: true, Error: ServerlessError, errorMessage: 'Non-string address argument in variable "opt" source: %v', - errorCode: 'INVALID_OPT_SOURCE_ADDRESS_ARGUMENT', + errorCode: 'INVALID_OPT_SOURCE_ADDRESS', }); if (!isSourceFulfilled) { if (address == null) return { value: null, isPending: true }; From 31a51e21396dba8ca4ad4d22c023a8a63960a40c Mon Sep 17 00:00:00 2001 From: Piotr Grzesik Date: Fri, 23 Apr 2021 13:48:45 +0200 Subject: [PATCH 0013/1101] docs: Add example on how to enable native CF for eventBridge --- docs/providers/aws/events/event-bridge.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/providers/aws/events/event-bridge.md b/docs/providers/aws/events/event-bridge.md index 2b8f29c6428..ff58f085740 100644 --- a/docs/providers/aws/events/event-bridge.md +++ b/docs/providers/aws/events/event-bridge.md @@ -71,7 +71,13 @@ functions: ### Re-using an existing event bus -If you want to reuse an existing event bus, you can define it with literal `arn` or with a reference to an existing event bus name via CF intrinsic functions. Referencing via intrinsic functions in available only if you use native CloudFormation support with `provider.eventBridge.useCloudFormation: true` setting. +If you want to reuse an existing event bus, you can define it with literal `arn` or with a reference to an existing event bus name via CF intrinsic functions. Referencing via intrinsic functions in available only if you use native CloudFormation support with `provider.eventBridge.useCloudFormation: true` setting: + +```yml +provider: + eventBridge: + useCloudFormation: true +``` Using literal `arn`: From 7a0fbf7143ab9901517f9a67fcd228155fa8e47b Mon Sep 17 00:00:00 2001 From: Corentin Doue <31917261+CorentinDoue@users.noreply.github.com> Date: Tue, 27 Apr 2021 09:52:35 +0200 Subject: [PATCH 0014/1101] docs: Improvements for `google` provider docs (#9326) --- .../google/cli-reference/invoke-local.md | 67 ----------------- docs/providers/google/guide/credentials.md | 74 ++++++++++++++++--- 2 files changed, 63 insertions(+), 78 deletions(-) delete mode 100644 docs/providers/google/cli-reference/invoke-local.md diff --git a/docs/providers/google/cli-reference/invoke-local.md b/docs/providers/google/cli-reference/invoke-local.md deleted file mode 100644 index 4dcc28c3d41..00000000000 --- a/docs/providers/google/cli-reference/invoke-local.md +++ /dev/null @@ -1,67 +0,0 @@ - - - - -### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/google/cli-reference/invoke-local) - - - -# Google - Invoke Local - -Invokes deployed function locally. It allows to send event data to the function, read logs and display other important information of the function invocation. - -```bash -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. - -> 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 - -### Local function invocation - -```bash -serverless invoke local -f functionName -``` - -### Local function invocation with data - -```bash -serverless invoke local -f functionName -d '{ "data": "hello world" }' -``` - -### Local function invocation with data passing - -```bash -serverless invoke local -f functionName -p path/to/file.json - -# OR - -serverless invoke local -f functionName -p path/to/file.yaml -``` - -### Local function invocation, setting environment variables - -```bash -serverless invoke local -f functionName -e VAR1=value1 - -# Or more than one variable - -serverless invoke local -f functionName -e VAR1=value1 -e VAR2=value2 -``` diff --git a/docs/providers/google/guide/credentials.md b/docs/providers/google/guide/credentials.md index 8ed710c5b03..e7c49344ae1 100644 --- a/docs/providers/google/guide/credentials.md +++ b/docs/providers/google/guide/credentials.md @@ -49,9 +49,39 @@ Go to the Google Accounts are real users who can be authenticated by the Google SSO) + +This method is the most convenient to allow developers to develop and deploy a Serverless application locally. + +If you are owner of the project you have nothing to do. +Otherwise, make sure your user has at least the following roles: + +- `Deployment Manager Editor` +- `Storage Admin` +- `Logging Admin` +- `Cloud Functions Developer` + +### Service Account + +(Service accounts are accounts for applications instead of individuals end users) + +This method is useful for to authenticate a CI/CD or to assume a specific role without changing the roles of a **Google Account**. + +Create a **Service Account** with at least the following roles: + +- `Deployment Manager Editor` +- `Storage Admin` +- `Logging Admin` +- `Cloud Functions Developer` + +How to create a **Service Account**: 1. Go to the Google Cloud Console. 2. Choose the project that you are working on from the top drop down @@ -60,21 +90,43 @@ You need to create credentials with appropriate roles Serverless can use to crea 5. Click `CREATE SERVICE ACCOUNT` button on the top 6. Input Service account name and Service account ID will be generated automatically for you. Change it if you wish to. 7. Click `Create` button -8. Add `Deployment Manager Editor`, `Storage Admin`, `Logging Admin`, `Cloud Functions Developer` roles and click `Continue` -9. Click `+CREATE KEY` button and select `JSON` key type and click `Create` button -10. You will see a json (AKA `keyfile`) file downloaded -11. Click `Done` button -12. Save the `keyfile` somewhere secure. We recommend making a folder in your root folder and putting it there. Like this, `~/.gcloud/keyfile.json`. You can change the file name from `keyfile` to anything. Remember the path you saved it to. +8. Add `Deployment Manager Editor`, `Storage Admin`, `Logging Admin`, `Cloud Functions Developer` roles +9. Click `Done` button -## Update the `provider` config in `serverless.yml` +## Authenticate -Open up your `serverless.yml` file and update the `provider` section with your Google Cloud Project id and -the path to your `keyfile.json` file (this path needs to be absolute!). It should look something like this: +The Serverless Google Cloud plugin supports several authentication methods. + +### Application Default Credentials + +The plugin will let Google find the **Application Default Credentials** and implicitly authenticate. + +To authenticate with a **Google Account** use gcloud cli login + +```shell +gcloud auth application-default login +``` + +To authenticate with a **Service Account**: + +1. Go on the Service Account panel of `IAM & admin` +2. Select a Service Account and click on `manage keys` +3. Create a JSON credentials keyfile +4. Download and store the keyfile +5. expose the absolute path of the keyfile in the environment variable `GOOGLE_APPLICATION_CREDENTIALS` + +### Explicitly provide the path of a credentials keyfile + +1. Get a credentials keyfile as explained above. +2. In the `provider` config in `serverless.yml`, add a `credentials` attribute with the absolute path of the credentials keyfile: ```yml provider: name: google runtime: nodejs project: my-serverless-project-1234 - credentials: ~/.gcloud/keyfile.json + + credentials: ~/.gcloud/keyfile.json # <- the path must be absolute ``` + +If `provider.credentials` is provided in the `serverless.yml`, the **Application Default Credentials** will be ignored. From c8adc0c796a6558c3fe1bc86e3647d3fe711a9ad Mon Sep 17 00:00:00 2001 From: Android3000 Date: Tue, 27 Apr 2021 06:24:04 -0400 Subject: [PATCH 0015/1101] feat(AWS IAM): Support `provider.iam.role.path` (#9363) --- docs/providers/aws/guide/iam.md | 17 +++++++ docs/providers/aws/guide/serverless.yml.md | 1 + lib/plugins/aws/lib/naming.js | 3 +- lib/plugins/aws/provider.js | 4 ++ test/unit/lib/plugins/aws/lib/naming.test.js | 6 +++ .../aws/package/lib/mergeIamTemplates.test.js | 45 ++++++++++++------- 6 files changed, 60 insertions(+), 16 deletions(-) diff --git a/docs/providers/aws/guide/iam.md b/docs/providers/aws/guide/iam.md index 8de6af6b2d0..846e99c2615 100644 --- a/docs/providers/aws/guide/iam.md +++ b/docs/providers/aws/guide/iam.md @@ -24,6 +24,8 @@ All IAM-related properties of provider are grouped under `iam` property: provider: iam: role: + name: custom-role-name + path: /custom-role-path/ statements: - Effect: 'Allow', Resource: '*', @@ -120,6 +122,21 @@ provider: name: your-custom-name-${sls:stage}-role ``` +### Path for the Default IAM Role + +By default, it will use a path of: `/` + +This can be overridden by setting `provider.iam.role.path`: + +```yml +service: new-service + +provider: + iam: + role: + path: /your-custom-path/ +``` + ## Custom IAM Roles **WARNING:** You need to take care of the overall role setup as soon as you define custom roles. diff --git a/docs/providers/aws/guide/serverless.yml.md b/docs/providers/aws/guide/serverless.yml.md index b7eb727ca72..359ab8b46ca 100644 --- a/docs/providers/aws/guide/serverless.yml.md +++ b/docs/providers/aws/guide/serverless.yml.md @@ -208,6 +208,7 @@ provider: # .. OR configure logical role role: name: your-custom-name-role # Optional custom name for default IAM role + path: /your-custom-path/ # Optional custom path for default IAM role managedPolicies: # Optional IAM Managed Policies, which allows to include the policies into IAM Role - arn:aws:iam:*****:policy/some-managed-policy permissionsBoundary: arn:aws:iam::XXXXXX:policy/policy # ARN of an Permissions Boundary for the role. diff --git a/lib/plugins/aws/lib/naming.js b/lib/plugins/aws/lib/naming.js index 7893a57e17f..c93353c6f5b 100644 --- a/lib/plugins/aws/lib/naming.js +++ b/lib/plugins/aws/lib/naming.js @@ -88,7 +88,8 @@ module.exports = { // Role getRolePath() { - return '/'; + const customRolePath = _.get(this.provider, 'serverless.service.provider.iam.role.path'); + return customRolePath || '/'; }, getRoleName() { const customRoleName = _.get(this.provider, 'serverless.service.provider.iam.role.name'); diff --git a/lib/plugins/aws/provider.js b/lib/plugins/aws/provider.js index baad1017a67..7789c6c154e 100644 --- a/lib/plugins/aws/provider.js +++ b/lib/plugins/aws/provider.js @@ -886,6 +886,10 @@ class AwsProvider { type: 'string', pattern: '^[A-Za-z0-9/_+=,.@-]{1,64}$', }, + path: { + type: 'string', + pattern: '(^\\/$)|(^\\/[\u0021-\u007f]+\\/$)', + }, managedPolicies: { type: 'array', items: { $ref: '#/definitions/awsArn' }, diff --git a/test/unit/lib/plugins/aws/lib/naming.test.js b/test/unit/lib/plugins/aws/lib/naming.test.js index 949f4f37a37..f0a348ca5d6 100644 --- a/test/unit/lib/plugins/aws/lib/naming.test.js +++ b/test/unit/lib/plugins/aws/lib/naming.test.js @@ -106,6 +106,12 @@ describe('#naming()', () => { it('should return `/`', () => { expect(sdk.naming.getRolePath()).to.equal('/'); }); + + it('uses custom role path', () => { + const customRolePath = '/custom-role-path/'; + _.set(sdk.naming.provider, 'serverless.service.provider.iam.role.path', customRolePath); + expect(sdk.naming.getRolePath()).to.eql(customRolePath); + }); }); describe('#getRoleName()', () => { diff --git a/test/unit/lib/plugins/aws/package/lib/mergeIamTemplates.test.js b/test/unit/lib/plugins/aws/package/lib/mergeIamTemplates.test.js index 388e3e9b710..74b5edc6a61 100644 --- a/test/unit/lib/plugins/aws/package/lib/mergeIamTemplates.test.js +++ b/test/unit/lib/plugins/aws/package/lib/mergeIamTemplates.test.js @@ -1,8 +1,12 @@ 'use strict'; -const expect = require('chai').expect; +const chai = require('chai'); const runServerless = require('../../../../../../utils/run-serverless'); +chai.use(require('chai-as-promised')); + +const expect = chai.expect; + describe('lib/plugins/aws/package/lib/mergeIamTemplates.test.js', () => { describe('No default role', () => { it('should not create role resource if there are no functions', async () => { @@ -274,6 +278,8 @@ describe('lib/plugins/aws/package/lib/mergeIamTemplates.test.js', () => { provider: { iam: { role: { + name: 'custom-default-role', + path: '/custom-role-path/', statements: [ { Effect: 'Allow', @@ -307,23 +313,32 @@ describe('lib/plugins/aws/package/lib/mergeIamTemplates.test.js', () => { }); it('should support `provider.iam.role.name`', async () => { - const customRoleName = 'custom-default-role'; - const { cfTemplate, awsNaming } = await runServerless({ - fixture: 'function', - command: 'package', - configExt: { - provider: { - iam: { - role: { - name: customRoleName, + const iamRoleLambdaResource = cfResources[naming.getRoleLogicalId()]; + expect(iamRoleLambdaResource.Properties.RoleName).to.be.eq('custom-default-role'); + }); + + it('should support `provider.iam.role.path`', async () => { + const iamRoleLambdaResource = cfResources[naming.getRoleLogicalId()]; + expect(iamRoleLambdaResource.Properties.Path).to.be.eq('/custom-role-path/'); + }); + + it('should reject an invalid `provider.iam.role.path`', async () => { + const customRolePath = '/invalid'; + await expect( + runServerless({ + fixture: 'function', + command: 'package', + configExt: { + provider: { + iam: { + role: { + path: customRolePath, + }, }, }, }, - }, - }); - - const iamRoleLambdaResource = cfTemplate.Resources[awsNaming.getRoleLogicalId()]; - expect(iamRoleLambdaResource.Properties.RoleName).to.be.eq(customRoleName); + }) + ).to.be.eventually.rejectedWith(/'provider.iam.role.path': should match pattern/); }); it('should support `provider.iam.role.statements`', async () => { From d66215097658be0bd39aef2740cec1d3ffff763d Mon Sep 17 00:00:00 2001 From: Mariusz Nowak Date: Mon, 26 Apr 2021 15:24:32 +0200 Subject: [PATCH 0016/1101] test: Improve internal naming --- .../configuration/variables/resolve.test.js | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/test/unit/lib/configuration/variables/resolve.test.js b/test/unit/lib/configuration/variables/resolve.test.js index ca492efc2cc..a7efa4cf33e 100644 --- a/test/unit/lib/configuration/variables/resolve.test.js +++ b/test/unit/lib/configuration/variables/resolve.test.js @@ -34,10 +34,10 @@ describe('test/unit/lib/configuration/variables/resolve.test.js', () => { propertyDeepCircularC: '${sourceProperty(propertyDeepCircularA)}', propertyRoot: '${sourceProperty:}', withString: 'foo${sourceDirect:}', - resolvesVariablesObject: '${sourceVariables(object)}', - resolvesVariablesArray: '${sourceVariables(array)}', - resolvesVariablesString: '${sourceVariables(string)}', - resolvesVariablesStringInvalid: '${sourceVariables(stringInvalid)}', + resolvesResultVariablesObject: '${sourceResultVariables(object)}', + resolvesResultVariablesArray: '${sourceResultVariables(array)}', + resolvesResultVariablesString: '${sourceResultVariables(string)}', + resolvesResultVariablesStringInvalid: '${sourceResultVariables(stringInvalid)}', incomplete: '${sourceDirect:}elo${sourceIncomplete:}', missing: '${sourceDirect:}elo${sourceMissing:}other${sourceMissing:}', missingFallback: '${sourceDirect:}elo${sourceMissing:, "foo"}', @@ -83,7 +83,7 @@ describe('test/unit/lib/configuration/variables/resolve.test.js', () => { return { value: result == null ? null : result }; }, }, - sourceVariables: { + sourceResultVariables: { resolve: ({ params: [type] }) => { switch (type) { case 'object': @@ -229,9 +229,9 @@ describe('test/unit/lib/configuration/variables/resolve.test.js', () => { }); it('should resolve variables in returned results', () => { - expect(configuration.resolvesVariablesObject).to.deep.equal({ foo: 234 }); - expect(configuration.resolvesVariablesArray).to.deep.equal([1, 234]); - expect(configuration.resolvesVariablesString).to.equal(234); + expect(configuration.resolvesResultVariablesObject).to.deep.equal({ foo: 234 }); + expect(configuration.resolvesResultVariablesArray).to.deep.equal([1, 234]); + expect(configuration.resolvesResultVariablesString).to.equal(234); }); // https://github.com/serverless/serverless/issues/9016 @@ -340,7 +340,7 @@ describe('test/unit/lib/configuration/variables/resolve.test.js', () => { }); it('should error on invalid variable notation in returned result', () => { - const valueMeta = variablesMeta.get('resolvesVariablesStringInvalid'); + const valueMeta = variablesMeta.get('resolvesResultVariablesStringInvalid'); expect(valueMeta).to.not.have.property('variables'); expect(valueMeta.error.code).to.equal('UNTERMINATED_VARIABLE'); }); @@ -375,7 +375,7 @@ describe('test/unit/lib/configuration/variables/resolve.test.js', () => { 'propertyDeepCircularB', 'propertyDeepCircularC', 'propertyRoot', - 'resolvesVariablesStringInvalid', + 'resolvesResultVariablesStringInvalid', 'missing', 'nonStringStringPart', 'nestUnrecognized\0unrecognized', From 102d0bafe63c9ad0c824fb0d6e50e74ef5b4b550 Mon Sep 17 00:00:00 2001 From: Mariusz Nowak Date: Mon, 26 Apr 2021 15:40:45 +0200 Subject: [PATCH 0017/1101] refactor(Variables): Fix inline comment typo --- lib/configuration/variables/resolve.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/configuration/variables/resolve.js b/lib/configuration/variables/resolve.js index 583c26a4a1b..48a5421421b 100644 --- a/lib/configuration/variables/resolve.js +++ b/lib/configuration/variables/resolve.js @@ -48,7 +48,7 @@ class VariablesResolver { // Resolve all variables simultaneously // Resolution batches are identified to ensure source resolution cache is not shared among them. - // (each new resolutioon batch has access to extended configuration structure + // (each new resolution batch has access to extended configuration structure // and cached source resolver may block access to already resolved structure) const resolutionBatchId = ++lastResolutionBatchId; From 2ff58b16bf3fe766685d5b6c30fd9a2bb6e22f0f Mon Sep 17 00:00:00 2001 From: Mariusz Nowak Date: Tue, 27 Apr 2021 12:36:14 +0200 Subject: [PATCH 0018/1101] feat(Variables): Expose variable resolver function to variable sources --- docs/providers/aws/guide/variables.md | 14 ++++----- lib/configuration/variables/resolve.js | 24 +++++++++++++++ lib/configuration/variables/sources/file.js | 13 ++++++-- .../configuration/variables/resolve.test.js | 30 +++++++++++++++++++ .../variables/sources/file.test.js | 11 +++++++ .../sources/fixture/file-function-variable.js | 5 ++++ .../file-property-function-variable.js | 5 ++++ 7 files changed, 92 insertions(+), 10 deletions(-) create mode 100644 test/unit/lib/configuration/variables/sources/fixture/file-function-variable.js create mode 100644 test/unit/lib/configuration/variables/sources/fixture/file-property-function-variable.js diff --git a/docs/providers/aws/guide/variables.md b/docs/providers/aws/guide/variables.md index 60e6b2afd6a..3661b4b13b7 100644 --- a/docs/providers/aws/guide/variables.md +++ b/docs/providers/aws/guide/variables.md @@ -476,17 +476,17 @@ With a new variables resolver (_which will be the only used resolver in v3 of a - `options` - An object referencing resolved CLI params as passed to the command - `resolveConfigurationProperty([key1, key2, ...keyN])` - Async function which resolves specific service configuration property. It returns a fully resolved value of configuration property. If circular reference is detected resolution will be rejected. +- `resolveVariable(variableString)` - Async function which resolves provided variable string. String should be passed without wrapping (`${` and `}`) braces. Example valid values: + - `file(./config.js):SOME_VALUE` + - `env:SOME_ENV_VAR, null` (end with `, null`, if missing value at the variable source should be resolved with `null`, and not with a thrown error) -Example, of how to obtain a value of AWS region that will be used by Serverless Framework: +Example on how to obtain some Serverless Framework configuration values: ```js // config.js (when relying on new variables resolver) -module.exports = async ({ options, resolveConfigurationProperty }) => { - let region = options.region; - if (!region) { - region = await resolveConfigurationProperty(['provider', 'region']); - if (!region) region = 'us-east-1'; // Framework default - } +module.exports = async ({ options, resolveVariable }) => { + const stage = await resolveVariable('sls:stage'); + const region = await resolveVariable('opt:region, self:provider.region, "us-east-1"'); ... } ``` diff --git a/lib/configuration/variables/resolve.js b/lib/configuration/variables/resolve.js index 48a5421421b..c71aef5c244 100644 --- a/lib/configuration/variables/resolve.js +++ b/lib/configuration/variables/resolve.js @@ -483,6 +483,30 @@ Object.defineProperties( propertyPath, ensureArray(dependencyPropertyPathKeys, { ensureItem: ensureString }) ), + resolveVariable: async (variableString) => { + variableString = `\${${ensureString(variableString, { + Error: ServerlessError, + name: 'variableString', + errorCode: 'INVALID_VARIABLE_INPUT_TYPE', + })}}`; + const variableData = parse(variableString); + if (!variableData) { + throw new ServerlessError( + `Invalid variable value: "${variableString}"`, + 'INVALID_VARIABLE_INPUT' + ); + } + const meta = { + value: variableString, + variables: variableData, + }; + await this.resolveVariables(resolutionBatchId, propertyPath, meta); + if (meta.error) throw meta.error; + if (meta.variables) { + throw new ServerlessError('Cannot resolve variable', 'MISSING_VARIABLE_DEPENDENCY'); + } + return meta.value; + }, }); ensurePlainObject(result, { errorMessage: `Unexpected "${sourceData.type}" source result: %v`, diff --git a/lib/configuration/variables/sources/file.js b/lib/configuration/variables/sources/file.js index 5075144dc45..4b6ad596bc8 100644 --- a/lib/configuration/variables/sources/file.js +++ b/lib/configuration/variables/sources/file.js @@ -21,7 +21,14 @@ const readFile = async (filePath, serviceDir) => { }; module.exports = { - resolve: async ({ serviceDir, params, address, resolveConfigurationProperty, options }) => { + resolve: async ({ + serviceDir, + params, + address, + resolveConfigurationProperty, + resolveVariable, + options, + }) => { if (!params || !params[0]) { throw new ServerlessError( 'Missing path argument in variable "file" source', @@ -116,7 +123,7 @@ module.exports = { } try { isResolvedByFunction = true; - return await result({ options, resolveConfigurationProperty }); + return await result({ options, resolveConfigurationProperty, resolveVariable }); } catch (error) { if (error.code === 'MISSING_VARIABLE_DEPENDENCY') throw error; throw new ServerlessError( @@ -163,7 +170,7 @@ module.exports = { } isResolvedByFunction = true; try { - result = await result({ options, resolveConfigurationProperty }); + result = await result({ options, resolveConfigurationProperty, resolveVariable }); } catch (error) { if (error.code === 'MISSING_VARIABLE_DEPENDENCY') throw error; throw new ServerlessError( diff --git a/test/unit/lib/configuration/variables/resolve.test.js b/test/unit/lib/configuration/variables/resolve.test.js index a7efa4cf33e..078716316a6 100644 --- a/test/unit/lib/configuration/variables/resolve.test.js +++ b/test/unit/lib/configuration/variables/resolve.test.js @@ -38,6 +38,10 @@ describe('test/unit/lib/configuration/variables/resolve.test.js', () => { resolvesResultVariablesArray: '${sourceResultVariables(array)}', resolvesResultVariablesString: '${sourceResultVariables(string)}', resolvesResultVariablesStringInvalid: '${sourceResultVariables(stringInvalid)}', + resolvesVariables: '${sourceResolveVariable("sourceParam(${sourceDirect:})")}', + resolvesVariablesFallback: '${sourceResolveVariable("sourceMissing:, null"), null}', + resolvesVariablesInvalid1: '${sourceResolveVariable("sourceDirect(")}', + resolvesVariablesInvalid2: '${sourceResolveVariable("sourceDirect")}', incomplete: '${sourceDirect:}elo${sourceIncomplete:}', missing: '${sourceDirect:}elo${sourceMissing:}other${sourceMissing:}', missingFallback: '${sourceDirect:}elo${sourceMissing:, "foo"}', @@ -83,6 +87,11 @@ describe('test/unit/lib/configuration/variables/resolve.test.js', () => { return { value: result == null ? null : result }; }, }, + sourceResolveVariable: { + resolve: async ({ params, resolveVariable }) => { + return { value: await resolveVariable(params[0]) }; + }, + }, sourceResultVariables: { resolve: ({ params: [type] }) => { switch (type) { @@ -376,6 +385,8 @@ describe('test/unit/lib/configuration/variables/resolve.test.js', () => { 'propertyDeepCircularC', 'propertyRoot', 'resolvesResultVariablesStringInvalid', + 'resolvesVariablesInvalid1', + 'resolvesVariablesInvalid2', 'missing', 'nonStringStringPart', 'nestUnrecognized\0unrecognized', @@ -391,6 +402,25 @@ describe('test/unit/lib/configuration/variables/resolve.test.js', () => { `infiniteResolutionRecursion${'\0nest'.repeat(10)}`, ]); }); + + describe('"resolveVariable" source util', () => { + it('should resolve variable', () => { + expect(configuration.resolvesVariables).to.equal('234'); + }); + it('should support multiple sources', () => { + expect(configuration.resolvesVariablesFallback).to.equal(null); + }); + + it('should error on invalid input', () => { + let valueMeta = variablesMeta.get('resolvesVariablesInvalid1'); + expect(valueMeta).to.not.have.property('variables'); + expect(valueMeta.error.code).to.equal('VARIABLE_RESOLUTION_ERROR'); + + valueMeta = variablesMeta.get('resolvesVariablesInvalid2'); + expect(valueMeta).to.not.have.property('variables'); + expect(valueMeta.error.code).to.equal('VARIABLE_RESOLUTION_ERROR'); + }); + }); }); describe('Partial resolution', () => { diff --git a/test/unit/lib/configuration/variables/sources/file.test.js b/test/unit/lib/configuration/variables/sources/file.test.js index d568a39be49..2ee882d53e4 100644 --- a/test/unit/lib/configuration/variables/sources/file.test.js +++ b/test/unit/lib/configuration/variables/sources/file.test.js @@ -23,6 +23,8 @@ describe('test/unit/lib/configuration/variables/sources/file.test.js', () => { jsPropertyFunction: '${file(file-property-function.js):property}', jsPropertyFunctionProperty: '${file(file-property-function.js):property.result}', addressSupport: '${file(file.json):result}', + jsFunctionResolveVariable: '${file(file-function-variable.js)}', + jsPropertyFunctionResolveVariable: '${file(file-property-function-variable.js):property}', nonExistingYaml: '${file(not-existing.yaml), null}', nonExistingJson: '${file(not-existing.json), null}', nonExistingJs: '${file(not-existing.js), null}', @@ -89,6 +91,15 @@ describe('test/unit/lib/configuration/variables/sources/file.test.js', () => { it('should support "address" argument', () => expect(configuration.addressSupport).to.equal('json')); + it('should support internal variable resolution', () => { + expect(configuration.jsFunctionResolveVariable).to.deep.equal({ + varResult: { result: 'yaml' }, + }); + expect(configuration.jsPropertyFunctionResolveVariable).to.deep.equal({ + varResult: { result: 'json' }, + }); + }); + it('should uncoditionally split "address" property keys by "."', () => expect(configuration.ambiguousAddress).to.equal('object')); diff --git a/test/unit/lib/configuration/variables/sources/fixture/file-function-variable.js b/test/unit/lib/configuration/variables/sources/fixture/file-function-variable.js new file mode 100644 index 00000000000..3e4621d5310 --- /dev/null +++ b/test/unit/lib/configuration/variables/sources/fixture/file-function-variable.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = async ({ resolveVariable }) => ({ + varResult: await resolveVariable('file(file.yaml)'), +}); diff --git a/test/unit/lib/configuration/variables/sources/fixture/file-property-function-variable.js b/test/unit/lib/configuration/variables/sources/fixture/file-property-function-variable.js new file mode 100644 index 00000000000..13988c06e22 --- /dev/null +++ b/test/unit/lib/configuration/variables/sources/fixture/file-property-function-variable.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports.property = async ({ resolveVariable }) => ({ + varResult: await resolveVariable('file(file.json)'), +}); From 78a4a96d74848799b13d273eb25956f4518f5099 Mon Sep 17 00:00:00 2001 From: Piotr Grzesik Date: Tue, 27 Apr 2021 11:38:10 +0200 Subject: [PATCH 0019/1101] refactor(Telemetry): Add `isTelemetryReportedExternally` flag --- lib/Serverless.js | 1 + scripts/serverless.js | 1 + 2 files changed, 2 insertions(+) diff --git a/lib/Serverless.js b/lib/Serverless.js index defd63c6dfc..768c06f53e4 100644 --- a/lib/Serverless.js +++ b/lib/Serverless.js @@ -95,6 +95,7 @@ class Serverless { this.processedInput = { commands, options }; } this.hasResolvedCommandsExternally = Boolean(configObject.hasResolvedCommandsExternally); + this.isTelemetryReportedExternally = Boolean(configObject.isTelemetryReportedExternally); // Due to design flaw properties of configObject (which is to be merged onto `this.config`) // also are subject to variables resolution. diff --git a/scripts/serverless.js b/scripts/serverless.js index 44ffa54e85c..5f59ee29954 100755 --- a/scripts/serverless.js +++ b/scripts/serverless.js @@ -369,6 +369,7 @@ const processSpanPromise = (async () => { isConfigurationResolved: commands[0] === 'plugin' || Boolean(variablesMeta && !variablesMeta.size), hasResolvedCommandsExternally: true, + isTelemetryReportedExternally: true, commands, options, }); From fac47cfa23c6e0573b0baa9b379d7226654c4dbd Mon Sep 17 00:00:00 2001 From: Piotr Grzesik Date: Tue, 27 Apr 2021 11:41:52 +0200 Subject: [PATCH 0020/1101] refactor(Telemetry): Only process in `PluginManager` if flag missing --- lib/classes/PluginManager.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/classes/PluginManager.js b/lib/classes/PluginManager.js index efdf0ee5dc9..1efd5a8002c 100644 --- a/lib/classes/PluginManager.js +++ b/lib/classes/PluginManager.js @@ -598,12 +598,15 @@ class PluginManager { await hook(); } - await storeTelemetryLocally(await generateTelemetryPayload(this.serverless)); let deferredBackendNotificationRequest; - if (commandsArray.join(' ') === 'deploy') { - deferredBackendNotificationRequest = sendTelemetry({ - serverlessExecutionSpan: this.serverless.onExitPromise, - }); + + if (!this.serverless.isTelemetryReportedExternally) { + await storeTelemetryLocally(await generateTelemetryPayload(this.serverless)); + if (commandsArray.join(' ') === 'deploy') { + deferredBackendNotificationRequest = sendTelemetry({ + serverlessExecutionSpan: this.serverless.onExitPromise, + }); + } } try { From 4cab803ccda05bb3834c0ec4df483837ff84802e Mon Sep 17 00:00:00 2001 From: Piotr Grzesik Date: Tue, 27 Apr 2021 11:54:52 +0200 Subject: [PATCH 0021/1101] refactor(Telemetry): Move telemetry handling to `scripts/serverless.js` --- scripts/serverless.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/scripts/serverless.js b/scripts/serverless.js index 5f59ee29954..f0296615510 100755 --- a/scripts/serverless.js +++ b/scripts/serverless.js @@ -16,6 +16,13 @@ if (require('../lib/utils/tabCompletion/isSupported') && process.argv[2] === 'co const handleError = require('../lib/cli/handle-error'); const humanizePropertyPathKeys = require('../lib/configuration/variables/humanize-property-path-keys'); +const { + storeLocally: storeTelemetryLocally, + send: sendTelemetry, +} = require('../lib/utils/telemetry'); +const generateTelemetryPayload = require('../lib/utils/telemetry/generatePayload'); +const processBackendNotificationRequest = require('../lib/utils/processBackendNotificationRequest'); + let serverless; process.once('uncaughtException', (error) => @@ -634,6 +641,16 @@ const processSpanPromise = (async () => { // Run command await serverless.run(); } + await storeTelemetryLocally(await generateTelemetryPayload(serverless)); + let backendNotificationRequest; + if (commands.join(' ') === 'deploy') { + backendNotificationRequest = await sendTelemetry({ + serverlessExecutionSpan: processSpanPromise, + }); + } + if (backendNotificationRequest) { + await processBackendNotificationRequest(backendNotificationRequest); + } } catch (error) { // If Dashboard Plugin, capture error const dashboardPlugin = From 98e6e1eaf7a58794f51843f75e08fd4a7ceb261a Mon Sep 17 00:00:00 2001 From: Piotr Grzesik Date: Tue, 27 Apr 2021 12:01:14 +0200 Subject: [PATCH 0022/1101] refactor(Telemetry): Ensure local serverless will not run telemetry --- scripts/serverless.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/serverless.js b/scripts/serverless.js index f0296615510..c0a240ac666 100755 --- a/scripts/serverless.js +++ b/scripts/serverless.js @@ -384,6 +384,11 @@ const processSpanPromise = (async () => { try { serverless.onExitPromise = processSpanPromise; serverless.invocationId = uuid.v4(); + // TODO: REMOVE WITH NEXT MAJOR + // The purpose of below logic is to ensure that locally resolved `serverless` instance + // that might be in lower version will not attempt to handle telemetry on its own + require('../lib/utils/telemetry/areDisabled'); // Ensure value is resolved + process.env.SLS_TRACKING_DISABLED = '1'; await serverless.init(); if (serverless.invokedInstance) { // Local (in service) installation was found and initialized internally, From e1b9ba46bda28db101f61a53b46a66ab195ad810 Mon Sep 17 00:00:00 2001 From: Piotr Grzesik Date: Tue, 27 Apr 2021 12:35:01 +0200 Subject: [PATCH 0023/1101] refactor(Telemetry): Do not generate payload if telemetry disabled --- lib/Serverless.js | 1 + lib/classes/PluginManager.js | 5 ++++- scripts/serverless.js | 23 +++++++++++++---------- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/lib/Serverless.js b/lib/Serverless.js index 768c06f53e4..679c9806944 100644 --- a/lib/Serverless.js +++ b/lib/Serverless.js @@ -108,6 +108,7 @@ class Serverless { delete configObject.commands; delete configObject.isConfigurationResolved; delete configObject.hasResolvedCommandsExternally; + delete configObject.isTelemetryReportedExternally; delete configObject.options; this.providers = {}; diff --git a/lib/classes/PluginManager.js b/lib/classes/PluginManager.js index 1efd5a8002c..f7d3f885386 100644 --- a/lib/classes/PluginManager.js +++ b/lib/classes/PluginManager.js @@ -15,6 +15,7 @@ const renderCommandHelp = require('../cli/render-help/command'); const { storeLocally: storeTelemetryLocally, send: sendTelemetry } = require('../utils/telemetry'); const generateTelemetryPayload = require('../utils/telemetry/generatePayload'); const processBackendNotificationRequest = require('../utils/processBackendNotificationRequest'); +const isTelemetryDisabled = require('../utils/telemetry/areDisabled'); const requireServicePlugin = (serviceDir, pluginPath, localPluginsPath) => { if (localPluginsPath && !pluginPath.startsWith('./')) { @@ -600,7 +601,9 @@ class PluginManager { let deferredBackendNotificationRequest; - if (!this.serverless.isTelemetryReportedExternally) { + // TODO: Remove this logic with `v3.0.0` along with removal of `isTelemetryReportedExternally` + // After we are ensured that local fallback from `v3` will always fall back to `v3` local installation + if (!this.serverless.isTelemetryReportedExternally && !isTelemetryDisabled) { await storeTelemetryLocally(await generateTelemetryPayload(this.serverless)); if (commandsArray.join(' ') === 'deploy') { deferredBackendNotificationRequest = sendTelemetry({ diff --git a/scripts/serverless.js b/scripts/serverless.js index c0a240ac666..1212c957bb7 100755 --- a/scripts/serverless.js +++ b/scripts/serverless.js @@ -22,6 +22,7 @@ const { } = require('../lib/utils/telemetry'); const generateTelemetryPayload = require('../lib/utils/telemetry/generatePayload'); const processBackendNotificationRequest = require('../lib/utils/processBackendNotificationRequest'); +const isTelemetryDisabled = require('../lib/utils/telemetry/areDisabled'); let serverless; @@ -387,7 +388,6 @@ const processSpanPromise = (async () => { // TODO: REMOVE WITH NEXT MAJOR // The purpose of below logic is to ensure that locally resolved `serverless` instance // that might be in lower version will not attempt to handle telemetry on its own - require('../lib/utils/telemetry/areDisabled'); // Ensure value is resolved process.env.SLS_TRACKING_DISABLED = '1'; await serverless.init(); if (serverless.invokedInstance) { @@ -646,15 +646,18 @@ const processSpanPromise = (async () => { // Run command await serverless.run(); } - await storeTelemetryLocally(await generateTelemetryPayload(serverless)); - let backendNotificationRequest; - if (commands.join(' ') === 'deploy') { - backendNotificationRequest = await sendTelemetry({ - serverlessExecutionSpan: processSpanPromise, - }); - } - if (backendNotificationRequest) { - await processBackendNotificationRequest(backendNotificationRequest); + + if (!isTelemetryDisabled) { + await storeTelemetryLocally(await generateTelemetryPayload(serverless)); + let backendNotificationRequest; + if (commands.join(' ') === 'deploy') { + backendNotificationRequest = await sendTelemetry({ + serverlessExecutionSpan: processSpanPromise, + }); + } + if (backendNotificationRequest) { + await processBackendNotificationRequest(backendNotificationRequest); + } } } catch (error) { // If Dashboard Plugin, capture error From 11e7311032824d357888ef615596c40ec1bbf66c Mon Sep 17 00:00:00 2001 From: Piotr Grzesik Date: Tue, 27 Apr 2021 15:36:23 +0200 Subject: [PATCH 0024/1101] refactor: Introduce `is-using-local-installation` util --- lib/cli/handle-error.js | 5 ++--- lib/utils/is-locally-installed.js | 11 +++++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 lib/utils/is-locally-installed.js diff --git a/lib/cli/handle-error.js b/lib/cli/handle-error.js index 66a0c88145d..89b11f14d98 100644 --- a/lib/cli/handle-error.js +++ b/lib/cli/handle-error.js @@ -10,8 +10,7 @@ const sfeVersion = require('@serverless/enterprise-plugin/package.json').version const { platformClientVersion } = require('@serverless/enterprise-plugin'); const ServerlessError = require('../serverless-error'); const tokenizeException = require('../utils/tokenize-exception'); - -const serverlessPath = path.resolve(__dirname, '../Serverless.js'); +const resolveIsLocallyInstalled = require('../utils/is-locally-installed'); const consoleLog = (message) => process.stdout.write(`${message}\n`); @@ -103,7 +102,7 @@ module.exports = async (exception, options = {}) => { const installationModePostfix = await (async () => { if (isStandaloneExecutable) return ' (standalone)'; if (isLocallyInstalled != null) return isLocallyInstalled ? ' (local)' : ''; - return serverlessPath === (await resolveLocalServerlessPath()) ? ' (local)' : ''; + return (await resolveIsLocallyInstalled()) ? ' (local)' : ''; })(); consoleLog( chalk.yellow(` Framework Version: ${slsVersion}${installationModePostfix}`) diff --git a/lib/utils/is-locally-installed.js b/lib/utils/is-locally-installed.js new file mode 100644 index 00000000000..697a4eb158a --- /dev/null +++ b/lib/utils/is-locally-installed.js @@ -0,0 +1,11 @@ +'use strict'; + +const path = require('path'); +const resolveLocalServerlessPath = require('../cli/resolve-local-serverless-path'); + +const serverlessPath = path.resolve(__dirname, '../Serverless.js'); + +module.exports = async () => { + const localServerlessPath = await resolveLocalServerlessPath(); + return serverlessPath === localServerlessPath; +}; From ba5f207d0ae83717567a9dad2e74af33eeec905f Mon Sep 17 00:00:00 2001 From: Piotr Grzesik Date: Tue, 27 Apr 2021 17:31:31 +0200 Subject: [PATCH 0025/1101] refactor(Telemetry): Make `serverless` optional in `generatePayload` --- lib/utils/telemetry/generatePayload.js | 25 +++++++++++---- .../utils/telemetry/generatePayload.test.js | 32 +++++++++++++++++++ 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/lib/utils/telemetry/generatePayload.js b/lib/utils/telemetry/generatePayload.js index 00d628bda84..c1b8524d2f4 100644 --- a/lib/utils/telemetry/generatePayload.js +++ b/lib/utils/telemetry/generatePayload.js @@ -8,6 +8,8 @@ const isStandalone = require('../isStandaloneExecutable'); const { triggeredDeprecations } = require('../logDeprecation'); const isNpmGlobal = require('../npmPackage/isGlobal'); const noServiceCommands = require('../../cli/commands-schema/no-service'); +const resolveInput = require('../../cli/resolve-input'); +const resolveIsLocallyInstalled = require('../../utils/is-locally-installed'); const ci = require('ci-info'); const versions = { @@ -26,11 +28,12 @@ const checkIsTabAutocompletionInstalled = async () => { }; const shouldIncludeServiceSpecificConfig = (serverless) => { - if (!serverless.serviceDir) { + if (!serverless || !serverless.serviceDir) { return false; } - const noServiceCommand = noServiceCommands.get(serverless.processedInput.commands.join(' ')); + const { command } = resolveInput(); + const noServiceCommand = noServiceCommands.get(command); if (noServiceCommand && !noServiceCommand.serviceDependencyMode) { return false; } @@ -72,7 +75,7 @@ const getServiceConfig = (serverless) => { }; }; -module.exports = async (serverless) => { +module.exports = async (serverless = null) => { let timezone; try { timezone = new Intl.DateTimeFormat().resolvedOptions().timeZone; @@ -108,10 +111,20 @@ module.exports = async (serverless) => { return userConfig.get('userId'); })(); + const isLocallyInstalled = await (async () => { + if (serverless) { + return serverless.isLocallyInstalled; + } + + return resolveIsLocallyInstalled(); + })(); + + const { command } = resolveInput(); + const payload = { ciName, cliName: 'serverless', - command: serverless.processedInput.commands.join(' '), + command, dashboard: { userId, }, @@ -122,10 +135,10 @@ module.exports = async (serverless) => { if (process.platform === 'win32') return 'global:standalone:windows'; return 'global:standalone:other'; } - if (!serverless.isLocallyInstalled) { + if (!isLocallyInstalled) { return (await isNpmGlobal()) ? 'global:npm' : 'global:other'; } - if (serverless._isInvokedByGlobalInstallation) return 'local:fallback'; + if (serverless && serverless._isInvokedByGlobalInstallation) return 'local:fallback'; return 'local:direct'; })(), isAutoUpdateEnabled: Boolean(userConfig.get('autoUpdate.enabled')), diff --git a/test/unit/lib/utils/telemetry/generatePayload.test.js b/test/unit/lib/utils/telemetry/generatePayload.test.js index 74a24e7ef03..b032c5dad3e 100644 --- a/test/unit/lib/utils/telemetry/generatePayload.test.js +++ b/test/unit/lib/utils/telemetry/generatePayload.test.js @@ -253,6 +253,38 @@ describe('lib/utils/telemetry/generatePayload', () => { }); }); + it('Should correctly resolve payload with missing `serverless` instance', async () => { + // Run serverless in order to ensure command resolution + await runServerless({ + fixture: 'aws', + command: 'print', + }); + + const payload = await generatePayload(); + + expect(payload).to.have.property('frameworkLocalUserId'); + delete payload.frameworkLocalUserId; + expect(payload).to.have.property('firstLocalInstallationTimestamp'); + delete payload.firstLocalInstallationTimestamp; + expect(payload).to.have.property('timestamp'); + delete payload.timestamp; + expect(payload).to.have.property('dashboard'); + delete payload.dashboard; + expect(payload).to.have.property('timezone'); + delete payload.timezone; + expect(payload).to.have.property('ciName'); + delete payload.ciName; + expect(payload).to.deep.equal({ + cliName: 'serverless', + command: 'print', + isAutoUpdateEnabled: false, + isTabAutocompletionInstalled: false, + triggeredDeprecations: [], + installationType: 'global:other', + versions, + }); + }); + it('Should resolve payload with predefined local config', async () => { const { serverless } = await runServerless({ fixture: 'customProvider', From 543423d869ba35c6866506bb49a8642700214b3a Mon Sep 17 00:00:00 2001 From: YErii Date: Tue, 27 Apr 2021 23:47:59 +0800 Subject: [PATCH 0026/1101] feat(Templates): Update dependencies for `cloudflare` template (#9373) --- .../create/templates/cloudflare-workers-enterprise/package.json | 2 +- lib/plugins/create/templates/cloudflare-workers/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/plugins/create/templates/cloudflare-workers-enterprise/package.json b/lib/plugins/create/templates/cloudflare-workers-enterprise/package.json index e8490dae4b8..c76d334292d 100644 --- a/lib/plugins/create/templates/cloudflare-workers-enterprise/package.json +++ b/lib/plugins/create/templates/cloudflare-workers-enterprise/package.json @@ -9,6 +9,6 @@ "author": "cloudflare", "license": "MIT", "devDependencies": { - "serverless-cloudflare-workers": "1.0.5" + "serverless-cloudflare-workers": "1.2.0" } } diff --git a/lib/plugins/create/templates/cloudflare-workers/package.json b/lib/plugins/create/templates/cloudflare-workers/package.json index cff6760a3ca..da5289f9552 100644 --- a/lib/plugins/create/templates/cloudflare-workers/package.json +++ b/lib/plugins/create/templates/cloudflare-workers/package.json @@ -9,6 +9,6 @@ "author": "cloudflare", "license": "MIT", "devDependencies": { - "serverless-cloudflare-workers": "1.0.5" + "serverless-cloudflare-workers": "1.2.0" } } From ed3d75dafd9b8721ac27e1e84e3cd2d0cd281d96 Mon Sep 17 00:00:00 2001 From: AlinoeDoctari <75314813+AlinoeDoctari@users.noreply.github.com> Date: Tue, 27 Apr 2021 17:56:18 +0200 Subject: [PATCH 0027/1101] test(AWS Deploy): Add integration tests for custom deployment bucket --- .../custom-deployment-bucket.test.js | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 test/integration/custom-deployment-bucket.test.js diff --git a/test/integration/custom-deployment-bucket.test.js b/test/integration/custom-deployment-bucket.test.js new file mode 100644 index 00000000000..befc4dc658d --- /dev/null +++ b/test/integration/custom-deployment-bucket.test.js @@ -0,0 +1,36 @@ +'use strict'; + +const uuid = require('uuid'); +const { expect } = require('chai'); +const fixtures = require('../fixtures/programmatic'); +const awsRequest = require('@serverless/test/aws-request'); +const { deployService, removeService } = require('../utils/integration'); +const { createBucket, deleteBucket } = require('../utils/s3'); + +describe('Base AWS provider test', function () { + this.timeout(1000 * 60 * 10); + + const bucketName = `serverless-test-${uuid.v4()}`; + let serviceDir; + + before(async () => { + await createBucket(bucketName); + ({ servicePath: serviceDir } = await fixtures.setup('function', { + configExt: { provider: { deploymentBucket: { name: bucketName } } }, + })); + await deployService(serviceDir); + }); + + it('should deploy in the configured aws bucket', async () => { + // we cannot deploy an empty fixture like aws so we go for a small one + const res = await awsRequest('S3', 'listObjects', { Bucket: bucketName }); + expect( + res.Contents.filter((obj) => /compiled-cloudformation-template.json$/.test(obj.Key)).length + ).to.equal(1); + }); + + after(async () => { + await removeService(serviceDir); + await deleteBucket(bucketName); + }); +}); From 07a5483bb6e989aa5d29b0bba03fd2160be28fb7 Mon Sep 17 00:00:00 2001 From: Piotr Grzesik Date: Tue, 27 Apr 2021 21:09:06 +0200 Subject: [PATCH 0028/1101] refactor(CLI): Pass `serverless` instance to `handle-error` --- lib/cli/handle-error.js | 18 ++++++++++++++++-- scripts/serverless.js | 6 ++---- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/lib/cli/handle-error.js b/lib/cli/handle-error.js index 89b11f14d98..75241782bde 100644 --- a/lib/cli/handle-error.js +++ b/lib/cli/handle-error.js @@ -33,7 +33,20 @@ const writeMessage = (title, message) => { module.exports = async (exception, options = {}) => { if (!isObject(options)) options = {}; - const { isUncaughtException, isLocallyInstalled, isInvokedByGlobalInstallation } = options; + // Due to the fact that the handler can be invoked via fallback, we need to support both `serverless` + // and `isLocallyInstalled` + `isInvokedByGlobalInstallation` properties + // TODO: Support for these properties should be removed with next major + const { + isUncaughtException, + isLocallyInstalled: passedIsLocallyInstalled, + isInvokedByGlobalInstallation: passedIsInvokedByGlobalInstallation, + serverless, + } = options; + + const isLocallyInstalled = serverless ? serverless.isLocallyInstalled : passedIsLocallyInstalled; + const isInvokedByGlobalInstallation = serverless + ? serverless._isInvokedByGlobalInstallation + : passedIsInvokedByGlobalInstallation; if (isInvokedByGlobalInstallation) { const localServerlessPath = await resolveLocalServerlessPath(); @@ -43,6 +56,7 @@ module.exports = async (exception, options = {}) => { // TODO: Remove local version fallback with next major (when its moved to the top of the process) try { require(path.resolve(localServerlessPath, '../cli/handle-error'))(exception, { + serverless, isLocallyInstalled, isUncaughtException, }); @@ -52,7 +66,7 @@ module.exports = async (exception, options = {}) => { // Ugly mock of serverless below is used to ensure that Framework version will be logged with `(local)` suffix require(path.resolve(localServerlessPath, '../classes/Error')).logError(exception, { - serverless: { isLocallyInstalled }, + serverless: serverless || { isLocallyInstalled }, forceExit: isUncaughtException, }); return; diff --git a/scripts/serverless.js b/scripts/serverless.js index 1212c957bb7..5f45acc7758 100755 --- a/scripts/serverless.js +++ b/scripts/serverless.js @@ -29,8 +29,7 @@ let serverless; process.once('uncaughtException', (error) => handleError(error, { isUncaughtException: true, - isLocallyInstalled: serverless && serverless.isLocallyInstalled, - isInvokedByGlobalInstallation: serverless && serverless.isInvokedByGlobalInstallation, + serverless, }) ); @@ -685,8 +684,7 @@ const processSpanPromise = (async () => { } } catch (error) { handleError(error, { - isLocallyInstalled: serverless && serverless.isLocallyInstalled, - isInvokedByGlobalInstallation: serverless && serverless.isInvokedByGlobalInstallation, + serverless, }); } })(); From a05e88d92e010ddfe019d5b5b873547b7d187d6d Mon Sep 17 00:00:00 2001 From: Piotr Grzesik Date: Wed, 28 Apr 2021 11:22:16 +0200 Subject: [PATCH 0029/1101] fix(AWS API Gateway): Ensure unique name for request validator --- .../events/apiGateway/lib/requestValidator.js | 4 +++- .../apiGateway/lib/requestValidator.test.js | 15 ++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/requestValidator.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/requestValidator.js index 690c539410a..ae20a516a27 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/requestValidator.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/requestValidator.js @@ -177,7 +177,9 @@ module.exports = { createRequestValidator() { const validatorLogicalId = this.provider.naming.getValidatorLogicalId(); - const validatorName = 'Validate request body and querystring parameters'; + const validatorName = `${ + this.serverless.service.service + }-${this.provider.getStage()} | Validate request body and querystring parameters`; this.serverless.service.provider.compiledCloudFormationTemplate.Resources[ validatorLogicalId ] = { diff --git a/test/unit/lib/plugins/aws/package/compile/events/apiGateway/lib/requestValidator.test.js b/test/unit/lib/plugins/aws/package/compile/events/apiGateway/lib/requestValidator.test.js index 0c0d6b2fbb2..d9e7f439ea5 100644 --- a/test/unit/lib/plugins/aws/package/compile/events/apiGateway/lib/requestValidator.test.js +++ b/test/unit/lib/plugins/aws/package/compile/events/apiGateway/lib/requestValidator.test.js @@ -9,14 +9,18 @@ chai.use(require('chai-as-promised')); describe('#compileRequestValidators()', () => { let cfResources; let naming; + let serviceName; + let stage; before(async () => { - const { cfTemplate, awsNaming } = await runServerless({ + const { cfTemplate, awsNaming, serverless } = await runServerless({ fixture: 'requestSchema', command: 'package', }); cfResources = cfTemplate.Resources; naming = awsNaming; + serviceName = serverless.service.service; + stage = serverless.getProvider('aws').getStage(); }); describe(' reusable schemas ', () => { @@ -331,6 +335,15 @@ describe('#compileRequestValidators()', () => { }); }); + it('should create validator with that includes `service` and `stage`', () => { + const validatorLogicalId = naming.getValidatorLogicalId(); + const validatorResource = cfResources[validatorLogicalId]; + + expect(validatorResource.Properties.Name).to.equal( + `${serviceName}-${stage} | Validate request body and querystring parameters` + ); + }); + it('should support multiple request:schema property for regression', () => { const modelJsonLogicalId = naming.getEndpointModelLogicalId( 'TestDashdeprecatedDashmultiple', From 72f952703b0a3ad92ca92c7180f940d7ab82cc4d Mon Sep 17 00:00:00 2001 From: Piotr Grzesik Date: Wed, 28 Apr 2021 12:20:44 +0200 Subject: [PATCH 0030/1101] test(Telemetry): Ensure telemetry is not emitted during tests --- test/mochaPatch.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/mochaPatch.js b/test/mochaPatch.js index 8bd417359a4..4e86c2c5b16 100644 --- a/test/mochaPatch.js +++ b/test/mochaPatch.js @@ -1,6 +1,5 @@ 'use strict'; -const path = require('path'); const fs = require('fs'); // Temporary patch to help tackle peekaboo error, by revelaing fuller stack trace for "fs" errors @@ -43,19 +42,19 @@ patchPromised('open'); patchCallback('readFile'); patchCallback('open'); -const disableServerlessStatsRequests = require('@serverless/test/disable-serverless-stats-requests'); const ensureArtifact = require('../lib/utils/ensureArtifact'); const resolveLocalServerless = require('../lib/cli/resolve-local-serverless-path'); const resolveInput = require('../lib/cli/resolve-input'); -disableServerlessStatsRequests(path.resolve(__dirname, '..')); - const BbPromise = require('bluebird'); BbPromise.config({ longStackTraces: true, }); +// In order to ensure that telemetry is not mistakenly emitted during test runs +process.env.SLS_TELEMETRY_DISABLED = '1'; + const { runnerEmitter } = require('@serverless/test/setup/patch'); runnerEmitter.on('runner', (runner) => { From d81730c219c70cd11d04a23ee217c4af3b320985 Mon Sep 17 00:00:00 2001 From: Piotr Grzesik Date: Wed, 28 Apr 2021 15:18:36 +0200 Subject: [PATCH 0031/1101] feat(Telemetry): Do not report telemetry for help commands --- scripts/serverless.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/serverless.js b/scripts/serverless.js index 5f45acc7758..fe7551934d4 100755 --- a/scripts/serverless.js +++ b/scripts/serverless.js @@ -646,7 +646,7 @@ const processSpanPromise = (async () => { await serverless.run(); } - if (!isTelemetryDisabled) { + if (!isTelemetryDisabled && !isHelpRequest) { await storeTelemetryLocally(await generateTelemetryPayload(serverless)); let backendNotificationRequest; if (commands.join(' ') === 'deploy') { From fd97e877cec6397cccf0e2bb2d1bfb5d71bdd798 Mon Sep 17 00:00:00 2001 From: Piotr Grzesik Date: Wed, 28 Apr 2021 15:27:17 +0200 Subject: [PATCH 0032/1101] refactor: Ensure that `options` are copied in Serverless constructor --- lib/Serverless.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/Serverless.js b/lib/Serverless.js index 679c9806944..ef0f66058ce 100644 --- a/lib/Serverless.js +++ b/lib/Serverless.js @@ -87,7 +87,10 @@ class Serverless { this._shouldReportMissingServiceDeprecation = true; } const commands = ensureArray(configObject.commands, { isOptional: true }); - const options = ensurePlainObject(configObject.options, { isOptional: true }); + let options = ensurePlainObject(configObject.options, { isOptional: true }); + // This is a temporary workaround to ensure that original `options` are not mutated + // Should be removed after addressing: https://github.com/serverless/serverless/issues/2582 + if (options) options = { ...options }; if (!commands || !options) { this._shouldReportCommandsDeprecation = true; this.processedInput = resolveCliInput(); From f5b2b9be395c9c2d3de4c4f91f991276bc22dc33 Mon Sep 17 00:00:00 2001 From: Piotr Grzesik Date: Wed, 28 Apr 2021 15:54:27 +0200 Subject: [PATCH 0033/1101] feat(Telemetry): Add `commandOptionNames` to payload --- lib/utils/telemetry/generatePayload.js | 7 +++++- .../utils/telemetry/generatePayload.test.js | 25 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/lib/utils/telemetry/generatePayload.js b/lib/utils/telemetry/generatePayload.js index c1b8524d2f4..c800b63cb78 100644 --- a/lib/utils/telemetry/generatePayload.js +++ b/lib/utils/telemetry/generatePayload.js @@ -119,12 +119,17 @@ module.exports = async (serverless = null) => { return resolveIsLocallyInstalled(); })(); - const { command } = resolveInput(); + const { command, options, commandSchema } = resolveInput(); + + // We only consider options that are present in command schema + const availableOptionNames = new Set(Object.keys(commandSchema.options)); + const commandOptionNames = Object.keys(options).filter((x) => availableOptionNames.has(x)); const payload = { ciName, cliName: 'serverless', command, + commandOptionNames, dashboard: { userId, }, diff --git a/test/unit/lib/utils/telemetry/generatePayload.test.js b/test/unit/lib/utils/telemetry/generatePayload.test.js index b032c5dad3e..ef5405046df 100644 --- a/test/unit/lib/utils/telemetry/generatePayload.test.js +++ b/test/unit/lib/utils/telemetry/generatePayload.test.js @@ -66,6 +66,7 @@ describe('lib/utils/telemetry/generatePayload', () => { expect(payload).to.deep.equal({ cliName: 'serverless', command: 'print', + commandOptionNames: [], config: { provider: { name: 'aws', @@ -113,6 +114,7 @@ describe('lib/utils/telemetry/generatePayload', () => { expect(payload).to.deep.equal({ cliName: 'serverless', command: 'print', + commandOptionNames: [], config: { provider: { name: 'customProvider', @@ -158,6 +160,7 @@ describe('lib/utils/telemetry/generatePayload', () => { expect(payload).to.deep.equal({ cliName: 'serverless', command: 'print', + commandOptionNames: [], config: { provider: { name: 'aws', @@ -199,6 +202,7 @@ describe('lib/utils/telemetry/generatePayload', () => { expect(payload).to.deep.equal({ cliName: 'serverless', command: 'config', + commandOptionNames: [], isAutoUpdateEnabled: false, isTabAutocompletionInstalled: false, triggeredDeprecations: [], @@ -228,6 +232,7 @@ describe('lib/utils/telemetry/generatePayload', () => { delete payload.ciName; expect(payload).to.deep.equal({ command: 'help', + commandOptionNames: [], cliName: 'serverless', config: { provider: { @@ -277,6 +282,7 @@ describe('lib/utils/telemetry/generatePayload', () => { expect(payload).to.deep.equal({ cliName: 'serverless', command: 'print', + commandOptionNames: [], isAutoUpdateEnabled: false, isTabAutocompletionInstalled: false, triggeredDeprecations: [], @@ -312,6 +318,7 @@ describe('lib/utils/telemetry/generatePayload', () => { const { serverless } = await runServerless({ fixture: 'customProvider', command: 'config', + commandOptionNames: [], }); await fs.promises.writeFile( @@ -349,6 +356,7 @@ describe('lib/utils/telemetry/generatePayload', () => { const { serverless } = await runServerless({ fixture: 'customProvider', command: 'config', + commandOptionNames: [], }); let payload; @@ -358,4 +366,21 @@ describe('lib/utils/telemetry/generatePayload', () => { }); expect(payload.ciName).to.equal('Seed'); }); + + it('Should correctly resolve `commandOptionNames` property', async () => { + const { serverless } = await runServerless({ + fixture: 'httpApi', + command: 'print', + options: { + region: 'eu-west-1', + format: 'json', + path: 'provider.name', + }, + }); + const payload = await generatePayload(serverless); + + expect(new Set(payload.commandOptionNames)).to.deep.equal( + new Set(['region', 'format', 'path']) + ); + }); }); From 95e063d904443044b73d61e73d04447b298d22ed Mon Sep 17 00:00:00 2001 From: Mariusz Nowak Date: Thu, 29 Apr 2021 12:39:47 +0200 Subject: [PATCH 0034/1101] docs: Remove not supported `--noDeploy` from docs --- docs/providers/aws/cli-reference/deploy.md | 1 - docs/providers/azure/cli-reference/deploy.md | 1 - docs/providers/kubeless/cli-reference/deploy.md | 1 - 3 files changed, 3 deletions(-) diff --git a/docs/providers/aws/cli-reference/deploy.md b/docs/providers/aws/cli-reference/deploy.md index 7a52946b012..23fda45ca63 100644 --- a/docs/providers/aws/cli-reference/deploy.md +++ b/docs/providers/aws/cli-reference/deploy.md @@ -23,7 +23,6 @@ serverless deploy ## Options - `--config` or `-c` Name of your configuration file, if other than `serverless.yml|.yaml|.js|.json`. -- `--noDeploy` or `-n` Skips the deployment steps and leaves artifacts in the `.serverless` directory - `--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/azure/cli-reference/deploy.md b/docs/providers/azure/cli-reference/deploy.md index 19e3657894b..b2099c42e4f 100644 --- a/docs/providers/azure/cli-reference/deploy.md +++ b/docs/providers/azure/cli-reference/deploy.md @@ -29,7 +29,6 @@ The `sls deploy apim` command will deploy API management as configured within `s - `--region` or `-r` - Specify region name - `--subscriptionId` or `-i` - Specify subscription ID - `--config` or `-c` Name of your configuration 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. ## Artifacts diff --git a/docs/providers/kubeless/cli-reference/deploy.md b/docs/providers/kubeless/cli-reference/deploy.md index ec43d318e07..38766ccad4c 100644 --- a/docs/providers/kubeless/cli-reference/deploy.md +++ b/docs/providers/kubeless/cli-reference/deploy.md @@ -27,7 +27,6 @@ This is the simplest deployment usage possible. With this command Serverless wil ## Options - `--config` or `-c` Name of your configuration 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). - `--function` or `-f` Invoke `deploy function` (see above). Convenience shortcut - cannot be used with `--package`. From 85b9e5319df48904664f966e988cc725116ce865 Mon Sep 17 00:00:00 2001 From: Mariusz Nowak Date: Thu, 29 Apr 2021 16:40:45 +0200 Subject: [PATCH 0035/1101] fix(Telemetry): Ensure that container commands do not trigger telemetry --- lib/cli/resolve-input.js | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/lib/cli/resolve-input.js b/lib/cli/resolve-input.js index c9f1f9ae8f8..1d41f45b1b3 100644 --- a/lib/cli/resolve-input.js +++ b/lib/cli/resolve-input.js @@ -73,6 +73,10 @@ module.exports = memoizee((commandsSchema = require('./commands-schema')) => { result.isContainerCommand = Array.from(commandsSchema.keys()).some((commandName) => commandName.startsWith(`${command} `) ); + if (result.isContainerCommand) { + result.isHelpRequest = true; + return result; + } } if ( @@ -82,18 +86,17 @@ module.exports = memoizee((commandsSchema = require('./commands-schema')) => { command === 'help' ) { result.isHelpRequest = true; + return result; } - if (!result.isHelpRequest) { - const argsString = args.join(' '); - if (command && argsString !== command && !argsString.startsWith(`${command} `)) { - // Some options were passed before command name (e.g. "sls -v deploy"), deprecate such usage - require('../utils/logDeprecation')( - 'CLI_OPTIONS_BEFORE_COMMAND', - '"serverless" command options are expected to follow command and not be put before the command.\n' + - 'Starting from next major Serverless will no longer support the latter form.' - ); - } + const argsString = args.join(' '); + if (command && argsString !== command && !argsString.startsWith(`${command} `)) { + // Some options were passed before command name (e.g. "sls -v deploy"), deprecate such usage + require('../utils/logDeprecation')( + 'CLI_OPTIONS_BEFORE_COMMAND', + '"serverless" command options are expected to follow command and not be put before the command.\n' + + 'Starting from next major Serverless will no longer support the latter form.' + ); } return result; From 360925d2e0cddb6fbbbb72ca47495aa71a43d1fc Mon Sep 17 00:00:00 2001 From: Stephen Date: Fri, 30 Apr 2021 07:33:32 +0000 Subject: [PATCH 0036/1101] fix(AWS S3): Fix parsing of the artifact S3 url (#9380) --- docs/providers/aws/guide/packaging.md | 8 +++-- lib/plugins/aws/package/compile/functions.js | 17 +++++----- lib/plugins/aws/utils/parse-s3-uri.js | 26 ++++++++++++++ .../plugins/aws/utils/parse-s3-uri.test.js | 34 +++++++++++++++++++ 4 files changed, 73 insertions(+), 12 deletions(-) create mode 100644 lib/plugins/aws/utils/parse-s3-uri.js create mode 100644 test/unit/lib/plugins/aws/utils/parse-s3-uri.test.js diff --git a/docs/providers/aws/guide/packaging.md b/docs/providers/aws/guide/packaging.md index 4c7953504a9..5d4cbde1ec9 100644 --- a/docs/providers/aws/guide/packaging.md +++ b/docs/providers/aws/guide/packaging.md @@ -119,7 +119,9 @@ functions: #### 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. +Artifacts can also be fetched from a remote S3 bucket. In this case you just need to provide the S3 object URI (old style or new) as the artifact value. This applies to both, service-wide and function-level artifact setups. + +**Note:** At this time, only S3 URIs are supported. Serverless does not yet support fetching artifacts from non-S3 remote locations. ##### Service package @@ -127,7 +129,7 @@ Artifacts can also be fetched from a remote S3 bucket. In this case you just nee service: my-service package: - artifact: https://s3.amazonaws.com/some-bucket/service-artifact.zip + artifact: s3://some-bucket/path/to/service-artifact.zip ``` ##### Individual function packages @@ -142,7 +144,7 @@ functions: hello: handler: com.serverless.Handler package: - artifact: https://s3.amazonaws.com/some-bucket/function-artifact.zip + artifact: s3://some-bucket/path/to/service-artifact.zip ``` ### Packaging functions separately diff --git a/lib/plugins/aws/package/compile/functions.js b/lib/plugins/aws/package/compile/functions.js index e259a986e5a..39c11a4ee87 100644 --- a/lib/plugins/aws/package/compile/functions.js +++ b/lib/plugins/aws/package/compile/functions.js @@ -9,6 +9,7 @@ const path = require('path'); const ServerlessError = require('../../../../serverless-error'); const deepSortObjectByKey = require('../../../../utils/deepSortObjectByKey'); const getHashForFilePath = require('../lib/getHashForFilePath'); +const parseS3URI = require('../../utils/parse-s3-uri'); class AwsCompileFunctions { constructor(serverless, options) { @@ -102,18 +103,16 @@ class AwsCompileFunctions { _.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; + const s3Object = parseS3URI(artifactFilePath); + if (!s3Object) return; + if (process.env.SLS_DEBUG) { + this.serverless.cli.log(`Downloading ${s3Object.Key} from bucket ${s3Object.Bucket}`); + } await new Promise((resolve, reject) => { const tmpDir = this.serverless.utils.getTmpDirPath(); - const filePath = path.join(tmpDir, match[2]); + const filePath = path.join(tmpDir, path.basename(s3Object.Key)); - const readStream = S3.getObject({ - Bucket: match[1], - Key: match[2], - }).createReadStream(); + const readStream = S3.getObject(s3Object).createReadStream(); const writeStream = fs.createWriteStream(filePath); readStream diff --git a/lib/plugins/aws/utils/parse-s3-uri.js b/lib/plugins/aws/utils/parse-s3-uri.js new file mode 100644 index 00000000000..ecb843c48c7 --- /dev/null +++ b/lib/plugins/aws/utils/parse-s3-uri.js @@ -0,0 +1,26 @@ +'use strict'; + +const patterns = [ + // S3 URI. Ex: s3://bucket/path/to/artifact.zip + new RegExp('^s3://([^/]+)/(.+)'), + + // New style S3 URL. Ex: https://bucket.s3.amazonaws.com/path/to/artifact.zip + new RegExp('([^/]+)\\.s3\\.amazonaws\\.com/(.+)'), + + // Old style S3 URL. Ex: https://s3.amazonaws.com/bucket/path/to/artifact.zip + new RegExp('s3\\.amazonaws\\.com/([^/]+)/(.+)'), +]; + +module.exports = (url) => { + for (const regex of patterns) { + const match = url.match(regex); + if (match) { + return { + Bucket: match[1], + Key: match[2], + }; + } + } + + return null; +}; diff --git a/test/unit/lib/plugins/aws/utils/parse-s3-uri.test.js b/test/unit/lib/plugins/aws/utils/parse-s3-uri.test.js new file mode 100644 index 00000000000..99aad0e3e10 --- /dev/null +++ b/test/unit/lib/plugins/aws/utils/parse-s3-uri.test.js @@ -0,0 +1,34 @@ +'use strict'; +const expect = require('chai').expect; +const parseS3URI = require('../../../../../../lib/plugins/aws/utils/parse-s3-uri'); + +describe('test/unit/lib/plugins/aws/utils/parse-s3-uri.test.js', () => { + it('should parse an S3 URI', () => { + const expected = { + Bucket: 'test-bucket', + Key: 'path/to/artifact.zip', + }; + const actual = parseS3URI('s3://test-bucket/path/to/artifact.zip'); + expect(actual).to.deep.equal(expected); + }); + it('should parse an old style S3 URL', () => { + const expected = { + Bucket: 'test-bucket', + Key: 'path/to/artifact.zip', + }; + const actual = parseS3URI('https://s3.amazonaws.com/test-bucket/path/to/artifact.zip'); + expect(actual).to.deep.equal(expected); + }); + it('should parse a new style S3 URL', () => { + const expected = { + Bucket: 'test-bucket', + Key: 'path/to/artifact.zip', + }; + const actual = parseS3URI('https://test-bucket.s3.amazonaws.com/path/to/artifact.zip'); + expect(actual).to.deep.equal(expected); + }); + it('should reject non S3 URLs', () => { + const actual = parseS3URI('https://example.com/path/to/artifact.zip'); + expect(actual).to.be.null; + }); +}); From f1a9a7b76032c312fd83c6dd5e1a332dfffa0b52 Mon Sep 17 00:00:00 2001 From: Piotr Grzesik Date: Fri, 30 Apr 2021 10:40:47 +0200 Subject: [PATCH 0037/1101] refactor(Telemetry): Add `anonymizeStacktracePaths` util --- .../telemetry/anonymize-stacktrace-paths.js | 23 +++ package.json | 1 + .../anonymize-stacktrace-paths.test.js | 132 ++++++++++++++++++ 3 files changed, 156 insertions(+) create mode 100644 lib/utils/telemetry/anonymize-stacktrace-paths.js create mode 100644 test/unit/lib/utils/telemetry/anonymize-stacktrace-paths.test.js diff --git a/lib/utils/telemetry/anonymize-stacktrace-paths.js b/lib/utils/telemetry/anonymize-stacktrace-paths.js new file mode 100644 index 00000000000..737fbe0035f --- /dev/null +++ b/lib/utils/telemetry/anonymize-stacktrace-paths.js @@ -0,0 +1,23 @@ +'use strict'; + +const path = require('path'); +const commonPath = require('path2/common'); + +const anonymizeStacktracePaths = (stacktracePaths) => { + let commonPathPrefix = commonPath(...stacktracePaths.filter((p) => path.isAbsolute(p))); + + const lastServerlessIndex = commonPathPrefix.lastIndexOf(`${path.sep}serverless${path.sep}`); + + if (lastServerlessIndex !== -1) { + commonPathPrefix = commonPathPrefix.slice(0, lastServerlessIndex); + } else { + const lastNodeModulesIndex = commonPathPrefix.lastIndexOf(`${path.sep}node_modules${path.sep}`); + if (lastNodeModulesIndex !== -1) { + commonPathPrefix = commonPathPrefix.slice(0, lastNodeModulesIndex); + } + } + + return stacktracePaths.map((s) => s.replace(commonPathPrefix, '')); +}; + +module.exports = anonymizeStacktracePaths; diff --git a/package.json b/package.json index a1aed905698..c972ca04f82 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "ncjsm": "^4.1.0", "node-fetch": "^2.6.1", "object-hash": "^2.1.1", + "path2": "^0.1.0", "promise-queue": "^2.2.5", "replaceall": "^0.1.6", "semver": "^7.3.5", diff --git a/test/unit/lib/utils/telemetry/anonymize-stacktrace-paths.test.js b/test/unit/lib/utils/telemetry/anonymize-stacktrace-paths.test.js new file mode 100644 index 00000000000..440e31b09b4 --- /dev/null +++ b/test/unit/lib/utils/telemetry/anonymize-stacktrace-paths.test.js @@ -0,0 +1,132 @@ +'use strict'; + +const expect = require('chai').expect; +const anonymizeStacktracePaths = require('../../../../../lib/utils/telemetry/anonymize-stacktrace-paths'); + +describe('test/unit/lib/utils/anonymize-stacktrace-paths.test.js', () => { + if (process.platform !== 'win32') { + it('Should remove common prefix up to last `serverless` occurence', () => { + const stacktracePaths = [ + '/home/xxx/serverless/yyy/zzz-serverless/serverless/lib/plugins/aws/package/lib/getHashForFilePath.js:23:13', + '/home/xxx/serverless/yyy/zzz-serverless/serverless/lib/plugins/otherfile.js:100:10', + '/home/xxx/serverless/yyy/zzz-serverless/serverless/lib/plugins/another.js:100:10', + ]; + + const result = anonymizeStacktracePaths(stacktracePaths); + expect(result).to.deep.equal([ + '/serverless/lib/plugins/aws/package/lib/getHashForFilePath.js:23:13', + '/serverless/lib/plugins/otherfile.js:100:10', + '/serverless/lib/plugins/another.js:100:10', + ]); + }); + + it('Should leave relative paths unaltered and do not consider them for common prefix', () => { + const stacktracePaths = [ + '/home/xxx/serverless/yyy/zzz-serverless/serverless/lib/plugins/aws/package/lib/getHashForFilePath.js:23:13', + '/home/xxx/serverless/yyy/zzz-serverless/serverless/lib/plugins/otherfile.js:100:10', + 'somefile.js:100:10', + 'another.js:100:10', + ]; + + const result = anonymizeStacktracePaths(stacktracePaths); + expect(result).to.deep.equal([ + '/serverless/lib/plugins/aws/package/lib/getHashForFilePath.js:23:13', + '/serverless/lib/plugins/otherfile.js:100:10', + 'somefile.js:100:10', + 'another.js:100:10', + ]); + }); + + it('Should remove common prefix if `/serverless/` or `/node_modules/` not found in path', () => { + const stacktracePaths = [ + '/home/xxx/yyy/zzz-serverless/lib/plugins/aws/package/lib/getHashForFilePath.js:23:13', + '/home/xxx/yyy/zzz-serverless/lib/plugins/otherfile.js:100:10', + '/home/xxx/yyy/zzz-serverless/lib/plugins/another.js:100:10', + ]; + + const result = anonymizeStacktracePaths(stacktracePaths); + expect(result).to.deep.equal([ + '/aws/package/lib/getHashForFilePath.js:23:13', + '/otherfile.js:100:10', + '/another.js:100:10', + ]); + }); + + it('Should remove common prefix up to last `node_modules` occurence if `serverless` not found', () => { + const stacktracePaths = [ + '/home/xxx/yyy/zzz-serverless/node_modules/lib/plugins/aws/package/lib/getHashForFilePath.js:23:13', + '/home/xxx/yyy/zzz-serverless/node_modules/lib/plugins/otherfile.js:100:10', + '/home/xxx/yyy/zzz-serverless/node_modules/lib/plugins/another.js:100:10', + ]; + + const result = anonymizeStacktracePaths(stacktracePaths); + expect(result).to.deep.equal([ + '/node_modules/lib/plugins/aws/package/lib/getHashForFilePath.js:23:13', + '/node_modules/lib/plugins/otherfile.js:100:10', + '/node_modules/lib/plugins/another.js:100:10', + ]); + }); + } + + if (process.platform === 'win32') { + it('Should remove common prefix up to last `serverless` occurence for windows-style paths', () => { + const stacktracePaths = [ + 'C:\\home\\xxx\\serverless\\yyy\\zzz-serverless\\serverless\\lib\\plugins\\aws\\package\\lib\\getHashForFilePath.js:23:13', + 'C:\\home\\xxx\\serverless\\yyy\\zzz-serverless\\serverless\\lib\\plugins\\otherfile.js:100:10', + 'C:\\home\\xxx\\serverless\\yyy\\zzz-serverless\\serverless\\lib\\plugins\\another.js:100:10', + ]; + + const result = anonymizeStacktracePaths(stacktracePaths); + expect(result).to.deep.equal([ + '\\serverless\\lib\\plugins\\aws\\package\\lib\\getHashForFilePath.js:23:13', + '\\serverless\\lib\\plugins\\otherfile.js:100:10', + '\\serverless\\lib\\plugins\\another.js:100:10', + ]); + }); + + it('Should remove common prefix up to last `serverless` occurence for windows-style paths', () => { + const stacktracePaths = [ + 'C:\\home\\xxx\\serverless\\yyy\\zzz-serverless\\serverless\\lib\\plugins\\aws\\package\\lib\\getHashForFilePath.js:23:13', + 'C:\\home\\xxx\\serverless\\yyy\\zzz-serverless\\serverless\\lib\\plugins\\otherfile.js:100:10', + 'another.js:100:10', + ]; + + const result = anonymizeStacktracePaths(stacktracePaths); + expect(result).to.deep.equal([ + '\\serverless\\lib\\plugins\\aws\\package\\lib\\getHashForFilePath.js:23:13', + '\\serverless\\lib\\plugins\\otherfile.js:100:10', + 'another.js:100:10', + ]); + }); + + it('Should remove common prefix if `\\serverless\\` not found in path', () => { + const stacktracePaths = [ + 'C:\\home\\xxx\\yyy\\zzz-serverless\\lib\\plugins\\aws\\package\\lib\\getHashForFilePath.js:23:13', + 'C:\\home\\xxx\\yyy\\zzz-serverless\\lib\\plugins\\otherfile.js:100:10', + 'C:\\home\\xxx\\yyy\\zzz-serverless\\lib\\plugins\\another.js:100:10', + ]; + + const result = anonymizeStacktracePaths(stacktracePaths); + expect(result).to.deep.equal([ + '\\aws\\package\\lib\\getHashForFilePath.js:23:13', + '\\otherfile.js:100:10', + '\\another.js:100:10', + ]); + }); + + it('Should remove common prefix up to last `node_modules` occurence if `serverless` not found', () => { + const stacktracePaths = [ + 'C:\\home\\xxx\\yyy\\zzz-serverless\\node_modules\\lib\\plugins\\aws\\package\\lib\\getHashForFilePath.js:23:13', + 'C:\\home\\xxx\\yyy\\zzz-serverless\\node_modules\\lib\\plugins\\otherfile.js:100:10', + 'C:\\home\\xxx\\yyy\\zzz-serverless\\node_modules\\lib\\plugins\\another.js:100:10', + ]; + + const result = anonymizeStacktracePaths(stacktracePaths); + expect(result).to.deep.equal([ + '\\node_modules\\lib\\plugins\\aws\\package\\lib\\getHashForFilePath.js:23:13', + '\\node_modules\\lib\\plugins\\otherfile.js:100:10', + '\\node_modules\\lib\\plugins\\another.js:100:10', + ]); + }); + } +}); From 8860397798398412d7232d9e4e0001a83efb0424 Mon Sep 17 00:00:00 2001 From: Piotr Grzesik Date: Fri, 30 Apr 2021 11:01:46 +0200 Subject: [PATCH 0038/1101] refactor(Telemetry): Add `resolveErrorLocation` util --- lib/utils/telemetry/resolve-error-location.js | 29 +++++++ lib/utils/tokenize-exception.js | 1 + .../telemetry/resolve-error-location.test.js | 78 +++++++++++++++++++ .../unit/lib/utils/tokenize-exception.test.js | 3 +- 4 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 lib/utils/telemetry/resolve-error-location.js create mode 100644 test/unit/lib/utils/telemetry/resolve-error-location.test.js diff --git a/lib/utils/telemetry/resolve-error-location.js b/lib/utils/telemetry/resolve-error-location.js new file mode 100644 index 00000000000..94779db85b0 --- /dev/null +++ b/lib/utils/telemetry/resolve-error-location.js @@ -0,0 +1,29 @@ +'use strict'; + +const anonymizeStacktracePaths = require('./anonymize-stacktrace-paths'); + +const resolveErrorLocation = (exceptionTokens) => { + if (!exceptionTokens.stack) return ''; + + const splittedStack = exceptionTokens.stack.split('\n'); + if (splittedStack.length === 1 && exceptionTokens.code) return ''; + + const stacktraceLineRegex = /\s*at.*\((.*)\).?/; + const stacktracePaths = []; + for (const line of splittedStack) { + const match = line.match(stacktraceLineRegex) || []; + const matchedPath = match[1]; + if (matchedPath) { + // Limited to maximum 5 lines in location + if (stacktracePaths.push(matchedPath) === 5) { + break; + } + } else if (stacktracePaths.length) break; + } + + if (!stacktracePaths.length) return ''; + + return anonymizeStacktracePaths(stacktracePaths).join('\n').replace(/\\/g, '/'); +}; + +module.exports = resolveErrorLocation; diff --git a/lib/utils/tokenize-exception.js b/lib/utils/tokenize-exception.js index 38282d3b23a..45122aa0f40 100644 --- a/lib/utils/tokenize-exception.js +++ b/lib/utils/tokenize-exception.js @@ -13,6 +13,7 @@ module.exports = (exception) => { stack: exception.stack, message: exception.message, isUserError: userErrorNames.has(exception.name), + code: exception.code, }; } return { diff --git a/test/unit/lib/utils/telemetry/resolve-error-location.test.js b/test/unit/lib/utils/telemetry/resolve-error-location.test.js new file mode 100644 index 00000000000..ceab86b564d --- /dev/null +++ b/test/unit/lib/utils/telemetry/resolve-error-location.test.js @@ -0,0 +1,78 @@ +'use strict'; + +const expect = require('chai').expect; + +const resolveErrorLocation = require('../../../../../lib/utils/telemetry/resolve-error-location'); +const tokenizeException = require('../../../../../lib/utils/tokenize-exception'); + +describe('test/unit/lib/utils/resolve-error-location.test.js', () => { + it('should be null when stack missing', () => { + const err = new Error('test'); + delete err.stack; + const result = resolveErrorLocation(tokenizeException(err)); + expect(result).to.equal(''); + }); + + it('should be null for error with code and one-line stacktrace', () => { + const err = new Error('test'); + err.code = 'ERR_CODE'; + err.stack = 'Oneline stacktrace'; + const result = resolveErrorLocation(tokenizeException(err)); + expect(result).to.equal(''); + }); + + it('should be null if no matching lines found', () => { + const err = new Error('test'); + err.stack = 'no matching\nlines in\nstacktrace'; + const result = resolveErrorLocation(tokenizeException(err)); + expect(result).to.equal(''); + }); + + if (process.platform !== 'win32') { + it('should return at most 5 lines', () => { + const err = new Error('test'); + err.stack = + 'Error:\n' + + ' at Context.it (/home/xxx/serverless/test/unit/lib/utils/resolve-error-location.test.js:10:17)\n' + + ' at callFn (/home/xxx/serverless/node_modules/mocha/lib/runnable.js:366:21)\n' + + ' at Test.Runnable.run (/home/xxx/serverless/node_modules/mocha/lib/runnable.js:354:5)\n' + + ' at Runner.runTest (/home/xxx/serverless/node_modules/mocha/lib/runner.js:677:10)\n' + + ' at next (/home/xxx/serverless/node_modules/mocha/lib/runner.js:801:12)\n' + + ' at next (/home/xxx/serverless/node_modules/mocha/lib/runner.js:594:14)\n'; + const result = resolveErrorLocation(tokenizeException(err)); + expect(result).to.equal( + [ + '/test/unit/lib/utils/resolve-error-location.test.js:10:17', + '/node_modules/mocha/lib/runnable.js:366:21', + '/node_modules/mocha/lib/runnable.js:354:5', + '/node_modules/mocha/lib/runner.js:677:10', + '/node_modules/mocha/lib/runner.js:801:12', + ].join('\n') + ); + }); + } + + if (process.platform === 'win32') { + it('should return at most 5 lines and use `/` path separator', () => { + const err = new Error('test'); + err.stack = + 'Error:\n' + + ' at Context.it (C:\\home\\xxx\\serverless\\test\\unit\\lib\\utils\\resolve-error-location.test.js:10:17)\n' + + ' at callFn (C:\\home\\xxx\\serverless\\node_modules\\mocha\\lib\\runnable.js:366:21)\n' + + ' at Test.Runnable.run (C:\\home\\xxx\\serverless\\node_modules\\mocha\\lib\\runnable.js:354:5)\n' + + ' at Runner.runTest (C:\\home\\xxx\\serverless\\node_modules\\mocha\\lib\\runner.js:677:10)\n' + + ' at next (C:\\home\\xxx\\serverless\\node_modules\\mocha\\lib\\runner.js:801:12)\n' + + ' at next (C:\\home\\xxx\\serverless\\node_modules\\mocha\\lib\\runner.js:594:14)\n'; + const result = resolveErrorLocation(tokenizeException(err)); + expect(result).to.equal( + [ + '/test/unit/lib/utils/resolve-error-location.test.js:10:17', + '/node_modules/mocha/lib/runnable.js:366:21', + '/node_modules/mocha/lib/runnable.js:354:5', + '/node_modules/mocha/lib/runner.js:677:10', + '/node_modules/mocha/lib/runner.js:801:12', + ].join('\n') + ); + }); + } +}); diff --git a/test/unit/lib/utils/tokenize-exception.test.js b/test/unit/lib/utils/tokenize-exception.test.js index 81d43d84b75..0e763f5da91 100644 --- a/test/unit/lib/utils/tokenize-exception.test.js +++ b/test/unit/lib/utils/tokenize-exception.test.js @@ -7,12 +7,13 @@ const tokenizeError = require('../../../../lib/utils/tokenize-exception'); describe('test/unit/lib/utils/tokenize-exception.test.js', () => { it('Should tokenize user error', () => { - const errorTokens = tokenizeError(new ServerlessError('Some error')); + const errorTokens = tokenizeError(new ServerlessError('Some error', 'ERR_CODE')); expect(errorTokens.title).to.equal('Serverless Error'); expect(errorTokens.name).to.equal('ServerlessError'); expect(errorTokens.stack).to.include('tokenize-exception.test.js:'); expect(errorTokens.message).to.equal('Some error'); expect(errorTokens.isUserError).to.equal(true); + expect(errorTokens.code).to.equal('ERR_CODE'); }); it('Should tokenize programmer error', () => { From 5861d08768a06e2e88609d0785ce590d3f693683 Mon Sep 17 00:00:00 2001 From: Piotr Grzesik Date: Fri, 30 Apr 2021 11:20:43 +0200 Subject: [PATCH 0039/1101] feat(Telemetry): Report failures via telemetry --- lib/cli/handle-error.js | 21 ++++ lib/utils/telemetry/resolve-error-location.js | 4 +- scripts/serverless.js | 8 +- test/unit/lib/cli/handle-error.test.js | 106 +++++++++++++++++- 4 files changed, 134 insertions(+), 5 deletions(-) diff --git a/lib/cli/handle-error.js b/lib/cli/handle-error.js index 75241782bde..e7f47940fd9 100644 --- a/lib/cli/handle-error.js +++ b/lib/cli/handle-error.js @@ -11,6 +11,10 @@ const { platformClientVersion } = require('@serverless/enterprise-plugin'); const ServerlessError = require('../serverless-error'); const tokenizeException = require('../utils/tokenize-exception'); const resolveIsLocallyInstalled = require('../utils/is-locally-installed'); +const isTelemetryDisabled = require('../utils/telemetry/areDisabled'); +const { storeLocally: storeTelemetryLocally, send: sendTelemetry } = require('../utils/telemetry'); +const generateTelemetryPayload = require('../utils/telemetry/generatePayload'); +const resolveErrorLocation = require('../utils/telemetry/resolve-error-location'); const consoleLog = (message) => process.stdout.write(`${message}\n`); @@ -41,6 +45,7 @@ module.exports = async (exception, options = {}) => { isLocallyInstalled: passedIsLocallyInstalled, isInvokedByGlobalInstallation: passedIsInvokedByGlobalInstallation, serverless, + hasTelemetryBeenReported, } = options; const isLocallyInstalled = serverless ? serverless.isLocallyInstalled : passedIsLocallyInstalled; @@ -59,6 +64,7 @@ module.exports = async (exception, options = {}) => { serverless, isLocallyInstalled, isUncaughtException, + hasTelemetryBeenReported, }); return; } catch (error) { @@ -134,6 +140,21 @@ module.exports = async (exception, options = {}) => { consoleLog(chalk.yellow(` Components Version: ${componentsVersion}`)); consoleLog(' '); + // If `hasTelemetryBeenReported` is not defined, we don't want to publish telemetry + if (!isTelemetryDisabled && hasTelemetryBeenReported === false) { + const telemetryPayload = await generateTelemetryPayload(serverless); + const failureReason = { + kind: isUserError ? 'user' : 'programmer', + code: exception.code, + }; + + if (!isUserError || !exception.code) { + failureReason.location = resolveErrorLocation(exceptionTokens); + } + await storeTelemetryLocally({ ...telemetryPayload, failureReason, outcome: 'failure' }); + await sendTelemetry(); + } + process.exitCode = 1; if (isUncaughtException) process.exit(); }; diff --git a/lib/utils/telemetry/resolve-error-location.js b/lib/utils/telemetry/resolve-error-location.js index 94779db85b0..55c6f077bdf 100644 --- a/lib/utils/telemetry/resolve-error-location.js +++ b/lib/utils/telemetry/resolve-error-location.js @@ -15,9 +15,7 @@ const resolveErrorLocation = (exceptionTokens) => { const matchedPath = match[1]; if (matchedPath) { // Limited to maximum 5 lines in location - if (stacktracePaths.push(matchedPath) === 5) { - break; - } + if (stacktracePaths.push(matchedPath) === 5) break; } else if (stacktracePaths.length) break; } diff --git a/scripts/serverless.js b/scripts/serverless.js index fe7551934d4..13830b280e9 100755 --- a/scripts/serverless.js +++ b/scripts/serverless.js @@ -26,10 +26,13 @@ const isTelemetryDisabled = require('../lib/utils/telemetry/areDisabled'); let serverless; +let hasTelemetryBeenReported = false; + process.once('uncaughtException', (error) => handleError(error, { isUncaughtException: true, serverless, + hasTelemetryBeenReported, }) ); @@ -647,7 +650,9 @@ const processSpanPromise = (async () => { } if (!isTelemetryDisabled && !isHelpRequest) { - await storeTelemetryLocally(await generateTelemetryPayload(serverless)); + hasTelemetryBeenReported = true; + const telemetryPayload = await generateTelemetryPayload(serverless); + await storeTelemetryLocally({ ...telemetryPayload, outcome: 'success' }); let backendNotificationRequest; if (commands.join(' ') === 'deploy') { backendNotificationRequest = await sendTelemetry({ @@ -685,6 +690,7 @@ const processSpanPromise = (async () => { } catch (error) { handleError(error, { serverless, + hasTelemetryBeenReported, }); } })(); diff --git a/test/unit/lib/cli/handle-error.test.js b/test/unit/lib/cli/handle-error.test.js index e689cac1ec5..403c4af4f76 100644 --- a/test/unit/lib/cli/handle-error.test.js +++ b/test/unit/lib/cli/handle-error.test.js @@ -1,6 +1,6 @@ 'use strict'; -const { expect } = require('chai'); +const chai = require('chai'); const sinon = require('sinon'); const path = require('path'); @@ -8,6 +8,11 @@ const overrideStdoutWrite = require('process-utils/override-stdout-write'); const handleError = require('../../../../lib/cli/handle-error'); const isStandaloneExecutable = require('../../../../lib/utils/isStandaloneExecutable'); const ServerlessError = require('../../../../lib/serverless-error'); +const proxyquire = require('proxyquire'); + +chai.use(require('sinon-chai')); + +const expect = chai.expect; describe('test/unit/lib/cli/handle-error.test.js', () => { it('should log environment information', async () => { @@ -95,4 +100,103 @@ describe('test/unit/lib/cli/handle-error.test.js', () => { ); expect(stdoutData).to.have.string('NON-ERROR'); }); + + describe('with mocked telemetry', () => { + const generateTelemetryPayloadStub = sinon.stub().resolves({}); + const storeTelemetryLocallyStub = sinon.stub(); + const sendTelemetryStub = sinon.stub(); + + const handleErrorWithMocks = proxyquire('../../../../lib/cli/handle-error', { + '../utils/telemetry/areDisabled': false, + '../utils/telemetry/generatePayload': generateTelemetryPayloadStub, + '../utils/telemetry/index': { + send: sendTelemetryStub, + storeLocally: storeTelemetryLocallyStub, + }, + }); + + beforeEach(() => { + sinon.resetHistory(); + }); + + it('should record telemetry only if `hasTelemetryBeenReported` is `false`', async () => { + // Override to avoid printing to stdout in tests + await overrideStdoutWrite( + () => {}, + () => + handleErrorWithMocks(new ServerlessError('Test error', 'ERR_CODE'), { + hasTelemetryBeenReported: false, + }) + ); + expect(generateTelemetryPayloadStub).to.be.calledOnce; + expect(storeTelemetryLocallyStub).to.be.calledOnce; + expect(sendTelemetryStub).to.be.calledOnce; + expect(storeTelemetryLocallyStub.getCall(0).args[0]).to.deep.equal({ + outcome: 'failure', + failureReason: { + code: 'ERR_CODE', + kind: 'user', + }, + }); + }); + + it('should add `location` to `failureReason` in telemetry if error code missing', async () => { + // Override to avoid printing to stdout in tests + await overrideStdoutWrite( + () => {}, + () => + handleErrorWithMocks(new ServerlessError('Test error'), { + hasTelemetryBeenReported: false, + }) + ); + expect(generateTelemetryPayloadStub).to.be.calledOnce; + expect(storeTelemetryLocallyStub).to.be.calledOnce; + expect(sendTelemetryStub).to.be.calledOnce; + expect(storeTelemetryLocallyStub.getCall(0).args[0].failureReason).to.have.property( + 'location' + ); + }); + + it('should add `location` to `failureReason` in telemetry for non-user errors', async () => { + // Override to avoid printing to stdout in tests + await overrideStdoutWrite( + () => {}, + () => + handleErrorWithMocks(new Error('Test error'), { + hasTelemetryBeenReported: false, + }) + ); + expect(generateTelemetryPayloadStub).to.be.calledOnce; + expect(storeTelemetryLocallyStub).to.be.calledOnce; + expect(sendTelemetryStub).to.be.calledOnce; + expect(storeTelemetryLocallyStub.getCall(0).args[0].failureReason).to.have.property( + 'location' + ); + }); + + it('should not record telemetry if `hasTelemetryBeenReported` is `true`', async () => { + // Override to avoid printing to stdout in tests + await overrideStdoutWrite( + () => {}, + () => + handleErrorWithMocks(new ServerlessError('Test error'), { + hasTelemetryBeenReported: true, + }) + ); + expect(generateTelemetryPayloadStub).not.to.be.called; + expect(storeTelemetryLocallyStub).not.to.be.called; + expect(sendTelemetryStub).not.to.be.called; + }); + + it('should not record telemetry if `hasTelemetryBeenReported` is not passed', async () => { + // Override to avoid printing to stdout in tests + await overrideStdoutWrite( + () => {}, + () => handleErrorWithMocks(new ServerlessError('Test error')) + ); + expect(generateTelemetryPayloadStub).not.to.be.called; + expect(storeTelemetryLocallyStub).not.to.be.called; + expect(sendTelemetryStub).not.to.be.called; + }); + }); }); From d647125ff5daa07972675fd28690d42746ab223b Mon Sep 17 00:00:00 2001 From: Piotr Grzesik Date: Thu, 29 Apr 2021 20:59:51 +0200 Subject: [PATCH 0040/1101] feat(Telemetry): Add `commandDurationMs` to payload --- bin/serverless.js | 3 +++ lib/utils/telemetry/generatePayload.js | 12 ++++++++++++ .../lib/utils/telemetry/generatePayload.test.js | 15 +++++++++++++++ 3 files changed, 30 insertions(+) diff --git a/bin/serverless.js b/bin/serverless.js index c7404e6d86e..607937045db 100755 --- a/bin/serverless.js +++ b/bin/serverless.js @@ -2,6 +2,9 @@ 'use strict'; +// `EvalError` is used to not pollute global namespace but still have the value accessible globally +EvalError.$serverlessCommandStartTime = process.hrtime(); + const nodeVersion = Number(process.version.split('.')[0].slice(1)); if (nodeVersion < 10) { diff --git a/lib/utils/telemetry/generatePayload.js b/lib/utils/telemetry/generatePayload.js index c800b63cb78..baf65f7108d 100644 --- a/lib/utils/telemetry/generatePayload.js +++ b/lib/utils/telemetry/generatePayload.js @@ -76,6 +76,14 @@ const getServiceConfig = (serverless) => { }; module.exports = async (serverless = null) => { + let commandDurationMs; + + if (EvalError.$serverlessCommandStartTime) { + const diff = process.hrtime(EvalError.$serverlessCommandStartTime); + // First element is in seconds and second in nanoseconds + commandDurationMs = Math.floor(diff[0] * 1000 + diff[1] / 1000000); + } + let timezone; try { timezone = new Intl.DateTimeFormat().resolvedOptions().timeZone; @@ -154,6 +162,10 @@ module.exports = async (serverless = null) => { versions, }; + if (commandDurationMs != null) { + payload.commandDurationMs = commandDurationMs; + } + if (shouldIncludeServiceSpecificConfig(serverless)) { const npmDependencies = (() => { const pkgJson = (() => { diff --git a/test/unit/lib/utils/telemetry/generatePayload.test.js b/test/unit/lib/utils/telemetry/generatePayload.test.js index ef5405046df..fc2af4df733 100644 --- a/test/unit/lib/utils/telemetry/generatePayload.test.js +++ b/test/unit/lib/utils/telemetry/generatePayload.test.js @@ -16,6 +16,9 @@ const versions = { }; describe('lib/utils/telemetry/generatePayload', () => { + // In order for tests below to return `commandDurationMs` + EvalError.$serverlessCommandStartTime = process.hrtime(); + it('Should resolve payload for AWS service', async () => { const { servicePath: serviceDir } = await fixtures.setup('httpApi', { configExt: { @@ -63,6 +66,8 @@ describe('lib/utils/telemetry/generatePayload', () => { delete payload.timezone; expect(payload).to.have.property('ciName'); delete payload.ciName; + expect(payload).to.have.property('commandDurationMs'); + delete payload.commandDurationMs; expect(payload).to.deep.equal({ cliName: 'serverless', command: 'print', @@ -111,6 +116,8 @@ describe('lib/utils/telemetry/generatePayload', () => { delete payload.timezone; expect(payload).to.have.property('ciName'); delete payload.ciName; + expect(payload).to.have.property('commandDurationMs'); + delete payload.commandDurationMs; expect(payload).to.deep.equal({ cliName: 'serverless', command: 'print', @@ -157,6 +164,8 @@ describe('lib/utils/telemetry/generatePayload', () => { delete payload.timezone; expect(payload).to.have.property('ciName'); delete payload.ciName; + expect(payload).to.have.property('commandDurationMs'); + delete payload.commandDurationMs; expect(payload).to.deep.equal({ cliName: 'serverless', command: 'print', @@ -199,6 +208,8 @@ describe('lib/utils/telemetry/generatePayload', () => { delete payload.timezone; expect(payload).to.have.property('ciName'); delete payload.ciName; + expect(payload).to.have.property('commandDurationMs'); + delete payload.commandDurationMs; expect(payload).to.deep.equal({ cliName: 'serverless', command: 'config', @@ -230,6 +241,8 @@ describe('lib/utils/telemetry/generatePayload', () => { delete payload.timezone; expect(payload).to.have.property('ciName'); delete payload.ciName; + expect(payload).to.have.property('commandDurationMs'); + delete payload.commandDurationMs; expect(payload).to.deep.equal({ command: 'help', commandOptionNames: [], @@ -279,6 +292,8 @@ describe('lib/utils/telemetry/generatePayload', () => { delete payload.timezone; expect(payload).to.have.property('ciName'); delete payload.ciName; + expect(payload).to.have.property('commandDurationMs'); + delete payload.commandDurationMs; expect(payload).to.deep.equal({ cliName: 'serverless', command: 'print', From 132c830b0a86998efbae1b4984dc9cea85957d61 Mon Sep 17 00:00:00 2001 From: Mariusz Nowak Date: Fri, 30 Apr 2021 13:23:00 +0200 Subject: [PATCH 0041/1101] fix(CLI): Ensure no general help is listed under interactive setup help --- lib/cli/render-help/index.js | 5 ++++- test/unit/scripts/serverless.test.js | 8 ++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/cli/render-help/index.js b/lib/cli/render-help/index.js index 8ee11545428..c1da2506e86 100644 --- a/lib/cli/render-help/index.js +++ b/lib/cli/render-help/index.js @@ -8,7 +8,10 @@ const renderCommandHelp = require('./command'); module.exports = (loadedPlugins) => { const { command, options } = resolveInput(); if (!command) { - if (options['help-interactive']) renderInteractiveSetupHelp(); + if (options['help-interactive']) { + renderInteractiveSetupHelp(); + return; + } renderGeneralHelp(loadedPlugins); } else if (command === 'help') { renderGeneralHelp(loadedPlugins); diff --git a/test/unit/scripts/serverless.test.js b/test/unit/scripts/serverless.test.js index e0a695f202a..17df68a9edf 100644 --- a/test/unit/scripts/serverless.test.js +++ b/test/unit/scripts/serverless.test.js @@ -185,6 +185,14 @@ describe('test/unit/scripts/serverless.test.js', () => { expect(output).to.include('stage'); }); + it('should print interactive setup help to stdout', async () => { + const output = String( + (await spawn('node', [serverlessPath, '--help-interactive'])).stdoutBuffer + ); + expect(output).to.include('Interactive CLI'); + expect(output).to.not.include('General Commands'); + }); + it('should show help when running container command', async () => { // Note: Arbitrarily picked "plugin" command for testing const output = stripAnsi( From 46d5f6aef9190b4e2be6e044b1fe8a1914fd8505 Mon Sep 17 00:00:00 2001 From: Piotr Grzesik Date: Fri, 30 Apr 2021 13:39:28 +0200 Subject: [PATCH 0042/1101] chore: Bump dependencies --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index c972ca04f82..700c1dc58ab 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "ajv": "^6.12.6", "ajv-keywords": "^3.5.2", "archiver": "^5.3.0", - "aws-sdk": "^2.891.0", + "aws-sdk": "^2.896.0", "bluebird": "^3.7.2", "boxen": "^5.0.1", "cachedir": "^2.3.0", @@ -58,7 +58,7 @@ "lodash": "^4.17.21", "memoizee": "^0.4.15", "micromatch": "^4.0.4", - "ncjsm": "^4.1.0", + "ncjsm": "^4.2.0", "node-fetch": "^2.6.1", "object-hash": "^2.1.1", "path2": "^0.1.0", @@ -80,7 +80,7 @@ "chai": "^4.3.4", "chai-as-promised": "^7.1.1", "cos-nodejs-sdk-v5": "^2.9.13", - "eslint": "^7.24.0", + "eslint": "^7.25.0", "eslint-plugin-import": "^2.22.1", "git-list-updated": "^1.2.1", "github-release-from-cc-changelog": "^2.2.0", From 6d2ed840fb21d469d8e4a548a38ca9302d5a9d2c Mon Sep 17 00:00:00 2001 From: Piotr Grzesik Date: Fri, 30 Apr 2021 13:48:41 +0200 Subject: [PATCH 0043/1101] chore: Release 2.39.0 --- CHANGELOG.md | 26 ++++++++++++++++++++++++++ package.json | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8dee4facf5d..ec189c4fbf8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,32 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [2.39.0](https://github.com/serverless/serverless/compare/v2.38.0...v2.39.0) (2021-04-30) + +### Features + +- **AWS IAM:** Support `provider.iam.role.path` ([#9363](https://github.com/serverless/serverless/issues/9363)) ([c8adc0c](https://github.com/serverless/serverless/commit/c8adc0c796a6558c3fe1bc86e3647d3fe711a9ad)) ([Android3000](https://github.com/Android3000)) +- **Variables:** Expose variable resolver function to variable sources ([#9368](https://github.com/serverless/serverless/pull/9368)) ([2ff58b1](https://github.com/serverless/serverless/commit/2ff58b16bf3fe766685d5b6c30fd9a2bb6e22f0f)) ([Mariusz Nowak](https://github.com/medikoo)) + +### Bug Fixes + +- **AWS API Gateway:** Ensure unique name for request validator ([#9382](https://github.com/serverless/serverless/pull/9382)) ([a05e88d](https://github.com/serverless/serverless/commit/a05e88d92e010ddfe019d5b5b873547b7d187d6d)) ([Piotr Grzesik](https://github.com/pgrzesik)) +- **AWS S3:** Fix parsing of the artifact S3 url ([#9380](https://github.com/serverless/serverless/issues/9380)) ([360925d](https://github.com/serverless/serverless/commit/360925d2e0cddb6fbbbb72ca47495aa71a43d1fc)) ([Stephen](https://github.com/bishtawi)) +- **CLI:** Ensure no general help is listed under interactive setup help ([#9406](https://github.com/serverless/serverless/pull/9406)) ([132c830](https://github.com/serverless/serverless/commit/132c830b0a86998efbae1b4984dc9cea85957d61)) ([Mariusz Nowak](https://github.com/medikoo)) + +### Maintenance Improvements + +- **Telemetry:** + - Report failures via telemetry ([#9396](https://github.com/serverless/serverless/pull/9396)) ([5861d08](https://github.com/serverless/serverless/commit/5861d08768a06e2e88609d0785ce590d3f693683)) ([Piotr Grzesik](https://github.com/pgrzesik)) + - Ensure that container commands do not trigger telemetry ([#9397](https://github.com/serverless/serverless/pull/9397)) ([85b9e53](https://github.com/serverless/serverless/commit/85b9e5319df48904664f966e988cc725116ce865)) ([Mariusz Nowak](https://github.com/medikoo)) + - Add `commandDurationMs` to payload ([#9401](https://github.com/serverless/serverless/pull/9401)) ([d647125](https://github.com/serverless/serverless/commit/d647125ff5daa07972675fd28690d42746ab223b)) ([Piotr Grzesik](https://github.com/pgrzesik)) + - Add `commandOptionNames` to payload ([#9387](https://github.com/serverless/serverless/pull/9387)) ([f5b2b9b](https://github.com/serverless/serverless/commit/f5b2b9be395c9c2d3de4c4f91f991276bc22dc33)) ([Piotr Grzesik](https://github.com/pgrzesik)) +- Ensure `code` to `ServerlessError` instances ([#9357](https://github.com/serverless/serverless/pull/9357)) ([822a7cf](https://github.com/serverless/serverless/commit/822a7cf9f527514b53fd8cfc5c172ec5dc53f4ce)) ([Piotr Grzesik](https://github.com/pgrzesik)) + +### Templates + +- Update dependencies for `cloudflare` template ([#9373](https://github.com/serverless/serverless/issues/9373)) ([543423d](https://github.com/serverless/serverless/commit/543423d869ba35c6866506bb49a8642700214b3a)) ([YErii](https://github.com/YEriin)) + ## [2.38.0](https://github.com/serverless/serverless/compare/v2.37.2...v2.38.0) (2021-04-23) ### Features diff --git a/package.json b/package.json index 700c1dc58ab..6ec8759f56e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "serverless", - "version": "2.38.0", + "version": "2.39.0", "description": "Serverless Framework - Build web, mobile and IoT applications with serverless architectures using AWS Lambda, Azure Functions, Google CloudFunctions & more", "preferGlobal": true, "homepage": "https://serverless.com/framework/docs/", From 80511a4b17e77e22cf8b20d1ce50eef7506d4f7f Mon Sep 17 00:00:00 2001 From: Filip Golonka Date: Mon, 3 May 2021 09:50:46 +0200 Subject: [PATCH 0044/1101] fix(AWS HTTP API): Ensure to apply tags to stage (#9407) --- docs/providers/aws/events/http-api.md | 4 ++-- lib/plugins/aws/package/compile/events/httpApi.js | 8 ++++++++ .../plugins/aws/package/compile/events/httpApi.test.js | 4 ++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/providers/aws/events/http-api.md b/docs/providers/aws/events/http-api.md index 0333c0c58b0..f5e325d4f51 100644 --- a/docs/providers/aws/events/http-api.md +++ b/docs/providers/aws/events/http-api.md @@ -406,7 +406,7 @@ provider: ### Tags -When using HTTP API, it is possible to tag the corresponding API Gateway. By setting `provider.httpApi.useProviderTags` to `true`, all tags defined on `provider.tags` will be applied to API Gateway. +When using HTTP API, it is possible to tag the corresponding API Gateway resources. By setting `provider.httpApi.useProviderTags` to `true`, all tags defined on `provider.tags` will be applied to API Gateway and API Gateway Stage. ```yaml provider: @@ -416,6 +416,6 @@ provider: useProviderTags: true ``` -In the above example, the tag project: myProject will be applied to API Gateway. +In the above example, the tag project: myProject will be applied to API Gateway and API Gateway Stage. _Note: If the API Gateway has any existing tags applied outside of Serverless Framework, they will be removed during deployment._ diff --git a/lib/plugins/aws/package/compile/events/httpApi.js b/lib/plugins/aws/package/compile/events/httpApi.js index bc704795172..3abb1835436 100644 --- a/lib/plugins/aws/package/compile/events/httpApi.js +++ b/lib/plugins/aws/package/compile/events/httpApi.js @@ -166,6 +166,14 @@ class HttpApiEvents { }, }; + if ( + this.serverless.service.provider.tags && + this.serverless.service.provider.httpApi && + this.serverless.service.provider.httpApi.useProviderTags + ) { + properties.Tags = Object.assign({}, this.serverless.service.provider.tags); + } + const resource = (this.cfTemplate.Resources[this.provider.naming.getHttpApiStageLogicalId()] = { Type: 'AWS::ApiGatewayV2::Stage', Properties: properties, diff --git a/test/unit/lib/plugins/aws/package/compile/events/httpApi.test.js b/test/unit/lib/plugins/aws/package/compile/events/httpApi.test.js index b5e075045ed..192f5d0246f 100644 --- a/test/unit/lib/plugins/aws/package/compile/events/httpApi.test.js +++ b/test/unit/lib/plugins/aws/package/compile/events/httpApi.test.js @@ -206,6 +206,10 @@ describe('lib/plugins/aws/package/compile/events/httpApi.test.js', () => { const { Tags } = cfApi.Properties; expect(Tags).to.be.a('object'); expect(Tags).to.deep.equal(expectedTags); + + const { Tags: stageTags } = cfStage.Properties; + expect(stageTags).to.be.a('object'); + expect(stageTags).to.deep.equal(expectedTags); }); it('should set payload format version', () => { From 3ccf6a3af3de093fabfa33a966b7a0a922712845 Mon Sep 17 00:00:00 2001 From: Piotr Grzesik Date: Mon, 3 May 2021 11:35:17 +0200 Subject: [PATCH 0045/1101] fix(Telemetry): Properly resolve location when for only relative paths (#9418) --- .../telemetry/anonymize-stacktrace-paths.js | 23 ++++++++++++------- .../anonymize-stacktrace-paths.test.js | 7 ++++++ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/lib/utils/telemetry/anonymize-stacktrace-paths.js b/lib/utils/telemetry/anonymize-stacktrace-paths.js index 737fbe0035f..15b653d11ad 100644 --- a/lib/utils/telemetry/anonymize-stacktrace-paths.js +++ b/lib/utils/telemetry/anonymize-stacktrace-paths.js @@ -4,16 +4,23 @@ const path = require('path'); const commonPath = require('path2/common'); const anonymizeStacktracePaths = (stacktracePaths) => { - let commonPathPrefix = commonPath(...stacktracePaths.filter((p) => path.isAbsolute(p))); + const absoluteStacktracePaths = stacktracePaths.filter((p) => path.isAbsolute(p)); + let commonPathPrefix = ''; - const lastServerlessIndex = commonPathPrefix.lastIndexOf(`${path.sep}serverless${path.sep}`); + if (absoluteStacktracePaths.length) { + commonPathPrefix = commonPath(...absoluteStacktracePaths); - if (lastServerlessIndex !== -1) { - commonPathPrefix = commonPathPrefix.slice(0, lastServerlessIndex); - } else { - const lastNodeModulesIndex = commonPathPrefix.lastIndexOf(`${path.sep}node_modules${path.sep}`); - if (lastNodeModulesIndex !== -1) { - commonPathPrefix = commonPathPrefix.slice(0, lastNodeModulesIndex); + const lastServerlessIndex = commonPathPrefix.lastIndexOf(`${path.sep}serverless${path.sep}`); + + if (lastServerlessIndex !== -1) { + commonPathPrefix = commonPathPrefix.slice(0, lastServerlessIndex); + } else { + const lastNodeModulesIndex = commonPathPrefix.lastIndexOf( + `${path.sep}node_modules${path.sep}` + ); + if (lastNodeModulesIndex !== -1) { + commonPathPrefix = commonPathPrefix.slice(0, lastNodeModulesIndex); + } } } diff --git a/test/unit/lib/utils/telemetry/anonymize-stacktrace-paths.test.js b/test/unit/lib/utils/telemetry/anonymize-stacktrace-paths.test.js index 440e31b09b4..0a2d915bbca 100644 --- a/test/unit/lib/utils/telemetry/anonymize-stacktrace-paths.test.js +++ b/test/unit/lib/utils/telemetry/anonymize-stacktrace-paths.test.js @@ -68,6 +68,13 @@ describe('test/unit/lib/utils/anonymize-stacktrace-paths.test.js', () => { }); } + it('Should handle stacktrace with only relative paths', () => { + const stacktracePaths = ['somefile.js:100:10', 'another.js:100:10']; + + const result = anonymizeStacktracePaths(stacktracePaths); + expect(result).to.deep.equal(['somefile.js:100:10', 'another.js:100:10']); + }); + if (process.platform === 'win32') { it('Should remove common prefix up to last `serverless` occurence for windows-style paths', () => { const stacktracePaths = [ From 3ab06282fdc9455f364fd73bd14761bae0c8d289 Mon Sep 17 00:00:00 2001 From: Piotr Grzesik Date: Mon, 3 May 2021 11:56:32 +0200 Subject: [PATCH 0046/1101] fix(Telemetry): Handle error locations not enclosed in parens --- lib/utils/telemetry/resolve-error-location.js | 4 +- .../telemetry/resolve-error-location.test.js | 40 +++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/lib/utils/telemetry/resolve-error-location.js b/lib/utils/telemetry/resolve-error-location.js index 55c6f077bdf..e7e91dba33a 100644 --- a/lib/utils/telemetry/resolve-error-location.js +++ b/lib/utils/telemetry/resolve-error-location.js @@ -8,11 +8,11 @@ const resolveErrorLocation = (exceptionTokens) => { const splittedStack = exceptionTokens.stack.split('\n'); if (splittedStack.length === 1 && exceptionTokens.code) return ''; - const stacktraceLineRegex = /\s*at.*\((.*)\).?/; + const stacktraceLineRegex = /(?:\s*at.*\((.*)\).?|\s*at\s(.*))/; const stacktracePaths = []; for (const line of splittedStack) { const match = line.match(stacktraceLineRegex) || []; - const matchedPath = match[1]; + const matchedPath = match[1] || match[2]; if (matchedPath) { // Limited to maximum 5 lines in location if (stacktracePaths.push(matchedPath) === 5) break; diff --git a/test/unit/lib/utils/telemetry/resolve-error-location.test.js b/test/unit/lib/utils/telemetry/resolve-error-location.test.js index ceab86b564d..3675678e781 100644 --- a/test/unit/lib/utils/telemetry/resolve-error-location.test.js +++ b/test/unit/lib/utils/telemetry/resolve-error-location.test.js @@ -29,6 +29,26 @@ describe('test/unit/lib/utils/resolve-error-location.test.js', () => { }); if (process.platform !== 'win32') { + it('should correctly handle paths not enclosed in parentheses', () => { + const err = new Error('test'); + err.stack = + 'Error: spawn E2BIG\n' + + ' at ChildProcess.spawn (node:internal/child_process:403:11)\n' + + ' at Object.spawn (node:child_process:573:9)\n' + + ' at /home/xxx/api/node_modules/bestzip/lib/bestzip.js:75:29\n' + + ' at /home/xxx/api/node_modules/async/dist/async.js:1802:20\n'; + + const result = resolveErrorLocation(tokenizeException(err)); + expect(result).to.equal( + [ + 'node:internal/child_process:403:11', + 'node:child_process:573:9', + '/bestzip/lib/bestzip.js:75:29', + '/async/dist/async.js:1802:20', + ].join('\n') + ); + }); + it('should return at most 5 lines', () => { const err = new Error('test'); err.stack = @@ -53,6 +73,26 @@ describe('test/unit/lib/utils/resolve-error-location.test.js', () => { } if (process.platform === 'win32') { + it('should correctly handle paths not enclosed in parentheses', () => { + const err = new Error('test'); + err.stack = + 'Error: spawn E2BIG\n' + + ' at ChildProcess.spawn (node:internal/child_process:403:11)\n' + + ' at Object.spawn (node:child_process:573:9)\n' + + ' at C:\\home\\xxx\\api\\node_modules\\bestzip\\lib\\bestzip.js:75:29\n' + + ' at C:\\home\\xxx\\api\\node_modules\\async\\dist\\async.js:1802:20\n'; + + const result = resolveErrorLocation(tokenizeException(err)); + expect(result).to.equal( + [ + 'node:internal/child_process:403:11', + 'node:child_process:573:9', + '/bestzip/lib/bestzip.js:75:29', + '/async/dist/async.js:1802:20', + ].join('\n') + ); + }); + it('should return at most 5 lines and use `/` path separator', () => { const err = new Error('test'); err.stack = From bdbf154c97abde6ad2ff807dbea3ad1110ee5fec Mon Sep 17 00:00:00 2001 From: Piotr Grzesik Date: Mon, 3 May 2021 11:58:47 +0200 Subject: [PATCH 0047/1101] fix(Telemetry): Split stack lines properly on all OS-es --- lib/utils/telemetry/resolve-error-location.js | 2 +- .../telemetry/resolve-error-location.test.js | 22 +++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/utils/telemetry/resolve-error-location.js b/lib/utils/telemetry/resolve-error-location.js index e7e91dba33a..3ddc7d18942 100644 --- a/lib/utils/telemetry/resolve-error-location.js +++ b/lib/utils/telemetry/resolve-error-location.js @@ -5,7 +5,7 @@ const anonymizeStacktracePaths = require('./anonymize-stacktrace-paths'); const resolveErrorLocation = (exceptionTokens) => { if (!exceptionTokens.stack) return ''; - const splittedStack = exceptionTokens.stack.split('\n'); + const splittedStack = exceptionTokens.stack.split(/[\r\n]+/); if (splittedStack.length === 1 && exceptionTokens.code) return ''; const stacktraceLineRegex = /(?:\s*at.*\((.*)\).?|\s*at\s(.*))/; diff --git a/test/unit/lib/utils/telemetry/resolve-error-location.test.js b/test/unit/lib/utils/telemetry/resolve-error-location.test.js index 3675678e781..c7ad4d678d6 100644 --- a/test/unit/lib/utils/telemetry/resolve-error-location.test.js +++ b/test/unit/lib/utils/telemetry/resolve-error-location.test.js @@ -76,11 +76,11 @@ describe('test/unit/lib/utils/resolve-error-location.test.js', () => { it('should correctly handle paths not enclosed in parentheses', () => { const err = new Error('test'); err.stack = - 'Error: spawn E2BIG\n' + - ' at ChildProcess.spawn (node:internal/child_process:403:11)\n' + - ' at Object.spawn (node:child_process:573:9)\n' + - ' at C:\\home\\xxx\\api\\node_modules\\bestzip\\lib\\bestzip.js:75:29\n' + - ' at C:\\home\\xxx\\api\\node_modules\\async\\dist\\async.js:1802:20\n'; + 'Error: spawn E2BIG\r\n' + + ' at ChildProcess.spawn (node:internal/child_process:403:11)\r\n' + + ' at Object.spawn (node:child_process:573:9)\r\n' + + ' at C:\\home\\xxx\\api\\node_modules\\bestzip\\lib\\bestzip.js:75:29\r\n' + + ' at C:\\home\\xxx\\api\\node_modules\\async\\dist\\async.js:1802:20\r\n'; const result = resolveErrorLocation(tokenizeException(err)); expect(result).to.equal( @@ -97,12 +97,12 @@ describe('test/unit/lib/utils/resolve-error-location.test.js', () => { const err = new Error('test'); err.stack = 'Error:\n' + - ' at Context.it (C:\\home\\xxx\\serverless\\test\\unit\\lib\\utils\\resolve-error-location.test.js:10:17)\n' + - ' at callFn (C:\\home\\xxx\\serverless\\node_modules\\mocha\\lib\\runnable.js:366:21)\n' + - ' at Test.Runnable.run (C:\\home\\xxx\\serverless\\node_modules\\mocha\\lib\\runnable.js:354:5)\n' + - ' at Runner.runTest (C:\\home\\xxx\\serverless\\node_modules\\mocha\\lib\\runner.js:677:10)\n' + - ' at next (C:\\home\\xxx\\serverless\\node_modules\\mocha\\lib\\runner.js:801:12)\n' + - ' at next (C:\\home\\xxx\\serverless\\node_modules\\mocha\\lib\\runner.js:594:14)\n'; + ' at Context.it (C:\\home\\xxx\\serverless\\test\\unit\\lib\\utils\\resolve-error-location.test.js:10:17)\r\n' + + ' at callFn (C:\\home\\xxx\\serverless\\node_modules\\mocha\\lib\\runnable.js:366:21)\r\n' + + ' at Test.Runnable.run (C:\\home\\xxx\\serverless\\node_modules\\mocha\\lib\\runnable.js:354:5)\r\n' + + ' at Runner.runTest (C:\\home\\xxx\\serverless\\node_modules\\mocha\\lib\\runner.js:677:10)\r\n' + + ' at next (C:\\home\\xxx\\serverless\\node_modules\\mocha\\lib\\runner.js:801:12)\r\n' + + ' at next (C:\\home\\xxx\\serverless\\node_modules\\mocha\\lib\\runner.js:594:14)\r\n'; const result = resolveErrorLocation(tokenizeException(err)); expect(result).to.equal( [ From 3282d9ace4a2d112def81c1326c48ff47dc9f255 Mon Sep 17 00:00:00 2001 From: Piotr Grzesik Date: Mon, 3 May 2021 12:13:50 +0200 Subject: [PATCH 0048/1101] chore: Bump dependencies --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 6ec8759f56e..15c4d22450d 100644 --- a/package.json +++ b/package.json @@ -23,13 +23,13 @@ }, "dependencies": { "@serverless/cli": "^1.5.2", - "@serverless/components": "^3.9.0", + "@serverless/components": "^3.9.1", "@serverless/enterprise-plugin": "^4.5.3", "@serverless/utils": "^4.1.0", "ajv": "^6.12.6", "ajv-keywords": "^3.5.2", "archiver": "^5.3.0", - "aws-sdk": "^2.896.0", + "aws-sdk": "^2.897.0", "bluebird": "^3.7.2", "boxen": "^5.0.1", "cachedir": "^2.3.0", @@ -79,7 +79,7 @@ "@serverless/test": "^8.1.0", "chai": "^4.3.4", "chai-as-promised": "^7.1.1", - "cos-nodejs-sdk-v5": "^2.9.13", + "cos-nodejs-sdk-v5": "^2.9.14", "eslint": "^7.25.0", "eslint-plugin-import": "^2.22.1", "git-list-updated": "^1.2.1", From 2403cc6e30f336003c5f561aa119e5732fe008b9 Mon Sep 17 00:00:00 2001 From: Piotr Grzesik Date: Mon, 3 May 2021 12:22:15 +0200 Subject: [PATCH 0049/1101] chore: Release v2.39.1 --- CHANGELOG.md | 13 +++++++++++++ package.json | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec189c4fbf8..adc0b6c0a99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [2.39.1](https://github.com/serverless/serverless/compare/v2.39.0...v2.39.1) (2021-05-03) + +### Bug Fixes + +- **AWS HTTP API:** Ensure to apply tags to stage ([#9407](https://github.com/serverless/serverless/issues/9407)) ([80511a4](https://github.com/serverless/serverless/commit/80511a4b17e77e22cf8b20d1ce50eef7506d4f7f)) ([Filip Golonka](https://github.com/filipgolonka)) + +### Maintenance Improvements + +- **Telemetry:** + - Handle error locations not enclosed in parens ([#9419](https://github.com/serverless/serverless/pull/9419)) ([3ab0628](https://github.com/serverless/serverless/commit/3ab06282fdc9455f364fd73bd14761bae0c8d289)) ([Piotr Grzesik](https://github.com/pgrzesik)) + - Properly resolve location when for only relative paths ([#9418](https://github.com/serverless/serverless/issues/9418)) ([3ccf6a3](https://github.com/serverless/serverless/commit/3ccf6a3af3de093fabfa33a966b7a0a922712845)) ([Piotr Grzesik](https://github.com/pgrzesik)) + - Split stack lines properly on all OS-es ([#9419](https://github.com/serverless/serverless/pull/9419)) ([bdbf154](https://github.com/serverless/serverless/commit/bdbf154c97abde6ad2ff807dbea3ad1110ee5fec)) ([Piotr Grzesik](https://github.com/pgrzesik)) + ## [2.39.0](https://github.com/serverless/serverless/compare/v2.38.0...v2.39.0) (2021-04-30) ### Features diff --git a/package.json b/package.json index 15c4d22450d..fbf684bd7bf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "serverless", - "version": "2.39.0", + "version": "2.39.1", "description": "Serverless Framework - Build web, mobile and IoT applications with serverless architectures using AWS Lambda, Azure Functions, Google CloudFunctions & more", "preferGlobal": true, "homepage": "https://serverless.com/framework/docs/", From e02a267e688fed77f3aa53a5424b048f5f1970db Mon Sep 17 00:00:00 2001 From: Piotr Grzesik Date: Mon, 3 May 2021 13:29:18 +0200 Subject: [PATCH 0050/1101] docs(Variables): Improve CLI options docs --- docs/providers/aws/guide/variables.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/providers/aws/guide/variables.md b/docs/providers/aws/guide/variables.md index 3661b4b13b7..c0eb6e6abf9 100644 --- a/docs/providers/aws/guide/variables.md +++ b/docs/providers/aws/guide/variables.md @@ -162,7 +162,7 @@ In the above example you're dynamically adding a prefix to the function names by ## 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: