serverless/scripts/serverless.js

606 lines
25 KiB
JavaScript
Executable File

#!/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,
});
}
resolverConfiguration.sources.sls = require('../lib/configuration/variables/sources/instance-dependent/get-sls')(
serverless
);
resolverConfiguration.fulfilledSources.add('opt').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) return;
if (
eventuallyReportVariableResolutionErrors(configurationPath, configuration, variablesMeta)
) {
return;
}
const unresolvedSources = require('../lib/configuration/variables/resolve-unresolved-source-types')(
variablesMeta
);
if (!(configuration.variablesResolutionMode >= 20210326)) {
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(<region>):stackName.outputName}" (while ' +
'"${cf.<region>: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(<region>):parameter-path}" (while ' +
'"${ssm.<region>: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 (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,
});
}
})();