mirror of
https://github.com/serverless/serverless.git
synced 2026-01-18 14:58:43 +00:00
chore: remove unnecessary files and rework variable resolution (#12340)
This commit is contained in:
parent
347d913c19
commit
4fa39b8a80
@ -5,7 +5,6 @@ const _ = require('lodash');
|
||||
const ServerlessError = require('../serverless-error');
|
||||
const resolveCliInput = require('../cli/resolve-input');
|
||||
const renderCommandHelp = require('../cli/render-help/command');
|
||||
const processBackendNotificationRequest = require('../utils/process-backend-notification-request');
|
||||
const tokenizeException = require('../utils/tokenize-exception');
|
||||
const getRequire = require('../utils/get-require');
|
||||
const importModule = require('../utils/require-with-import-fallback');
|
||||
@ -380,6 +379,16 @@ class PluginManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all public commands and aliases from the plugin manager. This method
|
||||
* iterates through the commands and stops at entrypoints to include only public
|
||||
* commands throughout the hierarchy. It then iterates through the existing aliases
|
||||
* and adds them as commands. The result is an object that maps command names to
|
||||
* their respective command objects, with each command object omitting its own
|
||||
* 'commands' property to prevent circular references.
|
||||
*
|
||||
* @returns {Object} An object mapping command names to command objects.
|
||||
*/
|
||||
getCommands() {
|
||||
const result = {};
|
||||
|
||||
@ -476,10 +485,12 @@ class PluginManager {
|
||||
|
||||
// Invalid command, can happen only when Framework is used programmatically,
|
||||
// as otherwise command is validated in main script
|
||||
throw new ServerlessError(
|
||||
`Unrecognized command "${commandsArray.join(' ')}"`,
|
||||
const err = new ServerlessError(
|
||||
`Unrecognized command "${commandsArray.join(' ')}".`,
|
||||
'UNRECOGNIZED COMMAND'
|
||||
);
|
||||
err.stack = undefined;
|
||||
throw err;
|
||||
},
|
||||
{ commands: this.commands }
|
||||
);
|
||||
@ -578,7 +589,16 @@ class PluginManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the CLI to start a public command.
|
||||
* Executes the provided commands array. This method initializes hooks, invokes
|
||||
* the commands, and handles any exceptions that occur during command execution.
|
||||
* If an exception occurs, it triggers the error hooks and rethrows the exception.
|
||||
* After successful execution or error handling, it triggers the finalize hooks.
|
||||
* If an exception occurs during finalize, it rethrows the exception.
|
||||
*
|
||||
* @async
|
||||
* @param {Array} commandsArray - The array of commands to execute.
|
||||
* @throws {Error} If there's an error in command execution or during the
|
||||
* execution of error or finalize hooks.
|
||||
*/
|
||||
async run(commandsArray) {
|
||||
this.commandRunStartTime = Date.now();
|
||||
@ -613,10 +633,6 @@ class PluginManager {
|
||||
await deferredBackendNotificationRequest;
|
||||
throw finalizeHookException;
|
||||
}
|
||||
|
||||
if (deferredBackendNotificationRequest) {
|
||||
await processBackendNotificationRequest(await deferredBackendNotificationRequest);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -175,6 +175,16 @@ class Service {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads the Service from the initial serverless configuration.
|
||||
* This method is responsible for setting the provider, package, custom, resources,
|
||||
* functions, config validation mode, layers (if the provider is AWS), and outputs
|
||||
* based on the initial serverless configuration. If no stage is specified for the
|
||||
* provider, it defaults to 'dev'. If no functions or layers are specified, they
|
||||
* default to empty objects.
|
||||
*
|
||||
* @throws {Error} If there's an error in loading the service file parameters.
|
||||
*/
|
||||
reloadServiceFileParam() {
|
||||
const configurationInput = this.initialServerlessConfig;
|
||||
if (_.isObject(configurationInput.provider)) {
|
||||
@ -188,9 +198,7 @@ class Service {
|
||||
this.resources = configurationInput.resources;
|
||||
this.functions = configurationInput.functions || {};
|
||||
this.configValidationMode = configurationInput.configValidationMode || 'warn';
|
||||
if (this.provider.name === 'aws') {
|
||||
this.layers = configurationInput.layers || {};
|
||||
}
|
||||
this.layers = configurationInput.layers || {};
|
||||
this.outputs = configurationInput.outputs;
|
||||
}
|
||||
|
||||
@ -271,13 +279,6 @@ class Service {
|
||||
'You can safely remove it from the configuration'
|
||||
);
|
||||
}
|
||||
if (userConfig.variablesResolutionMode != null) {
|
||||
this.serverless._logDeprecation(
|
||||
'VARIABLES_RESOLUTION_MODE',
|
||||
'Starting with v3.0, the "variablesResolutionMode" option is now useless. ' +
|
||||
'You can safely remove it from the configuration'
|
||||
);
|
||||
}
|
||||
if (userConfig.console != null) {
|
||||
this.serverless._logDeprecation(
|
||||
'CONSOLE_CONFIGURATION',
|
||||
|
||||
59
lib/cli/commands-options-schema.js
Normal file
59
lib/cli/commands-options-schema.js
Normal file
@ -0,0 +1,59 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
help: {
|
||||
usage: 'Show this message',
|
||||
shortcut: 'h',
|
||||
type: 'boolean'
|
||||
},
|
||||
version: {
|
||||
usage: 'Show version info',
|
||||
shortcut: 'v',
|
||||
type: 'boolean'
|
||||
},
|
||||
verbose: {
|
||||
usage: 'Show verbose logs',
|
||||
type: 'boolean'
|
||||
},
|
||||
debug: {
|
||||
usage: 'Namespace of debug logs to expose (use "*" to display all)',
|
||||
type: 'string'
|
||||
},
|
||||
config: {
|
||||
usage: 'Path to serverless config file',
|
||||
shortcut: 'c',
|
||||
type: 'string'
|
||||
},
|
||||
stage: {
|
||||
usage: 'Stage of the service',
|
||||
shortcut: 's',
|
||||
type: 'string'
|
||||
},
|
||||
param: {
|
||||
usage: 'Pass custom parameter values for "param" variable source (usage: --param="key=value")',
|
||||
type: 'multiple',
|
||||
},
|
||||
'region': {
|
||||
usage: 'Region of the service',
|
||||
shortcut: 'r',
|
||||
type: 'string',
|
||||
},
|
||||
'aws-profile': {
|
||||
usage: 'AWS profile to use with the command',
|
||||
type: 'string',
|
||||
},
|
||||
'app': {
|
||||
usage: 'Serverless Framework Dashboard App',
|
||||
type: 'string',
|
||||
},
|
||||
'org': {
|
||||
usage: 'Serverless Framework Dashboard Org',
|
||||
type: 'string',
|
||||
},
|
||||
'use-local-credentials': {
|
||||
usage:
|
||||
'Rely on locally resolved AWS credentials instead of loading them from ' +
|
||||
'Serverless Framework Providers. This applies only to services signed into the Serverless Framework Dashboard.',
|
||||
type: 'boolean',
|
||||
},
|
||||
};
|
||||
@ -1,11 +1,188 @@
|
||||
/**
|
||||
* Serverless Framework Default Command Schema.
|
||||
* In earlier versions of the Framework, there were multiple schemas
|
||||
* for providers and being in or outside of a Service.
|
||||
* In V.4 we have a single schema for all commands, since
|
||||
* we only support the AWS provider.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const awsServiceOptions = require('./common-options/aws-service');
|
||||
const serviceCommands = require('./service');
|
||||
const globalOptions = require('./commands-options-schema');
|
||||
|
||||
const commands = (module.exports = new Map());
|
||||
|
||||
commands.commonOptions = awsServiceOptions;
|
||||
commands.commonOptions = globalOptions;
|
||||
|
||||
commands.set('', {
|
||||
usage: 'Interactive Quickstart',
|
||||
serviceDependencyMode: 'optional',
|
||||
hasAwsExtension: true,
|
||||
options: {
|
||||
'help-interactive': {
|
||||
usage: 'Show this message',
|
||||
type: 'boolean'
|
||||
},
|
||||
'name': {
|
||||
type: 'string',
|
||||
usage: 'Name for the service.',
|
||||
},
|
||||
'template': {
|
||||
type: 'string',
|
||||
usage: 'Name of template for the service.',
|
||||
},
|
||||
'template-path': {
|
||||
type: 'string',
|
||||
usage: 'Template local path for the service.',
|
||||
},
|
||||
'template-url': {
|
||||
type: 'string',
|
||||
usage: 'Template url for the service.',
|
||||
},
|
||||
'function': {
|
||||
usage: 'Name of the function you would like the dev mode activity feed to observe.',
|
||||
type: 'string',
|
||||
shortcut: 'f',
|
||||
},
|
||||
},
|
||||
lifecycleEvents: [
|
||||
'initializeService',
|
||||
'setupAws',
|
||||
'autoUpdate',
|
||||
'end'
|
||||
],
|
||||
});
|
||||
|
||||
commands.set('config credentials', {
|
||||
usage: 'Configures a new provider profile for the Serverless Framework',
|
||||
hasAwsExtension: true,
|
||||
options: {
|
||||
provider: {
|
||||
type: 'string',
|
||||
usage: 'Name of the provider. Supported providers: aws',
|
||||
required: true,
|
||||
shortcut: 'p',
|
||||
},
|
||||
key: {
|
||||
type: 'string',
|
||||
usage: 'Access key for the provider',
|
||||
shortcut: 'k',
|
||||
required: true,
|
||||
},
|
||||
secret: {
|
||||
type: 'string',
|
||||
usage: 'Secret key for the provider',
|
||||
shortcut: 's',
|
||||
required: true,
|
||||
},
|
||||
profile: {
|
||||
type: 'string',
|
||||
usage: 'Name of the profile you wish to create. Defaults to "default"',
|
||||
shortcut: 'n',
|
||||
},
|
||||
overwrite: {
|
||||
usage: 'Overwrite the existing profile configuration in the credentials file',
|
||||
shortcut: 'o',
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
lifecycleEvents: [
|
||||
'config'
|
||||
],
|
||||
});
|
||||
|
||||
commands.set('config', {
|
||||
usage: 'Configure Serverless',
|
||||
options: {
|
||||
autoupdate: {
|
||||
usage: 'Turn on auto update mechanism (turn off via "--no-autoupdate")',
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
lifecycleEvents: [
|
||||
'config'
|
||||
],
|
||||
});
|
||||
|
||||
commands.set('plugin install', {
|
||||
usage: 'Install and add a plugin to your service',
|
||||
options: {
|
||||
name: {
|
||||
type: 'string',
|
||||
usage: 'The plugin name',
|
||||
required: true,
|
||||
shortcut: 'n',
|
||||
},
|
||||
},
|
||||
lifecycleEvents: [
|
||||
'install'
|
||||
],
|
||||
serviceDependencyMode: 'required'
|
||||
});
|
||||
|
||||
commands.set('plugin uninstall', {
|
||||
usage: 'Uninstall and remove a plugin from your service',
|
||||
options: {
|
||||
name: {
|
||||
type: 'string',
|
||||
usage: 'The plugin name',
|
||||
required: true,
|
||||
shortcut: 'n',
|
||||
},
|
||||
},
|
||||
lifecycleEvents: [
|
||||
'uninstall'
|
||||
],
|
||||
serviceDependencyMode: 'required'
|
||||
});
|
||||
|
||||
commands.set('print', {
|
||||
usage: 'Print your compiled and resolved config file',
|
||||
hasAwsExtension: true,
|
||||
options: {
|
||||
format: {
|
||||
type: 'string',
|
||||
usage: 'Print configuration in given format ("yaml", "json", "text"). Default: yaml',
|
||||
},
|
||||
path: {
|
||||
type: 'string',
|
||||
usage: 'Optional period-separated path to print a sub-value (eg: "provider.name")',
|
||||
},
|
||||
transform: {
|
||||
type: 'string',
|
||||
usage: 'Optional transform-function to apply to the value ("keys")',
|
||||
},
|
||||
},
|
||||
lifecycleEvents: ['print'],
|
||||
serviceDependencyMode: 'required'
|
||||
});
|
||||
|
||||
commands.set('package', {
|
||||
usage: 'Packages a Serverless Service',
|
||||
hasAwsExtension: true,
|
||||
options: {
|
||||
'package': {
|
||||
type: 'string',
|
||||
usage: 'Output path for the package',
|
||||
shortcut: 'p',
|
||||
},
|
||||
'minify-template': {
|
||||
usage: 'Minify the AWS CloudFormation template for AWS packages',
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
lifecycleEvents: [
|
||||
'cleanup',
|
||||
'initialize',
|
||||
'setupProviderConfiguration',
|
||||
'createDeploymentArtifacts',
|
||||
'compileLayers',
|
||||
'compileFunctions',
|
||||
'compileEvents',
|
||||
'finalize',
|
||||
],
|
||||
serviceDependencyMode: 'required',
|
||||
});
|
||||
|
||||
commands.set('deploy', {
|
||||
groupName: 'main',
|
||||
@ -16,6 +193,7 @@ commands.set('deploy', {
|
||||
type: 'boolean',
|
||||
},
|
||||
'package': {
|
||||
type: 'string',
|
||||
usage: 'Path of the deployment package',
|
||||
shortcut: 'p',
|
||||
},
|
||||
@ -38,6 +216,8 @@ commands.set('deploy', {
|
||||
},
|
||||
},
|
||||
lifecycleEvents: ['deploy', 'finalize'],
|
||||
serviceDependencyMode: 'required',
|
||||
hasAwsExtension: true
|
||||
});
|
||||
|
||||
commands.set('deploy function', {
|
||||
@ -45,6 +225,7 @@ commands.set('deploy function', {
|
||||
usage: 'Deploy a single function from the service',
|
||||
options: {
|
||||
'function': {
|
||||
type: 'string',
|
||||
usage: 'Name of the function',
|
||||
shortcut: 'f',
|
||||
required: true,
|
||||
@ -60,15 +241,24 @@ commands.set('deploy function', {
|
||||
},
|
||||
},
|
||||
lifecycleEvents: ['initialize', 'packageFunction', 'deploy'],
|
||||
serviceDependencyMode: 'required',
|
||||
hasAwsExtension: true
|
||||
});
|
||||
|
||||
commands.set('deploy list', {
|
||||
usage: 'List deployed version of your Serverless Service',
|
||||
options: {},
|
||||
lifecycleEvents: ['log'],
|
||||
serviceDependencyMode: 'required',
|
||||
hasAwsExtension: true
|
||||
});
|
||||
|
||||
commands.set('deploy list functions', {
|
||||
usage: 'List all the deployed functions and their versions',
|
||||
options: {},
|
||||
lifecycleEvents: ['log'],
|
||||
serviceDependencyMode: 'required',
|
||||
hasAwsExtension: true
|
||||
});
|
||||
|
||||
commands.set('info', {
|
||||
@ -81,6 +271,8 @@ commands.set('info', {
|
||||
},
|
||||
},
|
||||
lifecycleEvents: ['info'],
|
||||
serviceDependencyMode: 'required',
|
||||
hasAwsExtension: true
|
||||
});
|
||||
|
||||
commands.set('invoke', {
|
||||
@ -88,19 +280,23 @@ commands.set('invoke', {
|
||||
usage: 'Invoke a deployed function',
|
||||
options: {
|
||||
function: {
|
||||
type: 'string',
|
||||
usage: 'The function name',
|
||||
required: true,
|
||||
shortcut: 'f',
|
||||
},
|
||||
qualifier: {
|
||||
type: 'string',
|
||||
usage: 'Version number or alias to invoke',
|
||||
shortcut: 'q',
|
||||
},
|
||||
path: {
|
||||
type: 'string',
|
||||
usage: 'Path to JSON or YAML file holding input data',
|
||||
shortcut: 'p',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
usage: 'Type of invocation',
|
||||
shortcut: 't',
|
||||
},
|
||||
@ -110,6 +306,7 @@ commands.set('invoke', {
|
||||
type: 'boolean',
|
||||
},
|
||||
data: {
|
||||
type: 'string',
|
||||
usage: 'Input data',
|
||||
shortcut: 'd',
|
||||
},
|
||||
@ -118,13 +315,17 @@ commands.set('invoke', {
|
||||
type: 'boolean',
|
||||
},
|
||||
context: {
|
||||
type: 'string',
|
||||
usage: 'Context of the service',
|
||||
},
|
||||
contextPath: {
|
||||
type: 'string',
|
||||
usage: 'Path to JSON or YAML file holding context data',
|
||||
},
|
||||
},
|
||||
lifecycleEvents: ['invoke'],
|
||||
serviceDependencyMode: 'required',
|
||||
hasAwsExtension: true
|
||||
});
|
||||
|
||||
commands.set('invoke local', {
|
||||
@ -132,15 +333,18 @@ commands.set('invoke local', {
|
||||
usage: 'Invoke function locally',
|
||||
options: {
|
||||
'function': {
|
||||
type: 'string',
|
||||
usage: 'Name of the function',
|
||||
shortcut: 'f',
|
||||
required: true,
|
||||
},
|
||||
'path': {
|
||||
type: 'string',
|
||||
usage: 'Path to JSON or YAML file holding input data',
|
||||
shortcut: 'p',
|
||||
},
|
||||
'data': {
|
||||
type: 'string',
|
||||
usage: 'input data',
|
||||
shortcut: 'd',
|
||||
},
|
||||
@ -149,9 +353,11 @@ commands.set('invoke local', {
|
||||
type: 'boolean',
|
||||
},
|
||||
'context': {
|
||||
type: 'string',
|
||||
usage: 'Context of the service',
|
||||
},
|
||||
'contextPath': {
|
||||
type: 'string',
|
||||
usage: 'Path to JSON or YAML file holding context data',
|
||||
shortcut: 'x',
|
||||
},
|
||||
@ -160,12 +366,18 @@ commands.set('invoke local', {
|
||||
shortcut: 'e',
|
||||
type: 'multiple',
|
||||
},
|
||||
'docker': { usage: 'Flag to turn on docker use for node/python/ruby/java', type: 'boolean' },
|
||||
'docker': {
|
||||
usage: 'Flag to turn on docker use for node/python/ruby/java',
|
||||
type: 'boolean'
|
||||
},
|
||||
'docker-arg': {
|
||||
type: 'string',
|
||||
usage: 'Arguments to docker run command. e.g. --docker-arg "-p 9229:9229"',
|
||||
},
|
||||
},
|
||||
lifecycleEvents: ['loadEnvVars', 'invoke'],
|
||||
serviceDependencyMode: 'required',
|
||||
hasAwsExtension: true
|
||||
});
|
||||
|
||||
commands.set('logs', {
|
||||
@ -173,6 +385,7 @@ commands.set('logs', {
|
||||
usage: 'Output the logs of a deployed function',
|
||||
options: {
|
||||
function: {
|
||||
type: 'string',
|
||||
usage: 'The function name',
|
||||
required: true,
|
||||
shortcut: 'f',
|
||||
@ -183,106 +396,107 @@ commands.set('logs', {
|
||||
type: 'boolean',
|
||||
},
|
||||
startTime: {
|
||||
type: 'string',
|
||||
usage:
|
||||
'Logs before this time will not be displayed. Default: `10m` (last 10 minutes logs only)',
|
||||
},
|
||||
filter: {
|
||||
type: 'string',
|
||||
usage: 'A filter pattern',
|
||||
},
|
||||
interval: {
|
||||
type: 'string',
|
||||
usage: 'Tail polling interval in milliseconds. Default: `1000`',
|
||||
shortcut: 'i',
|
||||
},
|
||||
},
|
||||
lifecycleEvents: ['logs'],
|
||||
serviceDependencyMode: 'required',
|
||||
hasAwsExtension: true
|
||||
});
|
||||
|
||||
commands.set('metrics', {
|
||||
usage: 'Show metrics for a specific function',
|
||||
options: {
|
||||
function: {
|
||||
type: 'string',
|
||||
usage: 'The function name',
|
||||
shortcut: 'f',
|
||||
},
|
||||
startTime: {
|
||||
type: 'string',
|
||||
usage: 'Start time for the metrics retrieval (e.g. 1970-01-01)',
|
||||
},
|
||||
endTime: {
|
||||
type: 'string',
|
||||
usage: 'End time for the metrics retrieval (e.g. 1970-01-01)',
|
||||
},
|
||||
},
|
||||
lifecycleEvents: ['metrics'],
|
||||
serviceDependencyMode: 'required',
|
||||
hasAwsExtension: true
|
||||
});
|
||||
|
||||
commands.set('remove', {
|
||||
usage: 'Remove Serverless service and all resources',
|
||||
options: {},
|
||||
lifecycleEvents: ['remove'],
|
||||
serviceDependencyMode: 'required',
|
||||
hasAwsExtension: true
|
||||
});
|
||||
|
||||
commands.set('rollback', {
|
||||
usage: 'Rollback the Serverless service to a specific deployment',
|
||||
options: {
|
||||
timestamp: {
|
||||
type: 'string',
|
||||
usage: 'Timestamp of the deployment (list deployments with `serverless deploy list`)',
|
||||
shortcut: 't',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
lifecycleEvents: ['initialize', 'rollback'],
|
||||
serviceDependencyMode: 'required',
|
||||
hasAwsExtension: true
|
||||
});
|
||||
|
||||
commands.set('rollback function', {
|
||||
usage: 'Rollback the function to the previous version',
|
||||
options: {
|
||||
'function': {
|
||||
type: 'string',
|
||||
usage: 'Name of the function',
|
||||
shortcut: 'f',
|
||||
required: true,
|
||||
},
|
||||
'function-version': {
|
||||
type: 'string',
|
||||
usage: 'Version of the function',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
lifecycleEvents: ['rollback'],
|
||||
serviceDependencyMode: 'required',
|
||||
hasAwsExtension: true
|
||||
});
|
||||
|
||||
commands.set('test', {
|
||||
usage: 'Run HTTP tests',
|
||||
options: {
|
||||
function: {
|
||||
type: 'string',
|
||||
usage: 'Specify the function to test',
|
||||
shortcut: 'f',
|
||||
},
|
||||
test: {
|
||||
type: 'string',
|
||||
usage: 'Specify a specific test to run',
|
||||
shortcut: 't',
|
||||
},
|
||||
},
|
||||
lifecycleEvents: ['test'],
|
||||
hasAwsExtension: true,
|
||||
serviceDependencyMode: 'required',
|
||||
});
|
||||
|
||||
for (const schema of commands.values()) {
|
||||
schema.serviceDependencyMode = 'required';
|
||||
schema.hasAwsExtension = true;
|
||||
if (!schema.options) schema.options = {};
|
||||
for (const optionSchema of Object.values(schema.options)) {
|
||||
if (!optionSchema.type) optionSchema.type = 'string';
|
||||
}
|
||||
|
||||
Object.assign(schema.options, awsServiceOptions);
|
||||
}
|
||||
|
||||
for (const [name, schema] of serviceCommands) commands.set(name, schema);
|
||||
|
||||
const packageCommandConfig = commands.get('package');
|
||||
commands.set('package', {
|
||||
...packageCommandConfig,
|
||||
options: {
|
||||
...packageCommandConfig.options,
|
||||
'minify-template': {
|
||||
usage: 'Minify the CloudFormation template for AWS packages',
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -1,24 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
'region': {
|
||||
usage: 'Region of the service',
|
||||
shortcut: 'r',
|
||||
},
|
||||
'aws-profile': {
|
||||
usage: 'AWS profile to use with the command',
|
||||
},
|
||||
'app': { usage: 'Serverless Framework Dashboard app' },
|
||||
'org': { usage: 'Serverless Framework Dashboard org' },
|
||||
'use-local-credentials': {
|
||||
usage:
|
||||
'Rely on locally resolved AWS credentials instead of loading them from ' +
|
||||
'Serverless Framework Providers. This applies only to services signed into the Serverless Framework Dashboard.',
|
||||
type: 'boolean',
|
||||
},
|
||||
...require('./service'),
|
||||
};
|
||||
|
||||
for (const optionSchema of Object.values(module.exports)) {
|
||||
if (!optionSchema.type) optionSchema.type = 'string';
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
help: { usage: 'Show this message', shortcut: 'h', type: 'boolean' },
|
||||
version: { usage: 'Show version info', shortcut: 'v', type: 'boolean' },
|
||||
verbose: { usage: 'Show verbose logs', type: 'boolean' },
|
||||
debug: { usage: 'Namespace of debug logs to expose (use "*" to display all)', type: 'string' },
|
||||
};
|
||||
|
||||
for (const optionSchema of Object.values(module.exports)) {
|
||||
if (!optionSchema.type) optionSchema.type = 'string';
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
config: {
|
||||
usage: 'Path to serverless config file',
|
||||
shortcut: 'c',
|
||||
},
|
||||
stage: {
|
||||
usage: 'Stage of the service',
|
||||
shortcut: 's',
|
||||
},
|
||||
param: {
|
||||
usage: 'Pass custom parameter values for "param" variable source (usage: --param="key=value")',
|
||||
type: 'multiple',
|
||||
},
|
||||
...require('./global'),
|
||||
};
|
||||
|
||||
for (const optionSchema of Object.values(module.exports)) {
|
||||
if (!optionSchema.type) optionSchema.type = 'string';
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = require('./aws-service');
|
||||
@ -1,228 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const globalOptions = require('./common-options/global');
|
||||
const serviceOptions = require('./common-options/service');
|
||||
const awsServiceOptions = require('./common-options/aws-service');
|
||||
|
||||
const commands = (module.exports = new Map());
|
||||
|
||||
commands.commonOptions = globalOptions;
|
||||
|
||||
commands.set('', {
|
||||
usage: 'Interactive Quickstart',
|
||||
serviceDependencyMode: 'optional',
|
||||
hasAwsExtension: true,
|
||||
options: {
|
||||
'help-interactive': { usage: 'Show this message', type: 'boolean' },
|
||||
'name': {
|
||||
usage: 'Name for the service.',
|
||||
},
|
||||
'template': {
|
||||
usage: 'Name of template for the service.',
|
||||
},
|
||||
'template-path': {
|
||||
usage: 'Template local path for the service.',
|
||||
},
|
||||
'template-url': {
|
||||
usage: 'Template url for the service.',
|
||||
},
|
||||
'function': {
|
||||
usage: 'Name of the function you would like the dev mode activity feed to observe.',
|
||||
type: 'string',
|
||||
shortcut: 'f',
|
||||
},
|
||||
},
|
||||
lifecycleEvents: ['initializeService', 'setupAws', 'autoUpdate', 'end'],
|
||||
});
|
||||
|
||||
commands.set('config', {
|
||||
usage: 'Configure Serverless',
|
||||
options: {
|
||||
autoupdate: {
|
||||
usage: 'Turn on auto update mechanism (turn off via "--no-autoupdate")',
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
lifecycleEvents: ['config'],
|
||||
});
|
||||
|
||||
commands.set('config credentials', {
|
||||
usage: 'Configures a new provider profile for the Serverless Framework',
|
||||
hasAwsExtension: true,
|
||||
options: {
|
||||
provider: {
|
||||
usage: 'Name of the provider. Supported providers: aws',
|
||||
required: true,
|
||||
shortcut: 'p',
|
||||
},
|
||||
key: {
|
||||
usage: 'Access key for the provider',
|
||||
shortcut: 'k',
|
||||
required: true,
|
||||
},
|
||||
secret: {
|
||||
usage: 'Secret key for the provider',
|
||||
shortcut: 's',
|
||||
required: true,
|
||||
},
|
||||
profile: {
|
||||
usage: 'Name of the profile you wish to create. Defaults to "default"',
|
||||
shortcut: 'n',
|
||||
},
|
||||
overwrite: {
|
||||
usage: 'Overwrite the existing profile configuration in the credentials file',
|
||||
shortcut: 'o',
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
lifecycleEvents: ['config'],
|
||||
});
|
||||
|
||||
commands.set('create', {
|
||||
usage: 'Create new Serverless service',
|
||||
options: {
|
||||
'template': {
|
||||
usage:
|
||||
'Template for the service. Available templates: ' +
|
||||
`${require('../../templates/recommended-list/human-readable')}`,
|
||||
shortcut: 't',
|
||||
},
|
||||
'template-url': {
|
||||
usage: 'Template URL for the service. Supports: GitHub, BitBucket',
|
||||
shortcut: 'u',
|
||||
},
|
||||
'template-path': {
|
||||
usage: 'Template local path for the service.',
|
||||
},
|
||||
'path': {
|
||||
usage: 'The path where the service should be created (e.g. --path my-service)',
|
||||
shortcut: 'p',
|
||||
},
|
||||
'name': {
|
||||
usage: 'Name for the service. Overwrites the default name of the created service.',
|
||||
shortcut: 'n',
|
||||
},
|
||||
},
|
||||
lifecycleEvents: ['create'],
|
||||
});
|
||||
|
||||
commands.set('doctor', {
|
||||
usage: 'Print status on reported deprecations triggered in the last command run',
|
||||
});
|
||||
|
||||
commands.set('generate-event', {
|
||||
usage: 'Generate event',
|
||||
lifecycleEvents: ['generate-event'],
|
||||
options: {
|
||||
type: {
|
||||
usage:
|
||||
'Specify event type. "aws:apiGateway", "aws:sns", "aws:sqs", "aws:dynamo", ' +
|
||||
'"aws:kinesis", "aws:cloudWatchLog", "aws:s3", "aws:alexaSmartHome", "aws:alexaSkill", ' +
|
||||
'"aws:cloudWatch", "aws:iot", "aws:cognitoUserPool","aws:websocket" are supported.',
|
||||
shortcut: 't',
|
||||
required: true,
|
||||
},
|
||||
body: {
|
||||
usage: 'Specify the body for the message, request, or stream event.',
|
||||
shortcut: 'b',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
commands.set('help', {
|
||||
usage: 'Show this help',
|
||||
serviceDependencyMode: 'optional',
|
||||
});
|
||||
|
||||
commands.set('install', {
|
||||
usage: 'Install a Serverless service from GitHub or a plugin from the Serverless registry',
|
||||
options: {
|
||||
url: {
|
||||
usage: 'URL of the Serverless service on GitHub',
|
||||
required: true,
|
||||
shortcut: 'u',
|
||||
},
|
||||
name: {
|
||||
usage: 'Name for the service',
|
||||
shortcut: 'n',
|
||||
},
|
||||
},
|
||||
lifecycleEvents: ['install'],
|
||||
});
|
||||
|
||||
commands.set('plugin list', {
|
||||
usage: 'Lists all available plugins',
|
||||
lifecycleEvents: ['list'],
|
||||
});
|
||||
|
||||
commands.set('plugin search', {
|
||||
usage: 'Search for plugins',
|
||||
options: {
|
||||
query: {
|
||||
usage: 'Search query',
|
||||
required: true,
|
||||
shortcut: 'q',
|
||||
},
|
||||
},
|
||||
lifecycleEvents: ['search'],
|
||||
});
|
||||
|
||||
commands.set('slstats', {
|
||||
usage: 'Enable or disable stats',
|
||||
options: {
|
||||
enable: {
|
||||
usage: 'Enable stats ("--enable")',
|
||||
shortcut: 'e',
|
||||
type: 'boolean',
|
||||
},
|
||||
disable: {
|
||||
usage: 'Disable stats ("--disable")',
|
||||
shortcut: 'd',
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
lifecycleEvents: ['slstats'],
|
||||
});
|
||||
|
||||
(() => {
|
||||
const isHidden = !require('../../utils/is-standalone-executable') || process.platform === 'win32';
|
||||
const noSupportNotice =
|
||||
"It's applicable only in context of a standalone executable instance " +
|
||||
'in non Windows environment.';
|
||||
|
||||
commands.set('upgrade', {
|
||||
usage: 'Upgrade Serverless',
|
||||
isHidden,
|
||||
noSupportNotice,
|
||||
options: {
|
||||
major: {
|
||||
usage: 'Enable upgrade to a new major release',
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
lifecycleEvents: ['upgrade'],
|
||||
});
|
||||
commands.set('uninstall', {
|
||||
usage: 'Uninstall Serverless',
|
||||
isHidden,
|
||||
noSupportNotice,
|
||||
lifecycleEvents: ['uninstall'],
|
||||
});
|
||||
})();
|
||||
|
||||
for (const [name, schema] of commands) {
|
||||
if (!schema.options) schema.options = {};
|
||||
for (const optionSchema of Object.values(schema.options)) {
|
||||
if (!optionSchema.type) optionSchema.type = 'string';
|
||||
}
|
||||
if (schema.serviceDependencyMode) {
|
||||
Object.assign(schema.options, schema.hasAwsExtension ? awsServiceOptions : serviceOptions);
|
||||
} else {
|
||||
Object.assign(schema.options, globalOptions);
|
||||
}
|
||||
if (!name) {
|
||||
// Necessary tweaks for Interactive CLI help
|
||||
schema.options.help = { ...schema.options.help, usage: 'Show general help info' };
|
||||
schema.options.version = { ...schema.options.version, shortcut: 'v' };
|
||||
}
|
||||
}
|
||||
@ -1,124 +0,0 @@
|
||||
// Resolves final schema of commands for given service configuration
|
||||
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
|
||||
const serviceCommands = require('./service');
|
||||
const awsServiceCommands = require('./aws-service');
|
||||
const serviceOptions = require('./common-options/service');
|
||||
const awsServiceOptions = require('./common-options/aws-service');
|
||||
const logDeprecation = require('../../utils/log-deprecation');
|
||||
|
||||
module.exports = (loadedPlugins, { providerName, configuration }) => {
|
||||
const commands = new Map(providerName === 'aws' ? awsServiceCommands : serviceCommands);
|
||||
|
||||
if (providerName !== 'aws') {
|
||||
// Recognize AWS provider commands adapted in context of other provider
|
||||
// Those commands do not have to be defined as "commands" in plugin.
|
||||
// It's good enough if hooks for command lifecycle events are setup
|
||||
// and our detection confirms on that.
|
||||
const optionalServiceCommandsHooksMap = new Map(
|
||||
Array.from(awsServiceCommands)
|
||||
.filter(([name]) => !serviceCommands.has(name))
|
||||
.map(([name, schema]) => {
|
||||
const lifecycleEventNamePrefix = name.split(' ').join(':');
|
||||
return (schema.lifecycleEvents || []).map((lifecycleEventBaseName) => {
|
||||
const lifecycleEventName = `${lifecycleEventNamePrefix}:${lifecycleEventBaseName}`;
|
||||
return [
|
||||
[`before:${lifecycleEventName}`, name],
|
||||
[lifecycleEventName, name],
|
||||
[`after:${lifecycleEventName}`, name],
|
||||
];
|
||||
});
|
||||
})
|
||||
.flat(2)
|
||||
);
|
||||
|
||||
const awsSpecificOptionNames = new Set(
|
||||
Object.keys(awsServiceOptions).filter((optionName) => !serviceOptions[optionName])
|
||||
);
|
||||
|
||||
for (const loadedPlugin of loadedPlugins) {
|
||||
if (!loadedPlugin.hooks) continue;
|
||||
for (const hookName of Object.keys(loadedPlugin.hooks)) {
|
||||
const awsCommandName = optionalServiceCommandsHooksMap.get(hookName);
|
||||
if (awsCommandName && !commands.has(awsCommandName)) {
|
||||
const schema = _.merge({}, awsServiceCommands.get(awsCommandName), {
|
||||
isExtension: true,
|
||||
sourcePlugin: loadedPlugin,
|
||||
});
|
||||
for (const awsSpecificOptionName of awsSpecificOptionNames) {
|
||||
delete schema.options[awsSpecificOptionName];
|
||||
}
|
||||
commands.set(awsCommandName, schema);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const missingOptionTypes = new Map();
|
||||
const commonOptions = providerName === 'aws' ? awsServiceOptions : serviceOptions;
|
||||
commands.commonOptions = commonOptions;
|
||||
const resolveCommands = (loadedPlugin, config, commandPrefix = '') => {
|
||||
if (!config.commands) return;
|
||||
for (const [commandName, commandConfig] of Object.entries(config.commands)) {
|
||||
if (commandConfig.type === 'entrypoint') continue;
|
||||
const fullCommandName = `${commandPrefix}${commandName}`;
|
||||
if (commandConfig.type !== 'container') {
|
||||
const schema = commands.has(fullCommandName)
|
||||
? _.merge({}, commands.get(fullCommandName))
|
||||
: {
|
||||
usage: commandConfig.usage,
|
||||
serviceDependencyMode: 'required',
|
||||
isExtension: true,
|
||||
sourcePlugin: loadedPlugin,
|
||||
isHidden: commandConfig.isHidden,
|
||||
noSupportNotice: commandConfig.noSupportNotice,
|
||||
options: {},
|
||||
};
|
||||
if (commandConfig.lifecycleEvents) schema.lifecycleEvents = commandConfig.lifecycleEvents;
|
||||
if (commandConfig.options) {
|
||||
for (const [optionName, optionConfig] of Object.entries(commandConfig.options)) {
|
||||
if (!schema.options[optionName]) {
|
||||
schema.options[optionName] = optionConfig;
|
||||
if (!optionConfig.type) {
|
||||
if (!missingOptionTypes.has(loadedPlugin)) {
|
||||
missingOptionTypes.set(loadedPlugin, new Set());
|
||||
}
|
||||
missingOptionTypes.get(loadedPlugin).add(optionName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Put common options to end of index
|
||||
for (const optionName of Object.keys(commonOptions)) delete schema.options[optionName];
|
||||
Object.assign(schema.options, commonOptions);
|
||||
|
||||
commands.set(fullCommandName, schema);
|
||||
}
|
||||
resolveCommands(loadedPlugin, commandConfig, `${fullCommandName} `);
|
||||
}
|
||||
};
|
||||
|
||||
for (const loadedPlugin of loadedPlugins) resolveCommands(loadedPlugin, loadedPlugin);
|
||||
|
||||
if (missingOptionTypes.size) {
|
||||
logDeprecation(
|
||||
'CLI_OPTIONS_SCHEMA_V3',
|
||||
'CLI options definitions were upgraded with "type" property (which could be one of "string", "boolean", "multiple"). ' +
|
||||
'Below listed plugins do not predefine type for introduced options:\n' +
|
||||
` - ${Array.from(
|
||||
missingOptionTypes,
|
||||
([plugin, optionNames]) =>
|
||||
`${plugin.constructor.name} for "${Array.from(optionNames).join('", "')}"`
|
||||
).join('\n - ')}\n` +
|
||||
'Please report this issue in plugin issue tracker.\n' +
|
||||
'Starting with next major release, this will be communicated with a thrown error.',
|
||||
{ serviceConfig: configuration }
|
||||
);
|
||||
}
|
||||
|
||||
return commands;
|
||||
};
|
||||
@ -1,82 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const serviceOptions = require('./common-options/service');
|
||||
const awsServiceOptions = require('./common-options/aws-service');
|
||||
const noServiceCommands = require('./no-service');
|
||||
|
||||
const commands = (module.exports = new Map());
|
||||
|
||||
commands.commonOptions = serviceOptions;
|
||||
|
||||
commands.set('package', {
|
||||
usage: 'Packages a Serverless service',
|
||||
hasAwsExtension: true,
|
||||
options: {
|
||||
package: {
|
||||
usage: 'Output path for the package',
|
||||
shortcut: 'p',
|
||||
},
|
||||
},
|
||||
lifecycleEvents: [
|
||||
'cleanup',
|
||||
'initialize',
|
||||
'setupProviderConfiguration',
|
||||
'createDeploymentArtifacts',
|
||||
'compileLayers',
|
||||
'compileFunctions',
|
||||
'compileEvents',
|
||||
'finalize',
|
||||
],
|
||||
});
|
||||
|
||||
commands.set('plugin install', {
|
||||
usage: 'Install and add a plugin to your service',
|
||||
options: {
|
||||
name: {
|
||||
usage: 'The plugin name',
|
||||
required: true,
|
||||
shortcut: 'n',
|
||||
},
|
||||
},
|
||||
lifecycleEvents: ['install'],
|
||||
});
|
||||
|
||||
commands.set('plugin uninstall', {
|
||||
usage: 'Uninstall and remove a plugin from your service',
|
||||
options: {
|
||||
name: {
|
||||
usage: 'The plugin name',
|
||||
required: true,
|
||||
shortcut: 'n',
|
||||
},
|
||||
},
|
||||
lifecycleEvents: ['uninstall'],
|
||||
});
|
||||
|
||||
commands.set('print', {
|
||||
usage: 'Print your compiled and resolved config file',
|
||||
hasAwsExtension: true,
|
||||
options: {
|
||||
format: {
|
||||
usage: 'Print configuration in given format ("yaml", "json", "text"). Default: yaml',
|
||||
},
|
||||
path: {
|
||||
usage: 'Optional period-separated path to print a sub-value (eg: "provider.name")',
|
||||
},
|
||||
transform: {
|
||||
usage: 'Optional transform-function to apply to the value ("keys")',
|
||||
},
|
||||
},
|
||||
lifecycleEvents: ['print'],
|
||||
});
|
||||
|
||||
for (const schema of commands.values()) {
|
||||
schema.serviceDependencyMode = 'required';
|
||||
if (!schema.options) schema.options = {};
|
||||
for (const optionSchema of Object.values(schema.options)) {
|
||||
if (!optionSchema.type) optionSchema.type = 'string';
|
||||
}
|
||||
Object.assign(schema.options, schema.hasAwsExtension ? awsServiceOptions : serviceOptions);
|
||||
}
|
||||
|
||||
for (const [name, schema] of noServiceCommands) commands.set(name, schema);
|
||||
@ -1,12 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
const serviceOptionsNames = Object.keys(require('./commands-schema/common-options/service'));
|
||||
const awsServiceOptionsNames = Object.keys(require('./commands-schema/common-options/aws-service'));
|
||||
const globalOptions = require('./commands-options-schema');
|
||||
|
||||
module.exports = (options, { commandSchema, providerName }) => {
|
||||
module.exports = (options, { commandSchema }) => {
|
||||
const supportedNames = (() => {
|
||||
if (commandSchema) return Object.keys(commandSchema.options);
|
||||
return providerName === 'aws' ? awsServiceOptionsNames : serviceOptionsNames;
|
||||
return globalOptions;
|
||||
})();
|
||||
const result = Object.create(null);
|
||||
for (const name of supportedNames) result[name] = options[name] == null ? null : options[name];
|
||||
|
||||
@ -7,9 +7,6 @@ const slsVersion = require('./../../package').version;
|
||||
const isStandaloneExecutable = require('../utils/is-standalone-executable');
|
||||
const tokenizeException = require('../utils/tokenize-exception');
|
||||
const isLocallyInstalled = require('./is-locally-installed');
|
||||
const resolveErrorLocation = require('../utils/telemetry/resolve-error-location');
|
||||
|
||||
const isErrorCodeNormative = RegExp.prototype.test.bind(/^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*$/);
|
||||
|
||||
module.exports = async (exception, options = {}) => {
|
||||
if (!isObject(options)) options = {};
|
||||
@ -70,10 +67,5 @@ module.exports = async (exception, options = {}) => {
|
||||
|
||||
process.exitCode = 1;
|
||||
|
||||
const failureReason = { kind: isUserError ? 'user' : 'programmer', code: exception.code };
|
||||
if (!isUserError || !exception.code || !isErrorCodeNormative(exception.code)) {
|
||||
failureReason.location = resolveErrorLocation(exceptionTokens);
|
||||
}
|
||||
|
||||
return { telemetryData: { failureReason, outcome: 'failure' } };
|
||||
return { };
|
||||
};
|
||||
|
||||
@ -1,4 +1,21 @@
|
||||
// TODO: Consider publishing as independent package
|
||||
/**
|
||||
* Exports a function that parses command
|
||||
* line arguments provided as an array (args).
|
||||
* It categorizes these arguments based on their
|
||||
* types (boolean, string, multiple, alias) and
|
||||
* stores them in a result object. It also handles
|
||||
* special cases such as help requests (-h or --help)
|
||||
* and unexpected values for aliases.
|
||||
*
|
||||
* Example return value:
|
||||
* {
|
||||
* _: ['otherArgs'],
|
||||
* booleanArg: true,
|
||||
* stringArg: 'stringValue',
|
||||
* multipleArg: ['value1', 'value2'],
|
||||
* aliasArg: 'aliasValue'
|
||||
* }
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
const { writeText, style } = require('@serverless/utils/log');
|
||||
const { version } = require('../../../package');
|
||||
const globalOptions = require('../commands-schema/common-options/global');
|
||||
const globalOptions = require('../commands-options-schema');
|
||||
const resolveInput = require('../resolve-input');
|
||||
const generateCommandUsage = require('./generate-command-usage');
|
||||
const renderOptions = require('./options');
|
||||
|
||||
@ -1,19 +1,12 @@
|
||||
'use strict';
|
||||
|
||||
const resolveInput = require('../resolve-input');
|
||||
const renderInteractiveSetupHelp = require('./interactive-setup');
|
||||
const renderGeneralHelp = require('./general');
|
||||
const renderCommandHelp = require('./command');
|
||||
|
||||
module.exports = (loadedPlugins) => {
|
||||
const { command, options } = resolveInput();
|
||||
if (!command) {
|
||||
if (options['help-interactive']) {
|
||||
renderInteractiveSetupHelp();
|
||||
return;
|
||||
}
|
||||
renderGeneralHelp(loadedPlugins);
|
||||
} else if (command === 'help') {
|
||||
const { command } = resolveInput();
|
||||
if (command === 'help') {
|
||||
renderGeneralHelp(loadedPlugins);
|
||||
} else {
|
||||
renderCommandHelp(command);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const { writeText, style } = require('@serverless/utils/log');
|
||||
const commmandSchema = require('../commands-schema/no-service').get('');
|
||||
const commandSchema = require('../commands-schema').get('');
|
||||
const renderOptions = require('./options');
|
||||
|
||||
module.exports = () => {
|
||||
@ -12,5 +12,5 @@ module.exports = () => {
|
||||
style.aside('Options')
|
||||
);
|
||||
|
||||
renderOptions(commmandSchema.options);
|
||||
renderOptions(commandSchema.options);
|
||||
};
|
||||
|
||||
97
lib/cli/resolve-input-final.js
Normal file
97
lib/cli/resolve-input-final.js
Normal file
@ -0,0 +1,97 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const commandSchema = require('./commands-schema');
|
||||
const serviceOptions = require('./commands-options-schema');
|
||||
const logDeprecation = require('../utils/log-deprecation');
|
||||
|
||||
/**
|
||||
* This module exports a function that processes loaded plugins and their
|
||||
* configurations, mapping out their commands and options into a commands Map.
|
||||
* It imports AWS service commands and common options, and merges them with
|
||||
* the commands and options from the loaded plugins. It also handles missing
|
||||
* option types by logging a deprecation warning.
|
||||
*
|
||||
* @param {Array} loadedPlugins - An array of loaded plugin objects.
|
||||
* @param {Object} configuration - The configuration object for the plugins.
|
||||
* @returns {Map} A Map object of commands and their options.
|
||||
*/
|
||||
module.exports = (loadedPlugins, { configuration }) => {
|
||||
const commands = new Map(commandSchema);
|
||||
const missingOptionTypes = new Map();
|
||||
const commonOptions = serviceOptions;
|
||||
commands.commonOptions = commonOptions;
|
||||
|
||||
/**
|
||||
* Recursively processes a configuration object for a plugin, mapping out its
|
||||
* commands and their options. It skips commands of type 'entrypoint', and for
|
||||
* other commands, it creates or updates a schema with details like usage,
|
||||
* lifecycle events, and options. If an option is missing the 'type' property,
|
||||
* it is added to the `missingOptionTypes` set for later processing.
|
||||
*
|
||||
* @param {Object} loadedPlugin - The loaded plugin object.
|
||||
* @param {Object} config - The configuration object for the plugin.
|
||||
* @param {string} commandPrefix - The prefix for the command (default is '').
|
||||
*/
|
||||
const resolveCommands = (loadedPlugin, config, commandPrefix = '') => {
|
||||
if (!config.commands) return;
|
||||
for (const [commandName, commandConfig] of Object.entries(config.commands)) {
|
||||
if (commandConfig.type === 'entrypoint') continue;
|
||||
const fullCommandName = `${commandPrefix}${commandName}`;
|
||||
if (commandConfig.type !== 'container') {
|
||||
const schema = commands.has(fullCommandName)
|
||||
? _.merge({}, commands.get(fullCommandName))
|
||||
: {
|
||||
usage: commandConfig.usage,
|
||||
serviceDependencyMode: 'required',
|
||||
isExtension: true,
|
||||
sourcePlugin: loadedPlugin,
|
||||
isHidden: commandConfig.isHidden,
|
||||
noSupportNotice: commandConfig.noSupportNotice,
|
||||
options: {},
|
||||
};
|
||||
if (commandConfig.lifecycleEvents) schema.lifecycleEvents = commandConfig.lifecycleEvents;
|
||||
if (commandConfig.options) {
|
||||
for (const [optionName, optionConfig] of Object.entries(commandConfig.options)) {
|
||||
if (!schema.options[optionName]) {
|
||||
schema.options[optionName] = optionConfig;
|
||||
if (!optionConfig.type) {
|
||||
if (!missingOptionTypes.has(loadedPlugin)) {
|
||||
missingOptionTypes.set(loadedPlugin, new Set());
|
||||
}
|
||||
missingOptionTypes.get(loadedPlugin).add(optionName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Put common options to end of index
|
||||
for (const optionName of Object.keys(commonOptions)) delete schema.options[optionName];
|
||||
Object.assign(schema.options, commonOptions);
|
||||
|
||||
commands.set(fullCommandName, schema);
|
||||
}
|
||||
resolveCommands(loadedPlugin, commandConfig, `${fullCommandName} `);
|
||||
}
|
||||
};
|
||||
|
||||
for (const loadedPlugin of loadedPlugins) resolveCommands(loadedPlugin, loadedPlugin);
|
||||
|
||||
if (missingOptionTypes.size) {
|
||||
logDeprecation(
|
||||
'CLI_OPTIONS_SCHEMA_V3',
|
||||
'CLI options definitions were upgraded with "type" property (which could be one of "string", "boolean", "multiple"). ' +
|
||||
'Below listed plugins do not predefine type for introduced options:\n' +
|
||||
` - ${Array.from(
|
||||
missingOptionTypes,
|
||||
([plugin, optionNames]) =>
|
||||
`${plugin.constructor.name} for "${Array.from(optionNames).join('", "')}"`
|
||||
).join('\n - ')}\n` +
|
||||
'Please report this issue in plugin issue tracker.\n' +
|
||||
'Starting with next major release, this will be communicated with a thrown error.',
|
||||
{ serviceConfig: configuration }
|
||||
);
|
||||
}
|
||||
|
||||
return commands;
|
||||
};
|
||||
@ -1,4 +1,7 @@
|
||||
// CLI params parser, to be used before we have deducted what commands and options are supported in given context
|
||||
/**
|
||||
* CLI params parser, to be used before we have deducted
|
||||
* what commands and options are supported in given context.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
@ -8,6 +11,15 @@ const parseArgs = require('./parse-args');
|
||||
|
||||
const isParamName = RegExp.prototype.test.bind(require('./param-reg-exp'));
|
||||
|
||||
/**
|
||||
* This JavaScript function takes a schema of command
|
||||
* line options and organizes them into categories
|
||||
* (boolean, string, multiple, alias) based on their
|
||||
* types and shortcuts, returning an object with these
|
||||
* categorized options.
|
||||
* @param {*} commandOptionsSchema
|
||||
* @returns
|
||||
*/
|
||||
const resolveArgsSchema = (commandOptionsSchema) => {
|
||||
const options = { boolean: new Set(), string: new Set(), alias: new Map(), multiple: new Set() };
|
||||
for (const [name, optionSchema] of Object.entries(commandOptionsSchema)) {
|
||||
@ -28,6 +40,13 @@ const resolveArgsSchema = (commandOptionsSchema) => {
|
||||
return options;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses command line arguments, identifies the command and
|
||||
* its options, and returns an object containing these
|
||||
* details. It also determines if the command is a
|
||||
* container command or if a help request has been made,
|
||||
* and sets corresponding flags in the returned object.
|
||||
*/
|
||||
module.exports = memoizee((commandsSchema = require('./commands-schema')) => {
|
||||
commandsSchema = ensureMap(commandsSchema);
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
471
lib/configuration/resolve-variables.js
Normal file
471
lib/configuration/resolve-variables.js
Normal file
@ -0,0 +1,471 @@
|
||||
'use strict';
|
||||
|
||||
// Import required modules
|
||||
const path = require('path');
|
||||
const _ = require('lodash');
|
||||
const ServerlessError = require('../serverless-error');
|
||||
const resolveVariables = require('./variables/resolve');
|
||||
const resolveVariablesMeta = require('./variables/resolve-meta');
|
||||
const isPropertyResolved = require('./variables/is-property-resolved');
|
||||
const eventuallyReportVariableResolutionErrors = require('./variables/eventually-report-resolution-errors');
|
||||
const filterSupportedOptions = require('../cli/filter-supported-options');
|
||||
const humanizePropertyPathKeys = require('./variables/humanize-property-path-keys');
|
||||
const serverlessVariablesSourceEnv = require('./variables/sources/env');
|
||||
const serverlessVariablesSourceFile = require('./variables/sources/file');
|
||||
const serverlessVariablesSourceOpt = require('./variables/sources/opt');
|
||||
const serverlessVariablesSourceSelf = require('./variables/sources/self');
|
||||
const serverlessVariablesSourceStrToBool = require('./variables/sources/str-to-bool');
|
||||
const serverlessVariablesSourceSls = require('./variables/sources/sls');
|
||||
const serverlessVariablesSourceParams = require('./variables/sources/param');
|
||||
const serverlessVariablesSourceAwsCf = require('./variables/sources/aws-cf');
|
||||
const serverlessVariablesSourceAwsS3 = require('./variables/sources/aws-s3');
|
||||
const serverlessVariablesSourceAwsSsm = require('./variables/sources/aws-ssm');
|
||||
const serverlessVariablesSourceAws = require('./variables/sources/aws');
|
||||
const resolveExternalPluginSources = require('./variables/sources/resolve-external-plugin-sources');
|
||||
|
||||
/**
|
||||
* Resolve Serverless Variables: Phase One
|
||||
*
|
||||
* Resolve Serverless Variables "provider.stage" and "useDotenv"
|
||||
* These need to be resolved before any other variables, since a lot of
|
||||
* logic is dependent on them.
|
||||
*/
|
||||
const resolveServerlessVariablesPhaseOne = async ({
|
||||
service,
|
||||
servicePath,
|
||||
options,
|
||||
}) => {
|
||||
|
||||
const sourcesToResolveFrom = [
|
||||
'file',
|
||||
'self',
|
||||
'strToBool',
|
||||
'opt',
|
||||
'env',
|
||||
];
|
||||
const propertyPathsToResolve = [
|
||||
'provider\0stage',
|
||||
'useDotenv'
|
||||
];
|
||||
|
||||
// Creates a Map for each variable found in the configuration.
|
||||
const variablesMeta = resolveVariablesMeta(service)
|
||||
|
||||
// Configure resolver
|
||||
const resolverConfiguration = {
|
||||
serviceDir: servicePath,
|
||||
configuration: service,
|
||||
variablesMeta,
|
||||
sources: {
|
||||
file: serverlessVariablesSourceFile,
|
||||
self: serverlessVariablesSourceSelf,
|
||||
strToBool: serverlessVariablesSourceStrToBool,
|
||||
opt: serverlessVariablesSourceOpt,
|
||||
env: serverlessVariablesSourceEnv,
|
||||
},
|
||||
options,
|
||||
fulfilledSources: new Set(sourcesToResolveFrom),
|
||||
propertyPathsToResolve: new Set(propertyPathsToResolve),
|
||||
variableSourcesInConfig: new Set(),
|
||||
};
|
||||
|
||||
await resolveVariables(resolverConfiguration);
|
||||
|
||||
const resolvedService = resolverConfiguration.configuration;
|
||||
|
||||
// Throw any resolution errors
|
||||
const resolutionErrors = new Set(
|
||||
Array.from(variablesMeta.values(), ({ error }) => error).filter(Boolean)
|
||||
);
|
||||
if (resolutionErrors.size) {
|
||||
throw new ServerlessError(
|
||||
`Cannot resolve ${path.basename(
|
||||
servicePath
|
||||
)}: Variables resolution errored with:${Array.from(
|
||||
resolutionErrors,
|
||||
(error) => `\n - ${error.message}`
|
||||
)}`,
|
||||
'VARIABLES_RESOLUTION_ERROR'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Massage useDotenv to be a boolean
|
||||
*/
|
||||
if (resolvedService.useDotenv === 'true') { resolvedService.useDotenv = true; }
|
||||
if (resolvedService.useDotenv === 'false') { resolvedService.useDotenv = false; }
|
||||
|
||||
/**
|
||||
* If "provider.stage" or "useDotenv" variables were not resolved and
|
||||
* include "${env:", report them and explain what might have happened.
|
||||
*/
|
||||
if (!isPropertyResolved(variablesMeta, 'provider\0stage') &&
|
||||
resolvedService.provider.stage.includes('${env:')) {
|
||||
throw new ServerlessError(
|
||||
'Could not resolve "provider.stage" variable. Environment variable is missing. Please note that if the environment variable is specified in a .env file, it is not loaded until after the "provider.stage" variable is resolved. The reason is .env file loading looks for .env.${stage} as well as .env but when "provider.stage" is not resolved, the stage is unknown.',
|
||||
'VARIABLES_RESOLUTION_ERROR'
|
||||
);
|
||||
}
|
||||
if (!isPropertyResolved(variablesMeta, 'useDotenv') &&
|
||||
resolvedService.useDotenv.includes('${env:')) {
|
||||
throw new ServerlessError(
|
||||
'Could not resolve "useDotenv" variable. Environment variable is missing. Please note that if the environment variable is specified in a .env file, it is not loaded until after the "useDotenv" variable is resolved. The reason is .env file loading looks for .env.${stage} as well as .env but when "useDotenv" is not resolved, the stage is unknown.',
|
||||
'VARIABLES_RESOLUTION_ERROR'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Throw error if key properteis are not resolved with
|
||||
* helpful comments on the rationale.
|
||||
*/
|
||||
if (!isPropertyResolved(variablesMeta, 'provider\0stage')) {
|
||||
throw new ServerlessError(
|
||||
`Cannot resolve "provider.stage" variable. Please note, only Variable sources ${sourcesToResolveFrom.join(', ')} are supported for this property. AWS sources (e.g. SSM) or Serverless Framework Dashboard sources (e.g. params) are not supported for this property.`,
|
||||
'VARIABLES_RESOLUTION_ERROR'
|
||||
);
|
||||
}
|
||||
if (!isPropertyResolved(variablesMeta, 'useDotenv')) {
|
||||
throw new ServerlessError(
|
||||
`Cannot resolve "useDotenv" variable. Please note, only Variable sources ${sourcesToResolveFrom.join(', ')} are supported for this property. AWS sources (e.g. SSM) or Serverless Framework Dashboard sources (e.g. params) are not supported for this property.`,
|
||||
'VARIABLES_RESOLUTION_ERROR'
|
||||
);
|
||||
}
|
||||
|
||||
return resolvedService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve Serverless Variables: Phase Two
|
||||
*
|
||||
* Resolve everything, but still using limited sources.
|
||||
* We need to get "params" from the Platform for Dashboard
|
||||
* users, but need to ensure that some properties are resolved
|
||||
* (e.g. "org", "app", "service", "region", etc.)
|
||||
*/
|
||||
const resolveServerlessVariablesPhaseTwo = async ({
|
||||
service,
|
||||
servicePath,
|
||||
options,
|
||||
}) => {
|
||||
|
||||
const sourcesToResolveFrom = [
|
||||
'file',
|
||||
'self',
|
||||
'strToBool',
|
||||
'opt',
|
||||
'env',
|
||||
];
|
||||
|
||||
// Creates a Map for each variable found in the configuration.
|
||||
const variablesMeta = resolveVariablesMeta(service)
|
||||
|
||||
// Configure resolver
|
||||
const resolverConfiguration = {
|
||||
serviceDir: servicePath,
|
||||
configuration: service,
|
||||
variablesMeta,
|
||||
sources: {
|
||||
file: serverlessVariablesSourceFile,
|
||||
self: serverlessVariablesSourceSelf,
|
||||
strToBool: serverlessVariablesSourceStrToBool,
|
||||
opt: serverlessVariablesSourceOpt,
|
||||
env: serverlessVariablesSourceEnv,
|
||||
},
|
||||
options,
|
||||
fulfilledSources: new Set(sourcesToResolveFrom),
|
||||
variableSourcesInConfig: new Set(),
|
||||
// propertyPathsToResolve: new Set([]), // Omit to resolve everything
|
||||
};
|
||||
|
||||
await resolveVariables(resolverConfiguration);
|
||||
|
||||
const resolvedService = resolverConfiguration.configuration;
|
||||
|
||||
// Throw any resolution errors
|
||||
const resolutionErrors = new Set(
|
||||
Array.from(variablesMeta.values(), ({ error }) => error).filter(Boolean)
|
||||
);
|
||||
if (resolutionErrors.size) {
|
||||
throw new ServerlessError(
|
||||
`Cannot resolve ${path.basename(
|
||||
servicePath
|
||||
)}: Variables resolution errored with:${Array.from(
|
||||
resolutionErrors,
|
||||
(error) => `\n - ${error.message}`
|
||||
)}`,
|
||||
'VARIABLES_RESOLUTION_ERROR'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Throw errors for common mistakes to help users.
|
||||
*/
|
||||
const errorHandler = (property) => {
|
||||
throw new ServerlessError(
|
||||
`Cannot resolve "${property}" variable. Please note, only Variable sources ${sourcesToResolveFrom.join(', ')} are supported for this property. AWS sources (e.g. SSM) or Serverless Framework Dashboard sources (e.g. params) are not supported for this property.`,
|
||||
'VARIABLES_RESOLUTION_ERROR'
|
||||
);
|
||||
}
|
||||
if (!isPropertyResolved(variablesMeta, 'package\0path')) { return errorHandler('package\0path'); }
|
||||
if (!isPropertyResolved(variablesMeta, 'frameworkVersion')) { return errorHandler('frameworkVersion'); }
|
||||
if (!isPropertyResolved(variablesMeta, 'org')) { return errorHandler('org'); }
|
||||
if (!isPropertyResolved(variablesMeta, 'app')) { return errorHandler('app'); }
|
||||
if (!isPropertyResolved(variablesMeta, 'service')) { return errorHandler('service'); }
|
||||
if (!isPropertyResolved(variablesMeta, 'provider\0region')) { return errorHandler('provider\0region'); }
|
||||
if (!isPropertyResolved(variablesMeta, 'dashboard')) { return errorHandler('dashboard'); }
|
||||
|
||||
return resolvedService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve Serverless Variables: Phase Three
|
||||
*
|
||||
* Resolve everything except for Plugin sources
|
||||
*/
|
||||
const resolveServerlessVariablesPhaseThree = async ({
|
||||
service,
|
||||
servicePath,
|
||||
options,
|
||||
serverlessFrameworkInstance,
|
||||
serviceInstanceParamsFromPlatform,
|
||||
serviceOutputReferencesFromPlatform = {},
|
||||
}) => {
|
||||
|
||||
let sourcesToResolveFrom = [
|
||||
'file',
|
||||
'self',
|
||||
'strToBool',
|
||||
'opt',
|
||||
'env',
|
||||
'sls',
|
||||
'param',
|
||||
];
|
||||
|
||||
// Creates a Map for each variable found in the configuration.
|
||||
const variablesMeta = resolveVariablesMeta(service)
|
||||
|
||||
/**
|
||||
* First resolve, without AWS sources
|
||||
*/
|
||||
let resolverConfiguration = {
|
||||
serviceDir: servicePath,
|
||||
configuration: service,
|
||||
variablesMeta,
|
||||
sources: {
|
||||
file: serverlessVariablesSourceFile,
|
||||
self: serverlessVariablesSourceSelf,
|
||||
strToBool: serverlessVariablesSourceStrToBool,
|
||||
opt: serverlessVariablesSourceOpt,
|
||||
env: serverlessVariablesSourceEnv,
|
||||
sls: serverlessVariablesSourceSls(serverlessFrameworkInstance),
|
||||
param: serverlessVariablesSourceParams({
|
||||
service,
|
||||
serviceInstanceParamsFromPlatform
|
||||
}),
|
||||
},
|
||||
options,
|
||||
fulfilledSources: new Set(sourcesToResolveFrom),
|
||||
variableSourcesInConfig: new Set(),
|
||||
// propertyPathsToResolve: new Set([]), // Omit to resolve everything
|
||||
};
|
||||
await resolveVariables(resolverConfiguration);
|
||||
let resolvedService = resolverConfiguration.configuration;
|
||||
|
||||
// Throw any resolution errors
|
||||
let resolutionErrors = new Set(
|
||||
Array.from(variablesMeta.values(), ({ error }) => error).filter(Boolean)
|
||||
);
|
||||
if (resolutionErrors.size) {
|
||||
throw new ServerlessError(
|
||||
`Cannot resolve ${path.basename(
|
||||
servicePath
|
||||
)}: Variables resolution errored with:${Array.from(
|
||||
resolutionErrors,
|
||||
(error) => `\n - ${error.message}`
|
||||
)}`,
|
||||
'VARIABLES_RESOLUTION_ERROR'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Second resolve, with AWS sources
|
||||
*/
|
||||
sourcesToResolveFrom = [
|
||||
'file',
|
||||
'self',
|
||||
'strToBool',
|
||||
'opt',
|
||||
'env',
|
||||
'sls',
|
||||
'param',
|
||||
'cf',
|
||||
's3',
|
||||
'ssm',
|
||||
'aws',
|
||||
];
|
||||
|
||||
resolverConfiguration = {
|
||||
serviceDir: servicePath,
|
||||
configuration: service,
|
||||
variablesMeta,
|
||||
sources: {
|
||||
file: serverlessVariablesSourceFile,
|
||||
self: serverlessVariablesSourceSelf,
|
||||
strToBool: serverlessVariablesSourceStrToBool,
|
||||
opt: serverlessVariablesSourceOpt,
|
||||
env: serverlessVariablesSourceEnv,
|
||||
sls: serverlessVariablesSourceSls(serverlessFrameworkInstance),
|
||||
param: serverlessVariablesSourceParams({
|
||||
service,
|
||||
serviceInstanceParamsFromPlatform
|
||||
}),
|
||||
cf: serverlessVariablesSourceAwsCf(serverlessFrameworkInstance),
|
||||
s3: serverlessVariablesSourceAwsS3(serverlessFrameworkInstance),
|
||||
ssm: serverlessVariablesSourceAwsSsm(serverlessFrameworkInstance),
|
||||
aws: serverlessVariablesSourceAws(serverlessFrameworkInstance),
|
||||
},
|
||||
options,
|
||||
fulfilledSources: new Set(sourcesToResolveFrom),
|
||||
variableSourcesInConfig: new Set(),
|
||||
// propertyPathsToResolve: new Set([]), // Omit to resolve everything
|
||||
};
|
||||
await resolveVariables(resolverConfiguration);
|
||||
resolvedService = resolverConfiguration.configuration;
|
||||
|
||||
// Throw any resolution errors
|
||||
resolutionErrors = new Set(
|
||||
Array.from(variablesMeta.values(), ({ error }) => error).filter(Boolean)
|
||||
);
|
||||
if (resolutionErrors.size) {
|
||||
throw new ServerlessError(
|
||||
`Cannot resolve ${path.basename(
|
||||
servicePath
|
||||
)}: Variables resolution errored with:${Array.from(
|
||||
resolutionErrors,
|
||||
(error) => `\n - ${error.message}`
|
||||
)}`,
|
||||
'VARIABLES_RESOLUTION_ERROR'
|
||||
);
|
||||
}
|
||||
|
||||
return resolvedService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve Serverless Variables: Phase Four
|
||||
*
|
||||
* Plugins have been loaded at this point, so we can resolve
|
||||
* any variables from sources they might have added.
|
||||
*/
|
||||
const resolveServerlessVariablesPhaseFour = async ({
|
||||
service,
|
||||
servicePath,
|
||||
options,
|
||||
serverlessFrameworkInstance,
|
||||
serviceInstanceParamsFromPlatform,
|
||||
serviceOutputReferencesFromPlatform = {},
|
||||
}) => {
|
||||
|
||||
// Creates a Map for each variable found in the configuration.
|
||||
const variablesMeta = resolveVariablesMeta(service)
|
||||
|
||||
// Create a full list of sources to resolve from
|
||||
const sourcesToResolveFrom = [
|
||||
'file',
|
||||
'self',
|
||||
'strToBool',
|
||||
'opt',
|
||||
'env',
|
||||
'sls',
|
||||
'param',
|
||||
'cf',
|
||||
's3',
|
||||
'ssm',
|
||||
'aws',
|
||||
];
|
||||
|
||||
const resolverConfiguration = {
|
||||
serviceDir: servicePath,
|
||||
configuration: service,
|
||||
variablesMeta,
|
||||
sources: {
|
||||
file: serverlessVariablesSourceFile,
|
||||
self: serverlessVariablesSourceSelf,
|
||||
strToBool: serverlessVariablesSourceStrToBool,
|
||||
opt: serverlessVariablesSourceOpt,
|
||||
env: serverlessVariablesSourceEnv,
|
||||
sls: serverlessVariablesSourceSls(serverlessFrameworkInstance),
|
||||
param: serverlessVariablesSourceParams({
|
||||
service,
|
||||
serviceInstanceParamsFromPlatform
|
||||
}),
|
||||
cf: serverlessVariablesSourceAwsCf(serverlessFrameworkInstance),
|
||||
s3: serverlessVariablesSourceAwsS3(serverlessFrameworkInstance),
|
||||
ssm: serverlessVariablesSourceAwsSsm(serverlessFrameworkInstance),
|
||||
aws: serverlessVariablesSourceAws(serverlessFrameworkInstance),
|
||||
},
|
||||
options,
|
||||
fulfilledSources: new Set(sourcesToResolveFrom),
|
||||
variableSourcesInConfig: new Set(),
|
||||
// propertyPathsToResolve: new Set([]), // Omit to resolve everything
|
||||
};
|
||||
|
||||
resolveExternalPluginSources(
|
||||
service,
|
||||
servicePath,
|
||||
serverlessFrameworkInstance.pluginManager.externalPlugins
|
||||
);
|
||||
|
||||
await resolveVariables(resolverConfiguration);
|
||||
const resolvedService = resolverConfiguration.configuration;
|
||||
|
||||
// Throw any resolution errors
|
||||
const resolutionErrors = new Set(
|
||||
Array.from(variablesMeta.values(), ({ error }) => error).filter(Boolean)
|
||||
);
|
||||
if (resolutionErrors.size) {
|
||||
throw new ServerlessError(
|
||||
`Cannot resolve ${path.basename(
|
||||
servicePath
|
||||
)}: Variables resolution errored with:${Array.from(
|
||||
resolutionErrors,
|
||||
(error) => `\n - ${error.message}`
|
||||
)}`,
|
||||
'VARIABLES_RESOLUTION_ERROR'
|
||||
);
|
||||
}
|
||||
|
||||
// Return if no remaining variables exist
|
||||
if (!variablesMeta.size) { return resolvedService; }
|
||||
|
||||
/**
|
||||
* Report unrecognized Serverless Variable Sources found
|
||||
* within the Service configuration, which naturally, are
|
||||
* still unresolved.
|
||||
*/
|
||||
const unresolvedSources = require('./variables/resolve-unresolved-source-types')(variablesMeta)
|
||||
const recognizedSourceNames = new Set(Object.keys(resolverConfiguration.sources))
|
||||
const unrecognizedSourceNames = Array.from(unresolvedSources.keys()).filter(
|
||||
(sourceName) => !recognizedSourceNames.has(sourceName)
|
||||
)
|
||||
|
||||
if (unrecognizedSourceNames.includes('output')) {
|
||||
throw new ServerlessError(
|
||||
'"Cannot resolve configuration: ' +
|
||||
'"output" variable can only be used in ' +
|
||||
'services deployed with Serverless Dashboard (with "org" setting configured)',
|
||||
'DASHBOARD_VARIABLE_SOURCES_MISUSE'
|
||||
)
|
||||
}
|
||||
throw new ServerlessError(
|
||||
`Unrecognized configuration variable sources: "${unrecognizedSourceNames.join('", "')}"`,
|
||||
'UNRECOGNIZED_VARIABLE_SOURCES'
|
||||
)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
resolveServerlessVariablesPhaseOne,
|
||||
resolveServerlessVariablesPhaseTwo,
|
||||
resolveServerlessVariablesPhaseThree,
|
||||
resolveServerlessVariablesPhaseFour,
|
||||
};
|
||||
@ -1,4 +1,7 @@
|
||||
// Stringify property keys array for user facing message
|
||||
/**
|
||||
* Stringify property keys array for user facing message
|
||||
* For example, if the input is ['a', 'b', 'c'], the output will be 'a.b.c'
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
|
||||
@ -1,5 +1,21 @@
|
||||
// Resolves all non instance dependent variables in a provided configuration
|
||||
// This util is not used in Serverless process flow, but is handy for side resolution of variables
|
||||
/*
|
||||
* NOTE: NOT used in the main Serverless process flow.
|
||||
*
|
||||
* This utility module resolves all non-instance dependent variables in a
|
||||
* provided configuration. Although it's not used in the main Serverless
|
||||
* process flow, it's useful for side resolution of variables.
|
||||
*
|
||||
* The module imports necessary dependencies and defines default sources for
|
||||
* variable resolution, including environment variables, file sources, options,
|
||||
* self references, string-to-boolean conversions, and Serverless instance data.
|
||||
*
|
||||
* It also includes a function to report any errors that occur during variable
|
||||
* resolution, throwing a ServerlessError with a detailed message if any errors
|
||||
* are found.
|
||||
*
|
||||
* The module exports an asynchronous function that takes a configuration object
|
||||
* as input, which includes the service directory, service path, and configuration.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
@ -34,7 +50,13 @@ const reportEventualErrors = (variablesMeta) => {
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = async ({ serviceDir, servicePath, configuration, options, sources = null }) => {
|
||||
module.exports = async ({
|
||||
serviceDir,
|
||||
servicePath,
|
||||
configuration,
|
||||
options,
|
||||
sources = null
|
||||
}) => {
|
||||
serviceDir = ensureString(serviceDir || servicePath);
|
||||
ensurePlainObject(configuration);
|
||||
options = ensurePlainObject(options, { default: {} });
|
||||
|
||||
@ -1,4 +1,15 @@
|
||||
// Variables syntax parser, for a string input returns AST-like meta object
|
||||
/*
|
||||
* This module, parse.js, is a variables syntax parser that returns an AST-like
|
||||
* meta object for a given string input.
|
||||
*
|
||||
* The parser uses "states" to keep track of where it is in the string. Each state
|
||||
* has a function that handles what to do when the parser is in that state.
|
||||
*
|
||||
* There are also functions to finish parsing a source or a variable. These are
|
||||
* called when the parser reaches the end of a source or variable in the string.
|
||||
* They add the parsed source or variable to a list and update the state for the
|
||||
* next source or variable.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
|
||||
@ -1,4 +1,19 @@
|
||||
// Parse out variables meta for each configuration property
|
||||
/*
|
||||
* This module, resolve-meta.js, is responsible for parsing out variables meta
|
||||
* for each configuration property in a Serverless service.
|
||||
*
|
||||
* It imports necessary dependencies and defines a function, parseEntries, which
|
||||
* recursively traverses the configuration object. For each property, it checks
|
||||
* the type of the value. If the value is an object or an array, it recursively
|
||||
* calls parseEntries on the value. If the value is a string, it attempts to parse
|
||||
* any variables in the string. If an error occurs during parsing, it adds an
|
||||
* error message to the map for that property.
|
||||
*
|
||||
* The module exports a function that takes a configuration object as input. It
|
||||
* ensures that the configuration is a plain object, and then calls parseEntries
|
||||
* on the entries of the configuration object, initializing the map that will
|
||||
* hold the parsed variables meta.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
|
||||
@ -1,4 +1,21 @@
|
||||
// Having variables meta, configuration, and sources setup, attempt to resolve all variables
|
||||
/*
|
||||
* This module, resolve.js, is responsible for resolving all variables in a given
|
||||
* configuration. It's not used in the main Serverless process flow, but is
|
||||
* useful for side resolution of variables.
|
||||
*
|
||||
* The module imports necessary dependencies and defines default sources for
|
||||
* variable resolution. It also includes a function to report any errors that
|
||||
* occur during variable resolution, throwing a ServerlessError with a detailed
|
||||
* message if any errors are found.
|
||||
*
|
||||
* The resolveSourceValuesVariables function is a key part of this module. It
|
||||
* handles the resolution of variables that come from multiple sources. It parses
|
||||
* variables in each string part individually to avoid accidental resolution of
|
||||
* variable-like notation which may surface after joining two values.
|
||||
*
|
||||
* The module exports an asynchronous function that takes a configuration object
|
||||
* as input, which includes the service directory, service path, and configuration.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
@ -98,15 +115,16 @@ class VariablesResolver {
|
||||
})
|
||||
)
|
||||
)
|
||||
).then(() => {});
|
||||
).then(() => { });
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
Array.from(variablesMeta.keys(), (propertyPath) =>
|
||||
this.resolveProperty(resolutionBatchId, propertyPath)
|
||||
)
|
||||
).then(() => {});
|
||||
Array.from(variablesMeta.keys(), (propertyPath) => {
|
||||
return this.resolveProperty(resolutionBatchId, propertyPath)
|
||||
})
|
||||
).then(() => { });
|
||||
}
|
||||
|
||||
async resolveVariables(resolutionBatchId, propertyPath, valueMeta) {
|
||||
// Resolve all variables configured in given string value.
|
||||
await Promise.all(
|
||||
@ -218,6 +236,7 @@ class VariablesResolver {
|
||||
variableMeta.error = error;
|
||||
return;
|
||||
}
|
||||
|
||||
if (resolvedData.value != null) {
|
||||
// Source successfully resolved. Accept as final value
|
||||
delete variableMeta.sources;
|
||||
@ -332,9 +351,8 @@ class VariablesResolver {
|
||||
} catch (error) {
|
||||
error.message = `Cannot resolve variable at "${humanizePropertyPath(
|
||||
propertyPath.split('\0')
|
||||
)}": Approached variable syntax error in resolved value "${valueMeta.value}": ${
|
||||
error.message
|
||||
}`;
|
||||
)}": Approached variable syntax error in resolved value "${valueMeta.value}": ${error.message
|
||||
}`;
|
||||
delete valueMeta.value;
|
||||
valueMeta.error = error;
|
||||
throw error;
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
const ensureString = require('type/string/ensure');
|
||||
const _ = require('lodash');
|
||||
const ServerlessError = require('../../../../serverless-error');
|
||||
const ServerlessError = require('../../../serverless-error');
|
||||
|
||||
module.exports = (serverlessInstance) => {
|
||||
return {
|
||||
@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const ensureString = require('type/string/ensure');
|
||||
const ServerlessError = require('../../../../serverless-error');
|
||||
const ServerlessError = require('../../../serverless-error');
|
||||
|
||||
module.exports = (serverlessInstance) => {
|
||||
return {
|
||||
@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const ensureString = require('type/string/ensure');
|
||||
const ServerlessError = require('../../../../serverless-error');
|
||||
const ServerlessError = require('../../../serverless-error');
|
||||
const _ = require('lodash');
|
||||
|
||||
module.exports = (serverlessInstance) => {
|
||||
@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const ensureString = require('type/string/ensure');
|
||||
const ServerlessError = require('../../../../serverless-error');
|
||||
const ServerlessError = require('../../../serverless-error');
|
||||
|
||||
module.exports = (serverlessInstance) => {
|
||||
return {
|
||||
@ -21,10 +21,10 @@ module.exports = (serverlessInstance) => {
|
||||
|
||||
switch (address) {
|
||||
case 'accountId': {
|
||||
const { Account } = await serverlessInstance
|
||||
const { accountId } = await serverlessInstance
|
||||
.getProvider('aws')
|
||||
.request('STS', 'getCallerIdentity', {}, { useCache: true });
|
||||
return { value: Account };
|
||||
.getCredentials();
|
||||
return { value: accountId };
|
||||
}
|
||||
case 'region': {
|
||||
let region = options.region;
|
||||
@ -36,6 +36,7 @@ module.exports = {
|
||||
'MISSING_FILE_SOURCE_PATH'
|
||||
);
|
||||
}
|
||||
|
||||
const filePath = path.resolve(
|
||||
serviceDir,
|
||||
ensureString(params[0], {
|
||||
|
||||
@ -11,6 +11,7 @@ module.exports = {
|
||||
errorMessage: 'Non-string address argument in variable "opt" source: %v',
|
||||
errorCode: 'INVALID_OPT_SOURCE_ADDRESS',
|
||||
});
|
||||
|
||||
if (!isSourceFulfilled) {
|
||||
if (address == null) return { value: null, isPending: true };
|
||||
if (options[address] !== undefined) return { value: options[address] };
|
||||
|
||||
92
lib/configuration/variables/sources/param.js
Normal file
92
lib/configuration/variables/sources/param.js
Normal file
@ -0,0 +1,92 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const ensureString = require('type/string/ensure');
|
||||
const ServerlessError = require('../../../serverless-error');
|
||||
|
||||
module.exports = ({
|
||||
service = {},
|
||||
serviceInstanceParamsFromPlatform = {}
|
||||
}) => {
|
||||
return {
|
||||
resolve: async ({
|
||||
address,
|
||||
resolveConfigurationProperty,
|
||||
options
|
||||
}) => {
|
||||
|
||||
if (!address) {
|
||||
throw new ServerlessError(
|
||||
'Missing address argument in variable "param" source',
|
||||
'MISSING_PARAM_SOURCE_ADDRESS'
|
||||
);
|
||||
}
|
||||
address = ensureString(address, {
|
||||
Error: ServerlessError,
|
||||
errorMessage: 'Non-string address argument in variable "param" source: %v',
|
||||
errorCode: 'INVALID_PARAM_SOURCE_ADDRESS',
|
||||
});
|
||||
|
||||
const allParams = {};
|
||||
const stage = service.provider.stage;
|
||||
|
||||
// Collect all Params from CLI options, stage params, and Serverless Platform
|
||||
// If options.param is not an array, make it an array
|
||||
if (options.param && !Array.isArray(options.param)) {
|
||||
options.param = [options.param];
|
||||
}
|
||||
if (options.param) {
|
||||
const regex = /(?<key>[^=]+)=(?<value>.+)/;
|
||||
for (const item of options.param) {
|
||||
const res = item.match(regex);
|
||||
if (!res) {
|
||||
throw new ServerlessError(
|
||||
`Encountered invalid "--param" CLI option value: "${item}". Supported format: "--param='<key>=<val>'"`,
|
||||
'INVALID_CLI_PARAM_FORMAT'
|
||||
);
|
||||
}
|
||||
allParams[res.groups.key] = { value: res.groups.value.trimEnd(), type: 'cli' };
|
||||
}
|
||||
}
|
||||
const configParams = new Map(
|
||||
Object.entries(_.get(service, 'params') || {})
|
||||
);
|
||||
// Collect all params with "default"
|
||||
for (const [name, value] of new Map(Object.entries(configParams.get('default') || {}))) {
|
||||
if (value == null) continue;
|
||||
if (allParams[name] != null) continue;
|
||||
allParams[name] = { value, type: 'configService' };
|
||||
}
|
||||
// Collect all params from "stage"
|
||||
for (const [name, value] of Object.entries(configParams.get(stage) || {})) {
|
||||
if (value == null) continue;
|
||||
// Overwrite default params
|
||||
allParams[name] = { value, type: 'configServiceStage' };
|
||||
}
|
||||
// Collect all params from serviceInstanceParamsFromPlatform
|
||||
for (const [name, value] of Object.entries(serviceInstanceParamsFromPlatform)) {
|
||||
if (value == null) continue;
|
||||
if (allParams[name] != null) {
|
||||
throw new ServerlessError(
|
||||
`You have defined this parameter "${name}" in the Serverless Framework Dashboard as well as in your Service or CLI options. Please remove one of the definitions.`,
|
||||
'DUPLICATE_PARAM_FORMAT'
|
||||
);
|
||||
}
|
||||
allParams[name] = { value, type: 'dashboard' };
|
||||
}
|
||||
|
||||
const result = {
|
||||
value: allParams[address] ?
|
||||
allParams[address].value :
|
||||
null
|
||||
};
|
||||
|
||||
if (allParams[address] == null) {
|
||||
result.eventualErrorMessage = `The param "${address}" cannot be resolved from CLI options or stage params${service.org && service.app ? ' or Serverless Dashboard' : ''
|
||||
}. If you are using Serverless Framework Compose, make sure to run commands via Compose so that all parameters can be resolved`;
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,28 @@
|
||||
'use strict';
|
||||
|
||||
const processVariables = (propertyPath, variablesMeta, resultMap) => {
|
||||
if (!variablesMeta.variables) return;
|
||||
for (const variableMeta of variablesMeta.variables) {
|
||||
if (!variableMeta.sources) continue;
|
||||
const sourceData = variableMeta.sources[0];
|
||||
if (!sourceData.type) continue;
|
||||
if (sourceData.params) {
|
||||
for (const paramData of sourceData.params) {
|
||||
processVariables(propertyPath, paramData, resultMap);
|
||||
}
|
||||
}
|
||||
if (sourceData.address) {
|
||||
processVariables(propertyPath, sourceData.address, resultMap);
|
||||
}
|
||||
if (!resultMap.has(sourceData.type)) resultMap.set(sourceData.type, new Set());
|
||||
resultMap.get(sourceData.type).add(propertyPath);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = (propertiesVariablesMeta) => {
|
||||
const resultMap = new Map();
|
||||
for (const [propertyPath, propertyVariablesMeta] of propertiesVariablesMeta) {
|
||||
processVariables(propertyPath, propertyVariablesMeta, resultMap);
|
||||
}
|
||||
return resultMap;
|
||||
};
|
||||
@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const ensureString = require('type/string/ensure');
|
||||
const ServerlessError = require('../../../../serverless-error');
|
||||
const ServerlessError = require('../../../serverless-error');
|
||||
|
||||
module.exports = (serverlessInstance) => {
|
||||
return {
|
||||
@ -30,6 +30,26 @@ const isStackFromOldLocalFallback = RegExp.prototype.test.bind(
|
||||
);
|
||||
|
||||
class Serverless {
|
||||
|
||||
/**
|
||||
* Constructs a new Serverless instance. This constructor is responsible for
|
||||
* initializing various properties of the Serverless instance, including
|
||||
* providers, version, access key, credential providers, service directory,
|
||||
* commands, options, and various utility classes. It also handles validation
|
||||
* of service path and filename input, and throws errors for incompatible
|
||||
* configurations or outdated versions of the Framework.
|
||||
*
|
||||
* @param {Object} options - The options for the Serverless instance.
|
||||
* @param {string} options.accessKey - The access key for the Serverless instance.
|
||||
* @param {Array} options.commands - The commands for the Serverless instance.
|
||||
* @param {Object} options.options - The options for the Serverless instance.
|
||||
* @param {string} options.servicePath - The service path for the Serverless instance.
|
||||
* @param {string} options.serviceConfigFileName - The service config file name.
|
||||
* @param {Object} options.service - The service for the Serverless instance.
|
||||
* @param {Object} options.credentialProviders - The credential providers.
|
||||
* @param {Object} options.variablesMeta - The variables meta for the Serverless instance.
|
||||
* @throws {ServerlessError} If there's an error in validation or incompatible configurations.
|
||||
*/
|
||||
constructor({
|
||||
accessKey = null,
|
||||
commands,
|
||||
@ -39,6 +59,7 @@ class Serverless {
|
||||
service = {},
|
||||
credentialProviders = {},
|
||||
variablesMeta,
|
||||
isDashboardEnabledForService = false,
|
||||
} = {}) {
|
||||
|
||||
if (isStackFromOldLocalFallback(new Error().stack)) {
|
||||
@ -64,17 +85,22 @@ class Serverless {
|
||||
);
|
||||
}
|
||||
|
||||
// New root properties added in V.4
|
||||
this.providers = {};
|
||||
this.version = version;
|
||||
this.accessKey = accessKey || null;
|
||||
this.credentialProviders = credentialProviders;
|
||||
this.isDashboardEnabled = !!(service.org && service.app);
|
||||
this.isDashboardEnabledForService = isDashboardEnabledForService;
|
||||
|
||||
/**
|
||||
* Validate Service path and filename input
|
||||
*/
|
||||
this.serviceDir = ensureString(servicePath, {
|
||||
name: 'options.serviceDir',
|
||||
Error: ServerlessError,
|
||||
errorCode: 'INVALID_NON_STRING_SERVICE_DIR',
|
||||
isOptional: true,
|
||||
});
|
||||
|
||||
if (this.serviceDir != null) {
|
||||
this.serviceDir = path.resolve(this.serviceDir);
|
||||
this.configurationFilename = ensureString(serviceConfigFileName, {
|
||||
@ -96,8 +122,6 @@ class Serverless {
|
||||
});
|
||||
}
|
||||
|
||||
this.providers = {};
|
||||
this.version = version;
|
||||
commands = ensureArray(commands);
|
||||
// Ensure that original `options` are not mutated, can be removed after addressing:
|
||||
// https://github.com/serverless/serverless/issues/2582
|
||||
@ -130,11 +154,19 @@ class Serverless {
|
||||
this.isStandaloneExecutable = isStandaloneExecutable;
|
||||
this.triggeredDeprecations = logDeprecation.triggeredDeprecations;
|
||||
this.isConfigurationExtendable = true;
|
||||
|
||||
// TODO: Remove once "@serverless/dashboard-plugin" is integrated into this repository
|
||||
this._commandsSchema = commmandsSchema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the serverless instance. This method is responsible for creating
|
||||
* an instance ID, initializing a new CLI instance, setting CLI options and
|
||||
* commands, loading the service and all plugins, and setting the loaded plugins
|
||||
* and commands for the CLI. This is typically called at the start of a
|
||||
* serverless lifecycle.
|
||||
*
|
||||
* @async
|
||||
* @throws {Error} If there's an error in loading the service or plugins.
|
||||
*/
|
||||
async init() {
|
||||
// create an instanceId (can be e.g. used when a predictable random value is needed)
|
||||
this.instanceId = new Date().getTime().toString();
|
||||
@ -148,6 +180,7 @@ class Serverless {
|
||||
this.pluginManager.setCliCommands(this.processedInput.commands);
|
||||
|
||||
await this.service.load(this.processedInput.options);
|
||||
|
||||
// load all plugins
|
||||
await this.pluginManager.loadAllPlugins(this.service.plugins);
|
||||
this.isConfigurationExtendable = false;
|
||||
@ -157,6 +190,18 @@ class Serverless {
|
||||
this.cli.setLoadedCommands(this.pluginManager.getCommands());
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the Serverless instance. This method is responsible for reloading
|
||||
* service file parameters, validating commands, setting variables, merging
|
||||
* arrays, setting function names, validating the service configuration, and
|
||||
* initializing service outputs. It also triggers the plugin lifecycle for
|
||||
* processing commands. This method is typically called to run a Serverless
|
||||
* instance.
|
||||
*
|
||||
* @async
|
||||
* @throws {Error} If there's an error in command validation, service validation,
|
||||
* or during the plugin lifecycle.
|
||||
*/
|
||||
async run() {
|
||||
if (this.configurationInput) this.service.reloadServiceFileParam();
|
||||
|
||||
|
||||
@ -1,48 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const commonPath = require('path2/common');
|
||||
|
||||
const anonymizeStacktracePaths = (stackFrames) => {
|
||||
const stackFramesWithAbsolutePaths = stackFrames.filter((p) => path.isAbsolute(p));
|
||||
let commonPathPrefix = '';
|
||||
|
||||
if (stackFramesWithAbsolutePaths.length) {
|
||||
commonPathPrefix = commonPath(...stackFramesWithAbsolutePaths);
|
||||
|
||||
const lastServerlessPathIndex = commonPathPrefix.lastIndexOf(
|
||||
`${path.sep}serverless${path.sep}`
|
||||
);
|
||||
|
||||
if (lastServerlessPathIndex !== -1) {
|
||||
commonPathPrefix = commonPathPrefix.slice(0, lastServerlessPathIndex);
|
||||
} else {
|
||||
const nodeModulesPathPart = `${path.sep}node_modules${path.sep}`;
|
||||
const lastNodeModulesPathIndex = commonPathPrefix.lastIndexOf(nodeModulesPathPart);
|
||||
if (lastNodeModulesPathIndex !== -1) {
|
||||
commonPathPrefix = commonPathPrefix.slice(
|
||||
0,
|
||||
lastNodeModulesPathIndex + nodeModulesPathPart.length - 1
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let previousStackFramePath = null;
|
||||
return stackFrames.map((stackFrame) => {
|
||||
stackFrame = stackFrame.replace(commonPathPrefix, '');
|
||||
const locationIndex = stackFrame.search(/:\d+:/);
|
||||
if (locationIndex === -1) {
|
||||
previousStackFramePath = null;
|
||||
return stackFrame;
|
||||
}
|
||||
const currentStackFramePath = stackFrame.slice(0, locationIndex);
|
||||
if (currentStackFramePath === previousStackFramePath) {
|
||||
return `^${stackFrame.slice(currentStackFramePath.length)}`;
|
||||
}
|
||||
previousStackFramePath = currentStackFramePath;
|
||||
return stackFrame;
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = anonymizeStacktracePaths;
|
||||
@ -1,9 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const configUtils = require('@serverless/utils/config');
|
||||
|
||||
module.exports = Boolean(
|
||||
process.env.SLS_TELEMETRY_DISABLED ||
|
||||
process.env.SLS_TRACKING_DISABLED ||
|
||||
configUtils.get('trackingDisabled')
|
||||
);
|
||||
@ -1,10 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
module.exports = (() => {
|
||||
const resolvedHomeDir = os.homedir();
|
||||
if (!resolvedHomeDir) return null;
|
||||
return path.resolve(resolvedHomeDir, '.serverless', 'telemetry-cache');
|
||||
})();
|
||||
@ -1,289 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const _ = require('lodash');
|
||||
const isPlainObject = require('type/plain-object/is');
|
||||
const isObject = require('type/object/is');
|
||||
const userConfig = require('@serverless/utils/config');
|
||||
const getNotificationsMode = require('@serverless/utils/get-notifications-mode');
|
||||
const log = require('@serverless/utils/log').log.get('telemetry');
|
||||
const isStandalone = require('../is-standalone-executable');
|
||||
const { getConfigurationValidationResult } = require('../../classes/config-schema-handler');
|
||||
const { triggeredDeprecations } = require('../log-deprecation');
|
||||
const isNpmGlobal = require('../npm-package/is-global');
|
||||
const isLocallyInstalled = require('../../cli/is-locally-installed');
|
||||
const ci = require('ci-info');
|
||||
const AWS = require('../../aws/sdk-v2');
|
||||
|
||||
const configValidationModeValues = new Set(['off', 'warn', 'error']);
|
||||
const commandsReportingProjectId = new Set(['deploy', '', 'remove']);
|
||||
|
||||
const getServiceConfig = ({ configuration, options, variableSources }) => {
|
||||
const providerName = isObject(configuration.provider)
|
||||
? configuration.provider.name
|
||||
: configuration.provider;
|
||||
|
||||
const isAwsProvider = providerName === 'aws';
|
||||
|
||||
const defaultRuntime = isAwsProvider
|
||||
? configuration.provider.runtime || 'nodejs14.x'
|
||||
: _.get(configuration, 'provider.runtime');
|
||||
|
||||
const functions = (() => {
|
||||
if (isObject(configuration.functions)) return configuration.functions;
|
||||
if (!Array.isArray(configuration.functions)) return {};
|
||||
const result = {};
|
||||
for (const functionsBlock of configuration.functions) Object.assign(result, functionsBlock);
|
||||
return result;
|
||||
})();
|
||||
|
||||
const resolveResourceTypes = (resources) => {
|
||||
if (!isPlainObject(resources)) return [];
|
||||
return [
|
||||
...new Set(
|
||||
Object.values(resources)
|
||||
.map((resource) => {
|
||||
const type = _.get(resource, 'Type');
|
||||
if (typeof type !== 'string' || !type.includes('::')) return null;
|
||||
const domain = type.slice(0, type.indexOf(':'));
|
||||
return domain === 'AWS' ? type : domain;
|
||||
})
|
||||
.filter(Boolean)
|
||||
),
|
||||
];
|
||||
};
|
||||
|
||||
const resources = (() => {
|
||||
if (!isAwsProvider) return undefined;
|
||||
return {
|
||||
general: resolveResourceTypes(_.get(configuration.resources, 'Resources')),
|
||||
};
|
||||
})();
|
||||
|
||||
const plugins = configuration.plugins
|
||||
? configuration.plugins.modules || configuration.plugins
|
||||
: [];
|
||||
|
||||
const resolveUniqueParamsCount = (params) => {
|
||||
if (!isPlainObject(params)) return 0;
|
||||
return new Set(
|
||||
Object.values(params)
|
||||
.filter(isPlainObject)
|
||||
.map((stageParams) => Object.keys(stageParams))
|
||||
.reduce((acc, stageParamsKeys) => [...acc, ...stageParamsKeys], [])
|
||||
).size;
|
||||
};
|
||||
|
||||
const result = {
|
||||
// TODO: Update when upgrading the default for next major
|
||||
configValidationMode: configValidationModeValues.has(configuration.configValidationMode)
|
||||
? configuration.configValidationMode
|
||||
: 'warn',
|
||||
provider: {
|
||||
name: providerName,
|
||||
runtime: defaultRuntime,
|
||||
stage: options.stage || _.get(configuration, 'provider.stage') || 'dev',
|
||||
region: isAwsProvider
|
||||
? options.region || configuration.provider.region || 'us-east-1'
|
||||
: _.get(configuration, 'provider.region'),
|
||||
},
|
||||
variableSources: variableSources ? Array.from(variableSources) : [],
|
||||
plugins,
|
||||
functions: Object.values(functions)
|
||||
.map((functionConfig) => {
|
||||
if (!functionConfig) return null;
|
||||
const functionEvents = Array.isArray(functionConfig.events) ? functionConfig.events : [];
|
||||
const functionRuntime = (() => {
|
||||
if (functionConfig.image) return '$containerimage';
|
||||
return functionConfig.runtime || defaultRuntime;
|
||||
})();
|
||||
|
||||
return {
|
||||
url: Boolean(functionConfig.url),
|
||||
runtime: functionRuntime,
|
||||
events: functionEvents.map((eventConfig) => ({
|
||||
type: isObject(eventConfig) ? Object.keys(eventConfig)[0] || null : null,
|
||||
})),
|
||||
};
|
||||
})
|
||||
.filter(Boolean),
|
||||
resources,
|
||||
paramsCount: resolveUniqueParamsCount(configuration.params),
|
||||
};
|
||||
|
||||
// We want to recognize types of constructs from `serverless-lift` plugin if possible
|
||||
if (plugins.includes('serverless-lift') && _.isObject(configuration.constructs)) {
|
||||
result.constructs = Object.values(configuration.constructs)
|
||||
.map((construct) => {
|
||||
if (_.isObject(construct) && construct.type != null) {
|
||||
return { type: construct.type };
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// This method is explicitly kept as synchronous. The reason for it being the fact that it needs to
|
||||
// be executed in such manner due to its use in `process.on('SIGINT')` handler.
|
||||
module.exports = ({
|
||||
command,
|
||||
options,
|
||||
commandSchema,
|
||||
serviceDir,
|
||||
configuration,
|
||||
serverless,
|
||||
commandUsage,
|
||||
variableSources,
|
||||
dockerVersion,
|
||||
}) => {
|
||||
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;
|
||||
} catch {
|
||||
// Pass silently
|
||||
}
|
||||
|
||||
const ciName = (() => {
|
||||
if (process.env.SERVERLESS_CI_CD) {
|
||||
return 'Serverless CI/CD';
|
||||
}
|
||||
|
||||
if (process.env.SEED_APP_NAME) {
|
||||
return 'Seed';
|
||||
}
|
||||
|
||||
if (ci.isCI) {
|
||||
if (ci.name) {
|
||||
return ci.name;
|
||||
}
|
||||
return 'unknown';
|
||||
}
|
||||
return null;
|
||||
})();
|
||||
|
||||
const userId = (() => {
|
||||
// In this situation deployment relies on existence on company-wide access key
|
||||
// and `userId` from config does not matter
|
||||
if (process.env.SERVERLESS_ACCESS_KEY) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return userConfig.get('userId');
|
||||
})();
|
||||
|
||||
const usedVersions = (() => {
|
||||
return {
|
||||
'serverless': require('../../../package').version,
|
||||
};
|
||||
})();
|
||||
|
||||
// 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,
|
||||
isTtyTerminal: process.stdin.isTTY && process.stdout.isTTY,
|
||||
isDockerInstalled: dockerVersion === undefined ? undefined : Boolean(dockerVersion),
|
||||
dockerVersion: dockerVersion || undefined,
|
||||
cliName: 'serverless',
|
||||
command,
|
||||
commandOptionNames,
|
||||
dashboard: {
|
||||
userId,
|
||||
},
|
||||
firstLocalInstallationTimestamp: userConfig.get('meta.created_at'),
|
||||
frameworkLocalUserId: userConfig.get('frameworkId'),
|
||||
installationType: (() => {
|
||||
if (isStandalone) {
|
||||
if (process.platform === 'win32') return 'global:standalone:windows';
|
||||
return 'global:standalone:other';
|
||||
}
|
||||
if (!isLocallyInstalled) {
|
||||
return isNpmGlobal() ? 'global:npm' : 'global:other';
|
||||
}
|
||||
if (EvalError.$serverlessInitInstallationVersion) return 'local:fallback';
|
||||
return 'local:direct';
|
||||
})(),
|
||||
isAutoUpdateEnabled: Boolean(userConfig.get('autoUpdate.enabled')),
|
||||
isUsingCompose: Boolean(process.env.SLS_COMPOSE),
|
||||
notificationsMode: getNotificationsMode(),
|
||||
timestamp: Date.now(),
|
||||
timezone,
|
||||
triggeredDeprecations: Array.from(triggeredDeprecations),
|
||||
versions: usedVersions,
|
||||
};
|
||||
|
||||
if (commandDurationMs != null) {
|
||||
payload.commandDurationMs = commandDurationMs;
|
||||
}
|
||||
|
||||
if (configuration && commandSchema.serviceDependencyMode) {
|
||||
const npmDependencies = (() => {
|
||||
const pkgJson = (() => {
|
||||
try {
|
||||
return require(path.resolve(serviceDir, 'package.json'));
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
if (!pkgJson) return [];
|
||||
return Array.from(
|
||||
new Set([
|
||||
...Object.keys(pkgJson.dependencies || {}),
|
||||
...Object.keys(pkgJson.optionalDependencies || {}),
|
||||
...Object.keys(pkgJson.devDependencies || {}),
|
||||
])
|
||||
);
|
||||
})();
|
||||
|
||||
const providerName = isObject(configuration.provider)
|
||||
? configuration.provider.name
|
||||
: configuration.provider;
|
||||
const isAwsProvider = providerName === 'aws';
|
||||
|
||||
payload.hasLocalCredentials = isAwsProvider && Boolean(new AWS.Config().credentials);
|
||||
payload.npmDependencies = npmDependencies;
|
||||
payload.config = getServiceConfig({ configuration, options, variableSources });
|
||||
payload.isConfigValid = getConfigurationValidationResult(configuration);
|
||||
payload.dashboard.orgUid =
|
||||
serverless && serverless.isDashboardEnabled ? serverless.service.orgUid : undefined;
|
||||
if (isAwsProvider && serverless && commandsReportingProjectId.has(command)) {
|
||||
const serviceName = isObject(configuration.service)
|
||||
? configuration.service.name
|
||||
: configuration.service;
|
||||
const accountId = serverless && serverless.getProvider('aws').accountId;
|
||||
if (serviceName && accountId) {
|
||||
payload.projectId = crypto
|
||||
.createHash('sha256')
|
||||
.update(`${serviceName}-${accountId}`)
|
||||
.digest('base64');
|
||||
}
|
||||
}
|
||||
|
||||
if (isAwsProvider && serverless && (command === 'deploy' || command === '')) {
|
||||
payload.didCreateService = Boolean(
|
||||
serverless && serverless.getProvider('aws').didCreateService
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (commandUsage) {
|
||||
payload.commandUsage = commandUsage;
|
||||
}
|
||||
|
||||
log.debug('payload %o', payload);
|
||||
return payload;
|
||||
};
|
||||
@ -1,195 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const { join } = require('path');
|
||||
const ensurePlainObject = require('type/plain-object/ensure');
|
||||
const { v1: uuid } = require('uuid');
|
||||
const fetch = require('node-fetch');
|
||||
const fse = require('fs-extra');
|
||||
const fsp = require('fs').promises;
|
||||
const telemetryUrl = require('@serverless/utils/analytics-and-notfications-url');
|
||||
const { log } = require('@serverless/utils/log');
|
||||
const isTelemetryDisabled = require('./are-disabled');
|
||||
const cacheDirPath = require('./cache-path');
|
||||
|
||||
const telemetryLog = log.get('telemetry');
|
||||
|
||||
const timestampWeekBefore = Date.now() - 1000 * 60 * 60 * 24 * 7;
|
||||
|
||||
const isUuid = RegExp.prototype.test.bind(
|
||||
/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/
|
||||
);
|
||||
|
||||
let serverlessRunEndTime;
|
||||
|
||||
const logError = (type, error) => {
|
||||
telemetryLog.debug('User stats error: %s: %O', type, error);
|
||||
if (!process.env.SLS_STATS_DEBUG) return;
|
||||
};
|
||||
|
||||
const markServerlessRunEnd = () => (serverlessRunEndTime = Date.now());
|
||||
|
||||
const processResponseBody = async (response, ids, startTime) => {
|
||||
let result;
|
||||
|
||||
try {
|
||||
result = await response.json();
|
||||
} catch (error) {
|
||||
logError(`Response processing error for ${ids || '<no id>'}`, error);
|
||||
return null;
|
||||
}
|
||||
|
||||
const endTime = Date.now();
|
||||
if (serverlessRunEndTime) {
|
||||
telemetryLog.debug(
|
||||
'Stats request prevented process from exiting for %dms (request time: %dms)',
|
||||
endTime - serverlessRunEndTime,
|
||||
endTime - startTime
|
||||
);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
async function request(payload, { ids, timeout } = {}) {
|
||||
const startTime = Date.now();
|
||||
let response;
|
||||
const body = JSON.stringify(payload);
|
||||
try {
|
||||
response = await fetch(telemetryUrl, {
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
// Ensure reasonable timeout to not block process from exiting
|
||||
timeout: timeout || 3500,
|
||||
body,
|
||||
});
|
||||
} catch (networkError) {
|
||||
logError('Request network error', networkError);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (response.status < 200 || response.status >= 300) {
|
||||
logError('Unexpected request response', response);
|
||||
return processResponseBody(response, ids, startTime);
|
||||
}
|
||||
|
||||
if (!ids) return processResponseBody(response, ids, startTime);
|
||||
|
||||
await Promise.all(
|
||||
ids.map(async (id) => {
|
||||
const cachePath = join(cacheDirPath, id);
|
||||
try {
|
||||
await fsp.unlink(cachePath);
|
||||
} catch (error) {
|
||||
logError(`Could not remove cache file ${id}`, error);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return processResponseBody(response, ids, startTime);
|
||||
}
|
||||
|
||||
// This method is explicitly kept as synchronous. The reason for it being the fact that it needs to
|
||||
// be executed in such manner due to its use in `process.on('SIGINT')` handler.
|
||||
function storeLocally(payload, options = {}) {
|
||||
ensurePlainObject(payload);
|
||||
if (!telemetryUrl) return null;
|
||||
const isForced = options && options.isForced;
|
||||
if (isTelemetryDisabled && !isForced) return null;
|
||||
if (!cacheDirPath) return null;
|
||||
const id = uuid();
|
||||
|
||||
return (function self() {
|
||||
try {
|
||||
// Additionally, we also append `id` to the payload to be used as $insert_id in Mixpanel to ensure event deduplication
|
||||
return fse.writeJsonSync(join(cacheDirPath, id), {
|
||||
payload: { ...payload, id },
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
try {
|
||||
fse.ensureDirSync(cacheDirPath);
|
||||
return self();
|
||||
} catch (ensureDirError) {
|
||||
logError('Cache dir creation error:', ensureDirError);
|
||||
}
|
||||
}
|
||||
logError(`Write cache file error: ${id}`, error);
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
async function send(options = {}) {
|
||||
const isForced = options && options.isForced;
|
||||
serverlessRunEndTime = null; // Needed for testing
|
||||
if (options.serverlessExecutionSpan) {
|
||||
options.serverlessExecutionSpan.then(markServerlessRunEnd, markServerlessRunEnd);
|
||||
}
|
||||
if (isTelemetryDisabled && !isForced) return null;
|
||||
if (!cacheDirPath) return null;
|
||||
if (!telemetryUrl) return null;
|
||||
let dirFilenames;
|
||||
try {
|
||||
dirFilenames = await fsp.readdir(cacheDirPath);
|
||||
} catch (readdirError) {
|
||||
if (readdirError.code !== 'ENOENT') logError('Cannot access cache dir', readdirError);
|
||||
return null;
|
||||
}
|
||||
|
||||
const payloadsWithIds = (
|
||||
await Promise.all(
|
||||
dirFilenames.map(async (dirFilename) => {
|
||||
if (!isUuid(dirFilename)) return null;
|
||||
let data;
|
||||
try {
|
||||
data = await fse.readJson(join(cacheDirPath, dirFilename));
|
||||
} catch (readJsonError) {
|
||||
if (readJsonError.code === 'ENOENT') return null; // Race condition
|
||||
logError(`Cannot read cache file: ${dirFilename}`, readJsonError);
|
||||
const cacheFile = join(cacheDirPath, dirFilename);
|
||||
try {
|
||||
return await fsp.unlink(cacheFile);
|
||||
} catch (error) {
|
||||
logError(`Could not remove cache file ${dirFilename}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
if (data && data.payload) {
|
||||
const timestamp = Number(data.timestamp);
|
||||
if (timestamp > timestampWeekBefore) {
|
||||
return {
|
||||
payload: data.payload,
|
||||
id: dirFilename,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
logError(`Invalid cached data ${dirFilename}`, data);
|
||||
}
|
||||
|
||||
const cacheFile = join(cacheDirPath, dirFilename);
|
||||
try {
|
||||
return await fsp.unlink(cacheFile);
|
||||
} catch (error) {
|
||||
logError(`Could not remove cache file ${dirFilename}`, error);
|
||||
}
|
||||
return null;
|
||||
})
|
||||
)
|
||||
).filter(Boolean);
|
||||
|
||||
if (!payloadsWithIds.length) return null;
|
||||
|
||||
return request(
|
||||
payloadsWithIds
|
||||
.map((item) => item.payload)
|
||||
.sort((item, other) => item.timestamp - other.timestamp),
|
||||
{
|
||||
ids: payloadsWithIds.map((item) => item.id),
|
||||
timeout: 3000,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = { storeLocally, send };
|
||||
@ -1,27 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const anonymizeStacktracePaths = require('./anonymize-stacktrace-paths');
|
||||
|
||||
const resolveErrorLocation = (exceptionTokens) => {
|
||||
if (!exceptionTokens.stack) return '<not accessible due to non-error exception>';
|
||||
|
||||
const splittedStack = exceptionTokens.stack.split(/[\r\n]+/);
|
||||
if (splittedStack.length === 1 && exceptionTokens.code) return '<not available>';
|
||||
|
||||
const stacktraceLineRegex = /(?:\s*at.*\((.*:\d+:\d+)\).?|\s*at\s(.*:\d+:\d+))/;
|
||||
const stacktracePaths = [];
|
||||
for (const line of splittedStack) {
|
||||
const match = line.match(stacktraceLineRegex) || [];
|
||||
const matchedPath = match[1] || match[2];
|
||||
if (matchedPath) {
|
||||
// Limited to maximum 7 lines in location
|
||||
if (stacktracePaths.push(matchedPath) === 7) break;
|
||||
} else if (stacktracePaths.length) break;
|
||||
}
|
||||
|
||||
if (!stacktracePaths.length) return '<not reflected in stack>';
|
||||
|
||||
return anonymizeStacktracePaths(stacktracePaths).join('\n').replace(/\\/g, '/');
|
||||
};
|
||||
|
||||
module.exports = resolveErrorLocation;
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "serverless",
|
||||
"version": "3.38.0",
|
||||
"description": "Serverless Framework - Build web, mobile and IoT applications with serverless architectures using AWS Lambda, Azure Functions, Google CloudFunctions & more",
|
||||
"version": "4.0.0",
|
||||
"description": "Serverless Framework - Build web, mobile and IoT applications with serverless architectures using AWS Lambda",
|
||||
"preferGlobal": true,
|
||||
"homepage": "https://serverless.com/framework/docs/",
|
||||
"author": "serverless.com",
|
||||
@ -18,8 +18,10 @@
|
||||
],
|
||||
"main": "lib/serverless.js",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "^3.410.0",
|
||||
"@aws-sdk/client-cloudformation": "^3.410.0",
|
||||
"@aws-sdk/client-ssm": "^3.501.0",
|
||||
"@aws-sdk/client-sts": "^3.410.0",
|
||||
"@aws-sdk/credential-provider-node": "^3.501.0",
|
||||
"@serverless/platform-client": "^4.5.1",
|
||||
"@serverless/utils": "^6.14.0",
|
||||
"abort-controller": "^3.0.0",
|
||||
@ -228,7 +230,6 @@
|
||||
"pkg:build": "node ./scripts/pkg/build.js",
|
||||
"pkg:generate-choco-package": "node ./scripts/pkg/generate-choco-package.js",
|
||||
"pkg:upload": "node ./scripts/pkg/upload/index.js",
|
||||
"postinstall": "node ./scripts/postinstall.js",
|
||||
"prepare-release": "standard-version && prettier --write CHANGELOG.md",
|
||||
"prettier-check": "prettier -c \"**/*.{css,html,js,json,md,yaml,yml}\"",
|
||||
"prettier-check:updated": "pipe-git-updated --ext=css --ext=html --ext=js --ext=json --ext=md --ext=yaml --ext=yml --base=main -- prettier -c",
|
||||
|
||||
@ -1,32 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const chalk = require('chalk');
|
||||
const isStandaloneExecutable = require('../lib/utils/is-standalone-executable');
|
||||
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
const truthyStr = (val) => val && !['0', 'false', 'f', 'n', 'no'].includes(val.toLowerCase());
|
||||
const { CI, ADBLOCK, SILENT } = process.env;
|
||||
const isNpmGlobalPackage = require('../lib/utils/npm-package/is-global');
|
||||
|
||||
if (!truthyStr(CI) && !truthyStr(ADBLOCK) && !truthyStr(SILENT)) {
|
||||
const messageTokens = ['Serverless Framework successfully installed!'];
|
||||
|
||||
if (isStandaloneExecutable && !isWindows) {
|
||||
messageTokens.push(
|
||||
'To start your first project, please open another terminal and run “serverless”.'
|
||||
);
|
||||
} else {
|
||||
messageTokens.push('To start your first project run “serverless”.');
|
||||
}
|
||||
|
||||
if ((isStandaloneExecutable && !isWindows) || isNpmGlobalPackage()) {
|
||||
messageTokens.push('Turn on automatic updates by running “serverless config --autoupdate”.');
|
||||
}
|
||||
|
||||
if (isStandaloneExecutable && !isWindows) {
|
||||
messageTokens.push('Uninstall at any time by running “serverless uninstall”.');
|
||||
}
|
||||
|
||||
process.stdout.write(`${chalk.grey(messageTokens.join('\n\n'))}\n`);
|
||||
}
|
||||
@ -1,853 +0,0 @@
|
||||
#!/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'));
|
||||
|
||||
// Setup log writing
|
||||
require('@serverless/utils/log-reporters/node');
|
||||
const { log, progress, isInteractive: isInteractiveTerminal } = require('@serverless/utils/log');
|
||||
|
||||
const processLog = log.get('process');
|
||||
|
||||
const handleError = require('../lib/cli/handle-error');
|
||||
const {
|
||||
storeLocally: storeTelemetryLocally,
|
||||
send: sendTelemetry,
|
||||
} = require('../lib/utils/telemetry');
|
||||
const generateTelemetryPayload = require('../lib/utils/telemetry/generate-payload');
|
||||
const isTelemetryDisabled = require('../lib/utils/telemetry/are-disabled');
|
||||
const logDeprecation = require('../lib/utils/log-deprecation');
|
||||
|
||||
let command;
|
||||
let isHelpRequest;
|
||||
let options;
|
||||
let commandSchema;
|
||||
let serviceDir = null;
|
||||
let configuration = null;
|
||||
let serverless;
|
||||
let dockerVersion;
|
||||
const commandUsage = {};
|
||||
const variableSourcesInConfig = new Set();
|
||||
|
||||
// Inquirer async operations do not keep node process alive
|
||||
// We need to issue a keep alive timer so process does not die
|
||||
// to properly handle e.g. `SIGINT` interrupt
|
||||
const keepAliveTimer = setTimeout(() => {}, 60 * 60 * 1000);
|
||||
|
||||
const trueWithProbability = (probability) => Math.random() < probability;
|
||||
|
||||
let processSpanPromise;
|
||||
let hasBeenFinalized = false;
|
||||
const finalize = async ({ error, shouldBeSync, telemetryData, shouldSendTelemetry } = {}) => {
|
||||
processLog.debug('finalize %o', { error, shouldBeSync, telemetryData, shouldSendTelemetry });
|
||||
if (hasBeenFinalized) {
|
||||
if (error) {
|
||||
// Programmer error in finalize handling, ensure to expose
|
||||
process.nextTick(() => {
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
hasBeenFinalized = true;
|
||||
clearTimeout(keepAliveTimer);
|
||||
progress.clear();
|
||||
if (error) ({ telemetryData } = await handleError(error, { serverless }));
|
||||
if (!shouldBeSync) {
|
||||
await logDeprecation.printSummary();
|
||||
}
|
||||
if (isTelemetryDisabled || !commandSchema) return null;
|
||||
if (!error && isHelpRequest) return null;
|
||||
storeTelemetryLocally({
|
||||
...generateTelemetryPayload({
|
||||
command,
|
||||
options,
|
||||
commandSchema,
|
||||
serviceDir,
|
||||
configuration,
|
||||
serverless,
|
||||
commandUsage,
|
||||
variableSources: variableSourcesInConfig,
|
||||
dockerVersion,
|
||||
}),
|
||||
...telemetryData,
|
||||
});
|
||||
|
||||
// We want to explicitly ensure that when processing should be sync, we never attempt sending telemetry data
|
||||
if (shouldBeSync) return null;
|
||||
|
||||
// We want to send telemetry at least roughly every 20 commands (in addition to sending on deploy and on errors)
|
||||
// to avoid situations where we have very big batches of telemetry events that cannot be processed on the backend side
|
||||
const shouldForceTelemetry = trueWithProbability(0.05);
|
||||
|
||||
if (!error && !shouldSendTelemetry && !shouldForceTelemetry) return null;
|
||||
return sendTelemetry({ serverlessExecutionSpan: processSpanPromise });
|
||||
};
|
||||
|
||||
process.once('uncaughtException', (error) => {
|
||||
log.error('Uncaught exception');
|
||||
finalize({ error }).then(() => process.exit());
|
||||
});
|
||||
|
||||
processSpanPromise = (async () => {
|
||||
try {
|
||||
const wait = require('timers-ext/promise/sleep');
|
||||
await wait(); // Ensure access to "processSpanPromise"
|
||||
|
||||
require('signal-exit/signals').forEach((signal) => {
|
||||
process.once(signal, () => {
|
||||
processLog.debug('exit signal %s', signal);
|
||||
// If there's another listener (e.g. we're in daemon context or reading stdin input)
|
||||
// then let the other listener decide how process will exit
|
||||
const isOtherSigintListener = Boolean(process.listenerCount(signal));
|
||||
finalize({
|
||||
shouldBeSync: true,
|
||||
telemetryData: { outcome: 'interrupt', interruptSignal: signal },
|
||||
});
|
||||
if (isOtherSigintListener) return;
|
||||
// Follow recommendation from signal-exit:
|
||||
// https://github.com/tapjs/signal-exit/blob/654117d6c9035ff6a805db4d4acf1f0c820fcb21/index.js#L97-L98
|
||||
if (process.platform === 'win32' && signal === 'SIGHUP') signal = 'SIGINT';
|
||||
process.kill(process.pid, signal);
|
||||
});
|
||||
});
|
||||
|
||||
const humanizePropertyPathKeys = require('../lib/configuration/variables/humanize-property-path-keys');
|
||||
const processBackendNotificationRequest = require('../lib/utils/process-backend-notification-request');
|
||||
|
||||
(() => {
|
||||
// Rewrite eventual `sls deploy -f` into `sls deploy function -f`
|
||||
// Also rewrite `serverless dev` to `serverless --dev``
|
||||
const isParamName = RegExp.prototype.test.bind(require('../lib/cli/param-reg-exp'));
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const firstParamIndex = args.findIndex(isParamName);
|
||||
const commands = args.slice(0, firstParamIndex === -1 ? Infinity : firstParamIndex);
|
||||
|
||||
if (commands.join('') === 'dev') {
|
||||
process.argv[2] = '--dev';
|
||||
return;
|
||||
}
|
||||
|
||||
if (commands.join(' ') !== 'deploy') return;
|
||||
if (!args.includes('-f') && !args.includes('--function')) return;
|
||||
logDeprecation(
|
||||
'CLI_DEPLOY_FUNCTION_OPTION_V3',
|
||||
'Starting with v4.0.0, `--function` or `-f` option for `deploy` command will no longer be supported. In order to deploy a single function, please use `deploy function` command instead.'
|
||||
);
|
||||
process.argv.splice(3, 0, 'function');
|
||||
})();
|
||||
|
||||
const resolveInput = require('../lib/cli/resolve-input');
|
||||
|
||||
let commands;
|
||||
processLog.debug('resolve CLI input (no service schema)');
|
||||
// Parse args against schemas of commands which do not require to be run in service context
|
||||
({ command, commands, options, isHelpRequest, commandSchema } = resolveInput(
|
||||
require('../lib/cli/commands-schema/no-service')
|
||||
));
|
||||
|
||||
// If version number request, show it and abort
|
||||
if (options.version) {
|
||||
processLog.debug('render version');
|
||||
await require('../lib/cli/render-version')();
|
||||
await finalize();
|
||||
return;
|
||||
}
|
||||
|
||||
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}`,
|
||||
'NOT_SUPPORTED_COMMAND'
|
||||
);
|
||||
}
|
||||
|
||||
const path = require('path');
|
||||
const uuid = require('uuid');
|
||||
const _ = require('lodash');
|
||||
const clear = require('ext/object/clear');
|
||||
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 isDashboardEnabled = require('../lib/configuration/is-dashboard-enabled');
|
||||
|
||||
let configurationPath = null;
|
||||
let providerName;
|
||||
let variablesMeta;
|
||||
let resolverConfiguration;
|
||||
let isInteractiveSetup;
|
||||
|
||||
const ensureResolvedProperty = (propertyPath) => {
|
||||
if (isPropertyResolved(variablesMeta, propertyPath)) return true;
|
||||
variablesMeta = null;
|
||||
if (isHelpRequest) return false;
|
||||
const humanizedPropertyPath = humanizePropertyPathKeys(propertyPath.split('\0'));
|
||||
throw new ServerlessError(
|
||||
`Cannot resolve ${path.basename(
|
||||
configurationPath
|
||||
)}: "${humanizedPropertyPath}" property is not accessible ` +
|
||||
'(configured behind variables which cannot be resolved at this stage)',
|
||||
'INACCESSIBLE_CONFIGURATION_PROPERTY'
|
||||
);
|
||||
};
|
||||
|
||||
if (!commandSchema || commandSchema.serviceDependencyMode) {
|
||||
// Command is potentially service specific, follow up with resolution of service config
|
||||
|
||||
// Parse args again, taking account schema of service-specific flags
|
||||
// as they may influence configuration resolution
|
||||
processLog.debug('resolve CLI input (service schema)');
|
||||
resolveInput.clear();
|
||||
({ command, commands, options, isHelpRequest, commandSchema } = resolveInput(
|
||||
require('../lib/cli/commands-schema/service')
|
||||
));
|
||||
|
||||
isInteractiveSetup = !isHelpRequest && command === '';
|
||||
|
||||
processLog.debug('resolve eventual service configuration');
|
||||
const resolveConfigurationPath = require('../lib/cli/resolve-configuration-path');
|
||||
const readConfiguration = require('../lib/configuration/read');
|
||||
const resolveProviderName = require('../lib/configuration/resolve-provider-name');
|
||||
|
||||
// Resolve eventual service configuration path
|
||||
configurationPath = await resolveConfigurationPath();
|
||||
if (configurationPath) {
|
||||
processLog.debug('service configuration found at %s', configurationPath);
|
||||
} else processLog.debug('no service configuration found');
|
||||
|
||||
// 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) {
|
||||
processLog.debug('service configuration file successfully parsed');
|
||||
serviceDir = process.cwd();
|
||||
|
||||
// IIFE for maintenance convenience
|
||||
await (async () => {
|
||||
// We do not need to attempt resolution of further variables for login command as
|
||||
// the only variables from configuration that we potentially rely on is `app` and `org`
|
||||
// TODO: Remove when dashboard/console login prompt won't be needed - when that happens
|
||||
// login command should once again be service independent
|
||||
if (command === 'login') return;
|
||||
|
||||
processLog.debug('resolve variables meta');
|
||||
const resolveVariablesMeta = require('../lib/configuration/variables/resolve-meta');
|
||||
|
||||
variablesMeta = resolveVariablesMeta(configuration);
|
||||
|
||||
if (
|
||||
eventuallyReportVariableResolutionErrors(
|
||||
configurationPath,
|
||||
configuration,
|
||||
variablesMeta
|
||||
)
|
||||
) {
|
||||
// Variable syntax errors, abort
|
||||
variablesMeta = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ensureResolvedProperty('disabledDeprecations')) return;
|
||||
if (!ensureResolvedProperty('deprecationNotificationMode')) return;
|
||||
|
||||
if (isPropertyResolved(variablesMeta, 'provider\0name')) {
|
||||
providerName = resolveProviderName(configuration);
|
||||
if (providerName == null) {
|
||||
variablesMeta = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!commandSchema && providerName === 'aws') {
|
||||
// If command was not recognized in previous resolution phases
|
||||
// parse args again also against schemas commands which require AWS service context
|
||||
processLog.debug('resolve CLI input (AWS service schema)');
|
||||
resolveInput.clear();
|
||||
({ command, commands, options, isHelpRequest, commandSchema } = resolveInput(
|
||||
require('../lib/cli/commands-schema/aws-service')
|
||||
));
|
||||
}
|
||||
|
||||
let envVarNamesNeededForDotenvResolution;
|
||||
if (variablesMeta.size) {
|
||||
processLog.debug('resolve variables in core properties');
|
||||
// Some properties are configured with variables
|
||||
|
||||
// Resolve eventual variables in `provider.stage` and `useDotEnv`
|
||||
// (required for reliable .env resolution)
|
||||
resolverConfiguration = {
|
||||
serviceDir,
|
||||
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'),
|
||||
sls: require('../lib/configuration/variables/sources/instance-dependent/get-sls')(),
|
||||
},
|
||||
options: filterSupportedOptions(options, { commandSchema, providerName }),
|
||||
fulfilledSources: new Set(['file', 'self', 'strToBool']),
|
||||
propertyPathsToResolve: new Set(['provider\0name', 'provider\0stage', 'useDotenv']),
|
||||
variableSourcesInConfig,
|
||||
};
|
||||
|
||||
if (isInteractiveSetup) resolverConfiguration.fulfilledSources.add('opt');
|
||||
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 (providerName == null) {
|
||||
variablesMeta = null;
|
||||
return;
|
||||
}
|
||||
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
|
||||
processLog.debug('resolve CLI input (AWS service schema)');
|
||||
resolveInput.clear();
|
||||
({ command, commands, options, isHelpRequest, commandSchema } = resolveInput(
|
||||
require('../lib/cli/commands-schema/aws-service')
|
||||
));
|
||||
|
||||
if (commandSchema) {
|
||||
processLog.debug('resolve variables in core properties #2');
|
||||
resolverConfiguration.options = filterSupportedOptions(options, {
|
||||
commandSchema,
|
||||
providerName,
|
||||
});
|
||||
await resolveVariables(resolverConfiguration);
|
||||
if (
|
||||
eventuallyReportVariableResolutionErrors(
|
||||
configurationPath,
|
||||
configuration,
|
||||
variablesMeta
|
||||
)
|
||||
) {
|
||||
variablesMeta = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resolverConfiguration.fulfilledSources.add('env');
|
||||
if (
|
||||
!isPropertyResolved(variablesMeta, 'provider\0stage') ||
|
||||
!isPropertyResolved(variablesMeta, 'useDotenv')
|
||||
) {
|
||||
// Assume "env" source fulfilled for `provider.stage` and `useDotenv` resolution.
|
||||
// To pick eventual resolution conflict, track what env variables were reported
|
||||
// missing when applying this resolution
|
||||
processLog.debug('resolve variables in stage related properties');
|
||||
const envSource = require('../lib/configuration/variables/sources/env');
|
||||
envSource.missingEnvVariables.clear();
|
||||
await resolveVariables({
|
||||
...resolverConfiguration,
|
||||
propertyPathsToResolve: new Set(['provider\0stage', 'useDotenv']),
|
||||
});
|
||||
if (
|
||||
eventuallyReportVariableResolutionErrors(
|
||||
configurationPath,
|
||||
configuration,
|
||||
variablesMeta
|
||||
)
|
||||
) {
|
||||
// Unrecoverable resolution errors, abort
|
||||
variablesMeta = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!ensureResolvedProperty('provider\0stage', {
|
||||
shouldSilentlyReturnIfLegacyMode: true,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ensureResolvedProperty('useDotenv')) return;
|
||||
|
||||
envVarNamesNeededForDotenvResolution = envSource.missingEnvVariables;
|
||||
}
|
||||
}
|
||||
|
||||
// Load eventual environment variables from .env files
|
||||
if (await require('../lib/cli/conditionally-load-dotenv')(options, configuration)) {
|
||||
if (envVarNamesNeededForDotenvResolution) {
|
||||
for (const envVarName of envVarNamesNeededForDotenvResolution) {
|
||||
if (process.env[envVarName]) {
|
||||
throw new ServerlessError(
|
||||
'Cannot reliably resolve "env" variables due to resolution conflict.\n' +
|
||||
`Environment variable "${envVarName}" which influences resolution of ` +
|
||||
'".env" file were found to be defined in resolved ".env" file.' +
|
||||
'DOTENV_ENV_VAR_RESOLUTION_CONFLICT'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!isPropertyResolved(variablesMeta, 'provider\0name')) {
|
||||
processLog.debug('resolve variables in "provider.name"');
|
||||
await resolveVariables(resolverConfiguration);
|
||||
if (
|
||||
eventuallyReportVariableResolutionErrors(
|
||||
configurationPath,
|
||||
configuration,
|
||||
variablesMeta
|
||||
)
|
||||
) {
|
||||
variablesMeta = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!variablesMeta.size) return; // No properties configured with variables
|
||||
|
||||
if (!providerName) {
|
||||
if (!ensureResolvedProperty('provider\0name')) return;
|
||||
providerName = resolveProviderName(configuration);
|
||||
if (providerName == null) {
|
||||
variablesMeta = null;
|
||||
return;
|
||||
}
|
||||
if (!commandSchema && providerName === 'aws') {
|
||||
processLog.debug('resolve CLI input (AWS service schema)');
|
||||
resolveInput.clear();
|
||||
({ command, commands, options, isHelpRequest, commandSchema } = resolveInput(
|
||||
require('../lib/cli/commands-schema/aws-service')
|
||||
));
|
||||
if (commandSchema) {
|
||||
resolverConfiguration.options = filterSupportedOptions(options, {
|
||||
commandSchema,
|
||||
providerName,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isHelpRequest || commands[0] === 'plugin') {
|
||||
processLog.debug('resolve variables in "plugins"');
|
||||
// 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 {
|
||||
processLog.debug('resolve variables in all properties');
|
||||
delete resolverConfiguration.propertyPathsToResolve;
|
||||
}
|
||||
|
||||
await resolveVariables(resolverConfiguration);
|
||||
if (
|
||||
eventuallyReportVariableResolutionErrors(
|
||||
configurationPath,
|
||||
configuration,
|
||||
variablesMeta
|
||||
)
|
||||
) {
|
||||
variablesMeta = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!variablesMeta.size) return; // All properties successfully resolved
|
||||
|
||||
if (!ensureResolvedProperty('plugins')) return;
|
||||
|
||||
// At this point we have all properties needed for `plugin install/uninstall` commands
|
||||
if (commands[0] === 'plugin') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ensureResolvedProperty('package\0path')) return;
|
||||
|
||||
if (!ensureResolvedProperty('frameworkVersion')) return;
|
||||
if (!ensureResolvedProperty('app')) return;
|
||||
if (!ensureResolvedProperty('org')) return;
|
||||
if (!ensureResolvedProperty('dashboard')) return;
|
||||
if (!ensureResolvedProperty('service')) return;
|
||||
if (isDashboardEnabled({ configuration, options })) {
|
||||
// Dashboard requires AWS region to be resolved upfront
|
||||
ensureResolvedProperty('provider\0region');
|
||||
}
|
||||
})();
|
||||
|
||||
// Ensure to have full AWS commands schema loaded if we're in context of AWS provider
|
||||
// It's not the case if not AWS service specific command was resolved
|
||||
if (configuration && resolveProviderName(configuration) === 'aws') {
|
||||
processLog.debug('resolve CLI input (AWS service schema)');
|
||||
resolveInput.clear();
|
||||
({ command, commands, options, isHelpRequest, commandSchema } = resolveInput(
|
||||
require('../lib/cli/commands-schema/aws-service')
|
||||
));
|
||||
}
|
||||
} else {
|
||||
// In non-service context we recognize all AWS service commands
|
||||
processLog.debug('parsing of configuration file failed');
|
||||
processLog.debug('resolve CLI input (AWS service schema)');
|
||||
resolveInput.clear();
|
||||
({ command, commands, options, isHelpRequest, commandSchema } = resolveInput(
|
||||
require('../lib/cli/commands-schema/aws-service')
|
||||
));
|
||||
|
||||
// Validate result command and options
|
||||
require('../lib/cli/ensure-supported-command')();
|
||||
}
|
||||
} else {
|
||||
require('../lib/cli/ensure-supported-command')();
|
||||
}
|
||||
|
||||
const configurationFilename = configuration && configurationPath.slice(serviceDir.length + 1);
|
||||
|
||||
// Names of the commands which are configured independently in root `commands` folder
|
||||
// and not in Serverless class internals
|
||||
const notIntegratedCommands = new Set([
|
||||
'doctor',
|
||||
'login',
|
||||
'logout',
|
||||
'plugin install',
|
||||
'plugin uninstall',
|
||||
]);
|
||||
const isStandaloneCommand = notIntegratedCommands.has(command);
|
||||
|
||||
if (!isHelpRequest) {
|
||||
if (isStandaloneCommand) {
|
||||
processLog.debug('run standalone command');
|
||||
if (configuration) require('../lib/cli/ensure-supported-command')(configuration);
|
||||
await require(`../commands/${commands.join('-')}`)({
|
||||
configuration,
|
||||
serviceDir,
|
||||
configurationFilename,
|
||||
options,
|
||||
});
|
||||
await finalize({ telemetryData: { outcome: 'success' } });
|
||||
return;
|
||||
} else if (isInteractiveSetup) {
|
||||
if (!isInteractiveTerminal) {
|
||||
throw new ServerlessError(
|
||||
'Attempted to run an interactive setup in non TTY environment.\n' +
|
||||
"If that's intended, run with the SLS_INTERACTIVE_SETUP_ENABLE=1 environment variable",
|
||||
'INTERACTIVE_SETUP_IN_NON_TTY'
|
||||
);
|
||||
}
|
||||
if (!configuration) {
|
||||
processLog.debug('run interactive onboarding');
|
||||
const interactiveContext = await require('../lib/cli/interactive-setup')({
|
||||
configuration,
|
||||
serviceDir,
|
||||
configurationFilename,
|
||||
options,
|
||||
commandUsage,
|
||||
});
|
||||
if (interactiveContext.configuration) {
|
||||
configuration = interactiveContext.configuration;
|
||||
}
|
||||
if (interactiveContext.serverless) {
|
||||
serverless = interactiveContext.serverless;
|
||||
}
|
||||
await finalize({ telemetryData: { outcome: 'success' }, shouldSendTelemetry: true });
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
processLog.debug('construct Serverless instance');
|
||||
serverless = new Serverless({
|
||||
configuration,
|
||||
serviceDir,
|
||||
configurationFilename,
|
||||
commands,
|
||||
options,
|
||||
variablesMeta,
|
||||
});
|
||||
|
||||
try {
|
||||
serverless.onExitPromise = processSpanPromise;
|
||||
serverless.invocationId = uuid.v4();
|
||||
processLog.debug('initialize Serverless instance');
|
||||
await serverless.init();
|
||||
|
||||
// IIFE for maintenance convenience
|
||||
await (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
|
||||
|
||||
if (serverless.pluginManager.externalPlugins.size) {
|
||||
processLog.debug('resolve CLI input (+ plugins schema)');
|
||||
const commandsSchema = require('../lib/cli/commands-schema/resolve-final')(
|
||||
serverless.pluginManager.externalPlugins,
|
||||
{ providerName: providerName || 'aws', configuration }
|
||||
);
|
||||
resolveInput.clear();
|
||||
({ command, commands, options, isHelpRequest, commandSchema } =
|
||||
resolveInput(commandsSchema));
|
||||
serverless.processedInput.commands = serverless.pluginManager.cliCommands = commands;
|
||||
serverless.processedInput.options = options;
|
||||
Object.assign(clear(serverless.pluginManager.cliOptions), options);
|
||||
hasFinalCommandSchema = true;
|
||||
}
|
||||
}
|
||||
if (!providerName && !hasFinalCommandSchema) {
|
||||
// Invalid configuration, ensure to recognize all AWS commands
|
||||
processLog.debug('resolve CLI input (AWS service schema)');
|
||||
resolveInput.clear();
|
||||
({ command, commands, options, isHelpRequest, commandSchema } = resolveInput(
|
||||
require('../lib/cli/commands-schema/aws-service')
|
||||
));
|
||||
}
|
||||
hasFinalCommandSchema = true;
|
||||
|
||||
// Validate result command and options
|
||||
if (hasFinalCommandSchema) require('../lib/cli/ensure-supported-command')(configuration);
|
||||
if (isHelpRequest) return;
|
||||
if (!_.get(variablesMeta, 'size')) return;
|
||||
if (!resolverConfiguration) {
|
||||
// There were no variables in the initial configuration, yet it was extended by
|
||||
// the plugins with ones.
|
||||
// In this case we need to ensure `resolverConfiguration` which initially was not setup
|
||||
resolverConfiguration = {
|
||||
serviceDir,
|
||||
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'),
|
||||
sls: require('../lib/configuration/variables/sources/instance-dependent/get-sls')(),
|
||||
},
|
||||
options: filterSupportedOptions(options, { commandSchema, providerName }),
|
||||
fulfilledSources: new Set(['env', 'file', 'self', 'strToBool']),
|
||||
propertyPathsToResolve:
|
||||
commands[0] === 'plugin'
|
||||
? new Set(['plugins', 'provider\0name', 'provider\0stage', 'useDotenv'])
|
||||
: null,
|
||||
variableSourcesInConfig,
|
||||
};
|
||||
}
|
||||
|
||||
if (commandSchema) {
|
||||
resolverConfiguration.options = filterSupportedOptions(options, {
|
||||
commandSchema,
|
||||
providerName,
|
||||
});
|
||||
}
|
||||
resolverConfiguration.fulfilledSources.add('opt');
|
||||
|
||||
// Register serverless instance specific variable sources
|
||||
resolverConfiguration.sources.sls =
|
||||
require('../lib/configuration/variables/sources/instance-dependent/get-sls')(serverless);
|
||||
resolverConfiguration.fulfilledSources.add('sls');
|
||||
|
||||
resolverConfiguration.sources.param =
|
||||
serverless.pluginManager.dashboardPlugin.configurationVariablesSources.param;
|
||||
resolverConfiguration.fulfilledSources.add('param');
|
||||
|
||||
// Register dashboard specific variable source resolvers
|
||||
if (isDashboardEnabled({ configuration, options })) {
|
||||
for (const [sourceName, sourceConfig] of Object.entries(
|
||||
serverless.pluginManager.dashboardPlugin.configurationVariablesSources
|
||||
)) {
|
||||
if (sourceName === 'param') continue;
|
||||
resolverConfiguration.sources[sourceName] = sourceConfig;
|
||||
resolverConfiguration.fulfilledSources.add(sourceName);
|
||||
}
|
||||
}
|
||||
|
||||
// Register AWS provider specific variable sources
|
||||
if (providerName === 'aws') {
|
||||
// Pre-resolve to eventually pick not yet resolved AWS auth related properties
|
||||
processLog.debug('resolve variables');
|
||||
await resolveVariables(resolverConfiguration);
|
||||
if (!variablesMeta.size) return;
|
||||
if (
|
||||
eventuallyReportVariableResolutionErrors(
|
||||
configurationPath,
|
||||
configuration,
|
||||
variablesMeta
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure properties which are crucial to some variable source resolvers
|
||||
// are actually resolved.
|
||||
if (
|
||||
!ensureResolvedProperty('provider\0credentials') ||
|
||||
!ensureResolvedProperty('provider\0deploymentBucket\0serverSideEncryption') ||
|
||||
!ensureResolvedProperty('provider\0profile') ||
|
||||
!ensureResolvedProperty('provider\0region')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
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
|
||||
),
|
||||
aws: require('../lib/configuration/variables/sources/instance-dependent/get-aws')(
|
||||
serverless
|
||||
),
|
||||
});
|
||||
resolverConfiguration.fulfilledSources.add('cf').add('s3').add('ssm').add('aws');
|
||||
}
|
||||
|
||||
// Register variable source resolvers provided by external plugins
|
||||
const resolverExternalPluginSources = require('../lib/configuration/variables/sources/resolve-external-plugin-sources');
|
||||
resolverExternalPluginSources(
|
||||
configuration,
|
||||
resolverConfiguration,
|
||||
serverless.pluginManager.externalPlugins
|
||||
);
|
||||
|
||||
// Having all source resolvers configured, resolve variables
|
||||
processLog.debug('resolve all variables');
|
||||
await resolveVariables(resolverConfiguration);
|
||||
if (!variablesMeta.size) return;
|
||||
if (
|
||||
eventuallyReportVariableResolutionErrors(configurationPath, configuration, variablesMeta)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Do not confirm on unresolved sources with partially resolved configuration
|
||||
if (resolverConfiguration.propertyPathsToResolve) return;
|
||||
|
||||
processLog.debug('uresolved variables meta: %o', variablesMeta);
|
||||
// Report unrecognized variable sources found in variables configured in service config
|
||||
const unresolvedSources =
|
||||
require('../lib/configuration/variables/resolve-unresolved-source-types')(variablesMeta);
|
||||
const recognizedSourceNames = new Set(Object.keys(resolverConfiguration.sources));
|
||||
|
||||
const unrecognizedSourceNames = Array.from(unresolvedSources.keys()).filter(
|
||||
(sourceName) => !recognizedSourceNames.has(sourceName)
|
||||
);
|
||||
|
||||
if (unrecognizedSourceNames.includes('output')) {
|
||||
throw new ServerlessError(
|
||||
'"Cannot resolve configuration: ' +
|
||||
'"output" variable can only be used in ' +
|
||||
'services deployed with Serverless Dashboard (with "org" setting configured)',
|
||||
'DASHBOARD_VARIABLE_SOURCES_MISUSE'
|
||||
);
|
||||
}
|
||||
throw new ServerlessError(
|
||||
`Unrecognized configuration variable sources: "${unrecognizedSourceNames.join('", "')}"`,
|
||||
'UNRECOGNIZED_VARIABLE_SOURCES'
|
||||
);
|
||||
})();
|
||||
|
||||
if (isHelpRequest && serverless.pluginManager.externalPlugins) {
|
||||
// Show help
|
||||
processLog.debug('render help');
|
||||
require('../lib/cli/render-help')(serverless.pluginManager.externalPlugins);
|
||||
} else if (isInteractiveSetup) {
|
||||
processLog.debug('run interactive onboarding');
|
||||
const interactiveContext = await require('../lib/cli/interactive-setup')({
|
||||
configuration,
|
||||
serverless,
|
||||
serviceDir,
|
||||
configurationFilename,
|
||||
options,
|
||||
commandUsage,
|
||||
});
|
||||
if (interactiveContext.configuration) {
|
||||
configuration = interactiveContext.configuration;
|
||||
}
|
||||
if (interactiveContext.serverless) {
|
||||
serverless = interactiveContext.serverless;
|
||||
}
|
||||
} else {
|
||||
if (commands.join(' ') === 'deploy') {
|
||||
const spawn = require('child-process-ext/spawn');
|
||||
spawn('docker', ['--version']).then(
|
||||
({ stdoutBuffer }) => {
|
||||
dockerVersion = null;
|
||||
const matcher = String(stdoutBuffer).match(/(?<version>\d+\.\d+\.\d+),/);
|
||||
if (!matcher) return;
|
||||
dockerVersion = matcher.groups.version;
|
||||
},
|
||||
() => (dockerVersion = null)
|
||||
);
|
||||
}
|
||||
|
||||
processLog.debug('run Serverless instance');
|
||||
// Run command
|
||||
await serverless.run();
|
||||
}
|
||||
|
||||
const backendNotificationRequest = await finalize({
|
||||
telemetryData: { outcome: 'success' },
|
||||
shouldSendTelemetry: isInteractiveSetup || commands.join(' ') === 'deploy',
|
||||
});
|
||||
if (!isInteractiveSetup && backendNotificationRequest) {
|
||||
await processBackendNotificationRequest(backendNotificationRequest);
|
||||
}
|
||||
} catch (error) {
|
||||
processLog.debug('handle error');
|
||||
// If Dashboard Plugin, capture error
|
||||
const dashboardPlugin = serverless.pluginManager.dashboardPlugin;
|
||||
const dashboardErrorHandler = _.get(dashboardPlugin, 'enterprise.errorHandler');
|
||||
if (!dashboardErrorHandler) throw error;
|
||||
try {
|
||||
await dashboardErrorHandler(error, serverless.invocationId);
|
||||
} catch (dashboardErrorHandlerError) {
|
||||
const tokenizeException = require('../lib/utils/tokenize-exception');
|
||||
const exceptionTokens = tokenizeException(dashboardErrorHandlerError);
|
||||
log.warning(
|
||||
`Publication to Serverless Dashboard errored with:\n${' '.repeat('Serverless: '.length)}${
|
||||
exceptionTokens.isUserError || !exceptionTokens.stack
|
||||
? exceptionTokens.message
|
||||
: exceptionTokens.stack
|
||||
}`
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
await finalize({ error });
|
||||
}
|
||||
})();
|
||||
Loading…
x
Reference in New Issue
Block a user