#!/usr/bin/env node 'use strict'; require('essentials'); // global graceful-fs patch // https://github.com/isaacs/node-graceful-fs#global-patching require('graceful-fs').gracefulify(require('fs')); if (require('../lib/utils/tabCompletion/isSupported') && process.argv[2] === 'completion') { require('../lib/utils/autocomplete')(); return; } const handleError = require('../lib/cli/handle-error'); const humanizePropertyPathKeys = require('../lib/configuration/variables/humanize-property-path-keys'); let serverless; process.once('uncaughtException', (error) => handleError(error, { isUncaughtException: true, isLocallyInstalled: serverless && serverless.isLocallyInstalled, isInvokedByGlobalInstallation: serverless && serverless.isInvokedByGlobalInstallation, }) ); const processSpanPromise = (async () => { try { const wait = require('timers-ext/promise/sleep'); await wait(); // Ensure access to "processSpanPromise" // Propagate (in a background) eventual pending analytics requests require('../lib/utils/analytics').sendPending({ serverlessExecutionSpan: processSpanPromise, }); const resolveInput = require('../lib/cli/resolve-input'); // Parse args against schemas of commands which do not require to be run in service context let { command, commands, options, isHelpRequest, commandSchema } = resolveInput( require('../lib/cli/commands-schema/no-service') ); // If version number request, show it and abort if (options.version) { await require('../lib/cli/render-version')(); return; } const ServerlessError = require('../lib/serverless-error'); if (commandSchema && commandSchema.isHidden && commandSchema.noSupportNotice) { throw new ServerlessError( `Cannot run \`${command}\` command: ${commandSchema.noSupportNotice}` ); } const path = require('path'); const uuid = require('uuid'); const _ = require('lodash'); const Serverless = require('../lib/Serverless'); const resolveVariables = require('../lib/configuration/variables/resolve'); const isPropertyResolved = require('../lib/configuration/variables/is-property-resolved'); const eventuallyReportVariableResolutionErrors = require('../lib/configuration/variables/eventually-report-resolution-errors'); const filterSupportedOptions = require('../lib/cli/filter-supported-options'); const logDeprecation = require('../lib/utils/logDeprecation'); let configurationPath = null; let configuration = null; let providerName; let variablesMeta; let resolverConfiguration; const ensureResolvedProperty = (propertyPath, { shouldSilentlyReturnIfLegacyMode } = {}) => { if (isPropertyResolved(variablesMeta, propertyPath)) return true; variablesMeta = null; if (isHelpRequest) return false; const humanizedPropertyPath = humanizePropertyPathKeys(propertyPath.split('\0')); if (!shouldSilentlyReturnIfLegacyMode || configuration.variablesResolutionMode) { throw new ServerlessError( `Cannot resolve ${path.basename( configurationPath )}: "${humanizedPropertyPath}" property is not accessible ` + '(configured behind variables which cannot be resolved at this stage)' ); } logDeprecation( 'NEW_VARIABLES_RESOLVER', `"${humanizedPropertyPath}" is not accessible ` + '(configured behind variables which cannot be resolved at this stage).\n' + 'Starting with next major release, ' + 'this will be communicated with a thrown error.\n' + 'Set "variablesResolutionMode: 20210326" in your service config, ' + 'to adapt to this behavior now', { serviceConfig: configuration } ); return false; }; if (!commandSchema || commandSchema.serviceDependencyMode) { const resolveConfigurationPath = require('../lib/cli/resolve-configuration-path'); const readConfiguration = require('../lib/configuration/read'); // Resolve eventual service configuration path configurationPath = await resolveConfigurationPath(); // If service configuration file is found, load its content configuration = configurationPath ? await (async () => { try { return await readConfiguration(configurationPath); } catch (error) { // Configuration syntax error should not prevent help from being displayed // (if possible configuration should be read for help request as registered // plugins may introduce new commands to be listed in help output) if (isHelpRequest) return null; throw error; } })() : null; if (configuration) { if (!commandSchema) { // If command was not recognized in first resolution phase // Parse args again also against schemas commands which require service to be run resolveInput.clear(); ({ command, commands, options, isHelpRequest, commandSchema } = resolveInput( require('../lib/cli/commands-schema/service') )); } // IIFE for maintanance convenience await (async () => { if (_.get(configuration.provider, 'variableSyntax')) { // Request to rely on old variables resolver explictly, abort if (isHelpRequest) return; if (configuration.variablesResolutionMode) { throw new ServerlessError( `Cannot resolve ${path.basename( configurationPath )}: "variableSyntax" is not supported with new variables resolver. ` + 'Please drop this setting' ); } logDeprecation( 'NEW_VARIABLES_RESOLVER', 'Serverless Framework was enhanced with a new variables resolver ' + 'which doesn\'t recognize "provider.variableSyntax" setting.' + "Starting with a new major it will be the only resolver that's used." + '. Drop setting from a configuration to adapt to it', { serviceConfig: configuration } ); return; } const resolveVariablesMeta = require('../lib/configuration/variables/resolve-meta'); const resolveProviderName = require('../lib/configuration/resolve-provider-name'); variablesMeta = resolveVariablesMeta(configuration); if ( eventuallyReportVariableResolutionErrors( configurationPath, configuration, variablesMeta ) ) { // Variable syntax errors, abort variablesMeta = null; return; } // "variablesResolutionMode" must not be configured with variables as it influences // variable resolution choices if (!ensureResolvedProperty('variablesResolutionMode')) return; if (!ensureResolvedProperty('disabledDeprecations')) return; if (isPropertyResolved(variablesMeta, 'provider\0name')) { providerName = resolveProviderName(configuration); } 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 resolveInput.clear(); ({ command, commands, options, isHelpRequest, commandSchema } = resolveInput( require('../lib/cli/commands-schema/aws-service') )); } if (variablesMeta.size) { // Some properties are configured with variables // Resolve eventual variables in `provider.stage` and `useDotEnv` // (required for reliable .env resolution) resolverConfiguration = { servicePath: process.cwd(), configuration, variablesMeta, sources: { env: require('../lib/configuration/variables/sources/env'), file: require('../lib/configuration/variables/sources/file'), opt: require('../lib/configuration/variables/sources/opt'), self: require('../lib/configuration/variables/sources/self'), strToBool: require('../lib/configuration/variables/sources/str-to-bool'), }, options: filterSupportedOptions(options, { commandSchema, providerName }), fulfilledSources: new Set(['file', 'self', 'strToBool']), propertyPathsToResolve: new Set(['provider\0name', 'provider\0stage', 'useDotenv']), }; await resolveVariables(resolverConfiguration); if ( eventuallyReportVariableResolutionErrors( configurationPath, configuration, variablesMeta ) ) { // Unrecoverable resolution errors, abort variablesMeta = null; return; } if (!providerName && isPropertyResolved(variablesMeta, 'provider\0name')) { providerName = resolveProviderName(configuration); if (!commandSchema && providerName === 'aws') { // If command was not recognized in previous resolution phases // Parse args again also against schemas of commands which work in context of an AWS // service resolveInput.clear(); ({ command, commands, options, isHelpRequest, commandSchema } = resolveInput( require('../lib/cli/commands-schema/aws-service') )); if (commandSchema) { resolverConfiguration.options = filterSupportedOptions(options, { commandSchema, providerName, }); await resolveVariables(resolverConfiguration); if ( eventuallyReportVariableResolutionErrors( configurationPath, configuration, variablesMeta ) ) { variablesMeta = null; return; } } } } if ( !ensureResolvedProperty('provider\0stage', { shouldSilentlyReturnIfLegacyMode: true }) ) { // Hack to not duplicate the warning with similar deprecation logDeprecation.triggeredDeprecations.add('VARIABLES_ERROR_ON_UNRESOLVED'); return; } if (!ensureResolvedProperty('useDotenv')) return; } // Load eventual environment variables from .env files await require('../lib/cli/conditionally-load-dotenv')(options, configuration); if (!variablesMeta.size) return; // No properties configured with variables // Resolve all unresolved configuration properties resolverConfiguration.fulfilledSources.add('env'); if (isHelpRequest) { // We do not need full config resolved, we just need to know what // provider is service setup with, and with what eventual plugins Framework is extended // as that influences what CLI commands and options could be used, resolverConfiguration.propertyPathsToResolve.add('plugins'); } else { delete resolverConfiguration.propertyPathsToResolve; } await resolveVariables(resolverConfiguration); if ( eventuallyReportVariableResolutionErrors( configurationPath, configuration, variablesMeta ) ) { variablesMeta = null; return; } if (!providerName) { if (!ensureResolvedProperty('provider\0name')) return; providerName = resolveProviderName(configuration); if (!commandSchema && providerName === 'aws') { resolveInput.clear(); ({ command, commands, options, isHelpRequest, commandSchema } = resolveInput( require('../lib/cli/commands-schema/aws-service') )); if (commandSchema) { resolverConfiguration.options = filterSupportedOptions(options, { commandSchema, providerName, }); await resolveVariables(resolverConfiguration); if ( eventuallyReportVariableResolutionErrors( configurationPath, configuration, variablesMeta ) ) { variablesMeta = null; return; } } } } if (!variablesMeta.size) return; // All properties successuflly resolved if (!ensureResolvedProperty('plugins')) return; if (!ensureResolvedProperty('frameworkVersion')) return; if (!ensureResolvedProperty('configValidationMode')) return; if (!ensureResolvedProperty('app')) return; if (!ensureResolvedProperty('org')) return; if (!ensureResolvedProperty('service', { shouldSilentlyReturnIfLegacyMode: true })) { return; } if (configuration.org) { // Dashboard requires AWS region to be resolved upfront ensureResolvedProperty('provider\0region', { shouldSilentlyReturnIfLegacyMode: true }); } })(); } else { // In non-service context we recognize all AWS service commands resolveInput.clear(); ({ command, commands, options, isHelpRequest, commandSchema } = resolveInput( require('../lib/cli/commands-schema/aws-service') )); require('../lib/cli/ensure-supported-command')(); } } serverless = new Serverless({ configuration, configurationPath: configuration && configurationPath, isConfigurationResolved: Boolean(variablesMeta && !variablesMeta.size), hasResolvedCommandsExternally: true, commands, options, }); try { serverless.onExitPromise = processSpanPromise; serverless.invocationId = uuid.v4(); await serverless.init(); if (serverless.invokedInstance) { serverless.invokedInstance.invocationId = serverless.invocationId; serverless = serverless.invokedInstance; } // IIFE for maintanance convenience await (async () => { if (!configuration) return; let hasFinalCommandSchema = false; if (configuration.plugins) { // 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' } ); resolveInput.clear(); ({ command, commands, options, isHelpRequest, commandSchema } = resolveInput( commandsSchema )); serverless.processedInput.commands = serverless.pluginManager.cliCommands = commands; serverless.processedInput.options = serverless.pluginManager.cliOptions = options; hasFinalCommandSchema = true; } } else { // Invocation fallen back to old Framework version. As we do not have easily // accessible info on loaded plugins, skip further variables resolution variablesMeta = null; } } if (!providerName && !hasFinalCommandSchema) { // Invalid configuration, ensure to recognize all AWS commands resolveInput.clear(); ({ command, commands, options, isHelpRequest, commandSchema } = resolveInput( require('../lib/cli/commands-schema/aws-service') )); } require('../lib/cli/ensure-supported-command')(); if (isHelpRequest) return; if (!_.get(variablesMeta, 'size')) return; if (providerName === 'aws') { if ( !ensureResolvedProperty('provider\0credentials', { shouldSilentlyReturnIfLegacyMode: true, }) || !ensureResolvedProperty('provider\0deploymentBucket\0serverSideEncryption', { shouldSilentlyReturnIfLegacyMode: true, }) || !ensureResolvedProperty('provider\0profile', { shouldSilentlyReturnIfLegacyMode: true, }) || !ensureResolvedProperty('provider\0region', { shouldSilentlyReturnIfLegacyMode: true, }) ) { return; } } if (commandSchema) { resolverConfiguration.options = filterSupportedOptions(options, { commandSchema, providerName, }); } if (configuration.variablesResolutionMode >= 20210326) { // New resolver, resolves just recognized CLI options. Therefore we cannot assume // we have full "opt" source data if user didn't explicitly switch to new resolver resolverConfiguration.fulfilledSources.add('opt'); } resolverConfiguration.sources.sls = require('../lib/configuration/variables/sources/instance-dependent/get-sls')( serverless ); resolverConfiguration.fulfilledSources.add('sls'); if (providerName === 'aws') { Object.assign(resolverConfiguration.sources, { cf: require('../lib/configuration/variables/sources/instance-dependent/get-cf')( serverless ), s3: require('../lib/configuration/variables/sources/instance-dependent/get-s3')( serverless ), ssm: require('../lib/configuration/variables/sources/instance-dependent/get-ssm')( serverless ), }); resolverConfiguration.fulfilledSources.add('cf').add('s3').add('ssm'); } if (configuration.org && serverless.pluginManager.dashboardPlugin) { for (const [sourceName, sourceConfig] of Object.entries( serverless.pluginManager.dashboardPlugin.configurationVariablesSources )) { resolverConfiguration.sources[sourceName] = sourceConfig; resolverConfiguration.fulfilledSources.add(sourceName); } } const ensurePlainFunction = require('type/plain-function/ensure'); const ensurePlainObject = require('type/plain-object/ensure'); for (const externalPlugin of serverless.pluginManager.externalPlugins) { const pluginName = externalPlugin.constructor.name; if (externalPlugin.configurationVariablesSources != null) { ensurePlainObject(externalPlugin.configurationVariablesSources, { errorMessage: 'Invalid "configurationVariablesSources" ' + `configuration on "${pluginName}", expected object, got: %v"`, Error: ServerlessError, }); for (const [sourceName, sourceConfig] of Object.entries( externalPlugin.configurationVariablesSources )) { if (resolverConfiguration.sources[sourceName]) { throw new ServerlessError( `Cannot add "${sourceName}" configuration variable source ` + `(through "${pluginName}" plugin) as resolution rules ` + 'for this source name are already configured' ); } ensurePlainFunction( ensurePlainObject(sourceConfig, { errorMessage: `Invalid "configurationVariablesSources.${sourceName}" ` + `configuration on "${pluginName}", expected object, got: %v"`, Error: ServerlessError, }).resolve, { errorMessage: `Invalid "configurationVariablesSources.${sourceName}.resolve" ` + `value on "${pluginName}", expected function, got: %v"`, Error: ServerlessError, } ); resolverConfiguration.sources[sourceName] = sourceConfig; resolverConfiguration.fulfilledSources.add(sourceName); } } else if ( externalPlugin.variableResolvers && configuration.variablesResolutionMode < 20210326 ) { logDeprecation( 'NEW_VARIABLES_RESOLVER', `Plugin "${pluginName}" attempts to extend old variables resolver. ` + 'Ensure to rely on latest version of a plugin and if this warning is ' + 'still displayed please report the problem at plugin issue tracker' + 'Starting with next major release, ' + 'old variables resolver will not be supported.\n', { serviceConfig: configuration } ); } } await resolveVariables(resolverConfiguration); if (!variablesMeta.size) { serverless.isConfigurationInputResolved = true; return; } if ( eventuallyReportVariableResolutionErrors(configurationPath, configuration, variablesMeta) ) { return; } const unresolvedSources = require('../lib/configuration/variables/resolve-unresolved-source-types')( variablesMeta ); if (!(configuration.variablesResolutionMode >= 20210326)) { unresolvedSources.delete('opt'); const legacyCfVarPropertyPaths = new Set(); const legacySsmVarPropertyPaths = new Set(); for (const [sourceType, propertyPaths] of unresolvedSources) { if (sourceType.startsWith('cf.')) { for (const propertyPath of propertyPaths) legacyCfVarPropertyPaths.add(propertyPath); unresolvedSources.delete(sourceType); } if (sourceType.startsWith('ssm.')) { for (const propertyPath of propertyPaths) legacySsmVarPropertyPaths.add(propertyPath); unresolvedSources.delete(sourceType); } } if (legacyCfVarPropertyPaths.size) { logDeprecation( 'NEW_VARIABLES_RESOLVER', 'Syntax for referencing CF outputs was upgraded to ' + '"${cf():stackName.outputName}" (while ' + '"${cf.:stackName.outputName}" is now deprecated, ' + 'as not supported by new variables resolver).\n' + 'Please upgrade to use new form instead.' + 'Starting with next major release, ' + 'this will be communicated with a thrown error.\n', { serviceConfig: configuration } ); } if (legacySsmVarPropertyPaths.size) { logDeprecation( 'NEW_VARIABLES_RESOLVER', 'Syntax for referencing SSM parameters was upgraded to ' + '"${ssm():parameter-path}" (while ' + '"${ssm.:parameter-path}" is now deprecated, ' + 'as not supported by new variables resolver).\n' + 'Please upgrade to use new form instead.' + 'Starting with next major release, ' + 'this will be communicated with a thrown error.\n', { serviceConfig: configuration } ); } if (unresolvedSources.size) { logDeprecation( 'NEW_VARIABLES_RESOLVER', `Approached unrecognized configuration variable sources: "${Array.from( unresolvedSources.keys() ).join('", "')}".\n` + 'From a next major this will be communicated with a thrown error.\n' + 'Set "variablesResolutionMode: 20210326" in your service config, ' + 'to adapt to new behavior now', { serviceConfig: configuration } ); } } else { throw new ServerlessError( `Approached unrecognized configuration variable sources: "${Array.from( unresolvedSources.keys() ).join('", "')}"`, 'UNRECOGNIZED_VARIABLE_SOURCES' ); } })(); if (isHelpRequest && serverless.pluginManager.externalPlugins) { require('../lib/cli/render-help')(serverless.pluginManager.externalPlugins); } else { await serverless.run(); } } catch (error) { // If Dashboard Plugin, capture error const dashboardPlugin = serverless.pluginManager.dashboardPlugin || serverless.pluginManager.plugins.find((p) => p.enterprise); const dashboardErrorHandler = _.get(dashboardPlugin, 'enterprise.errorHandler'); if (!dashboardErrorHandler) throw error; try { await dashboardErrorHandler(error, serverless.invocationId); } catch (dashboardErrorHandlerError) { const log = require('@serverless/utils/log'); const tokenizeException = require('../lib/utils/tokenize-exception'); const exceptionTokens = tokenizeException(dashboardErrorHandlerError); log( `Publication to Serverless Dashboard errored with:\n${' '.repeat('Serverless: '.length)}${ exceptionTokens.isUserError || !exceptionTokens.stack ? exceptionTokens.message : exceptionTokens.stack }`, { color: 'orange' } ); } throw error; } } catch (error) { handleError(error, { isLocallyInstalled: serverless && serverless.isLocallyInstalled, isInvokedByGlobalInstallation: serverless && serverless.isInvokedByGlobalInstallation, }); } })();