mirror of
https://github.com/serverless/serverless.git
synced 2025-12-08 19:46:03 +00:00
472 lines
20 KiB
JavaScript
472 lines
20 KiB
JavaScript
'use strict';
|
|
|
|
const path = require('path');
|
|
const os = require('os');
|
|
const ensureString = require('type/string/ensure');
|
|
const ensureArray = require('type/array/ensure');
|
|
const ensurePlainObject = require('type/plain-object/ensure');
|
|
const _ = require('lodash');
|
|
const CLI = require('./classes/CLI');
|
|
const Config = require('./classes/Config');
|
|
const YamlParser = require('./classes/YamlParser');
|
|
const PluginManager = require('./classes/PluginManager');
|
|
const Utils = require('./classes/Utils');
|
|
const Service = require('./classes/Service');
|
|
const Variables = require('./classes/Variables');
|
|
const ConfigSchemaHandler = require('./classes/ConfigSchemaHandler');
|
|
const ServerlessError = require('./serverless-error');
|
|
const version = require('./../package.json').version;
|
|
const isStandaloneExecutable = require('./utils/isStandaloneExecutable');
|
|
const resolveConfigurationPath = require('./cli/resolve-configuration-path');
|
|
const logDeprecation = require('./utils/logDeprecation');
|
|
const eventuallyUpdate = require('./utils/eventuallyUpdate');
|
|
const resolveLocalServerlessPath = require('./cli/resolve-local-serverless-path');
|
|
const commmandsSchema = require('./cli/commands-schema');
|
|
const resolveCliInput = require('./cli/resolve-input');
|
|
const readConfiguration = require('./configuration/read');
|
|
const conditionallyLoadDotenv = require('./cli/conditionally-load-dotenv');
|
|
|
|
const serverlessPath = path.resolve(__dirname, '..');
|
|
|
|
class Serverless {
|
|
constructor(config) {
|
|
let configObject = config;
|
|
configObject = configObject || {};
|
|
this._isInvokedByGlobalInstallation = Boolean(configObject._isInvokedByGlobalInstallation);
|
|
|
|
if (configObject.serviceDir != null) {
|
|
// Modern intialization way, to be the only supported way with v3
|
|
this.serviceDir = path.resolve(
|
|
ensureString(configObject.serviceDir, {
|
|
name: 'config.serviceDir',
|
|
Error: ServerlessError,
|
|
errorCode: 'INVALID_NON_STRING_SERVICE_DIR',
|
|
})
|
|
);
|
|
this.configurationFilename = ensureString(configObject.configurationFilename, {
|
|
name: 'config.configurationFilename',
|
|
Error: ServerlessError,
|
|
errorCode: 'INVALID_NON_STRING_CONFIGURATION_FILENAME',
|
|
});
|
|
if (path.isAbsolute(this.configurationFilename)) {
|
|
throw new ServerlessError(
|
|
`"config.configurationFilename" cannot be absolute path. Received: ${configObject.configurationFilename}`,
|
|
'INVALID_ABSOLUTE_PATH_CONFIGURATION_FILENAME'
|
|
);
|
|
}
|
|
this.configurationInput = ensurePlainObject(configObject.configuration, {
|
|
name: 'config.configuration',
|
|
Error: ServerlessError,
|
|
errorCode: 'INVALID_NON_OBJECT_CONFIGURATION',
|
|
});
|
|
this.isConfigurationInputResolved = Boolean(configObject.isConfigurationResolved);
|
|
} else if (configObject.configurationPath != null) {
|
|
// Semi-modern initialization way, mid-step introduced over the course of v2 refactor
|
|
const configurationPath = path.resolve(
|
|
ensureString(configObject.configurationPath, {
|
|
name: 'config.configurationPath',
|
|
Error: ServerlessError,
|
|
errorCode: 'INVALID_NON_STRING_CONFIGURATION_PATH',
|
|
})
|
|
);
|
|
this.serviceDir = process.cwd();
|
|
this.configurationFilename = configurationPath.slice(this.serviceDir.length + 1);
|
|
this.configurationInput = ensurePlainObject(configObject.configuration, {
|
|
isOptional: true,
|
|
name: 'config.configuration',
|
|
Error: ServerlessError,
|
|
errorCode: 'INVALID_NON_OBJECT_CONFIGURATION',
|
|
});
|
|
if (this.configurationInput) {
|
|
this.isConfigurationInputResolved = Boolean(configObject.isConfigurationResolved);
|
|
}
|
|
this._shouldReportMissingServiceDeprecation = true;
|
|
} else if (
|
|
configObject.configurationPath === undefined &&
|
|
configObject.serviceDir === undefined
|
|
) {
|
|
// Old intialization way
|
|
this._shouldResolveConfigurationInternally = true;
|
|
this._shouldReportMissingServiceDeprecation = true;
|
|
}
|
|
const commands = ensureArray(configObject.commands, { isOptional: true });
|
|
let options = ensurePlainObject(configObject.options, { isOptional: true });
|
|
// This is a temporary workaround to ensure that original `options` are not mutated
|
|
// Should be removed after addressing: https://github.com/serverless/serverless/issues/2582
|
|
if (options) options = { ...options };
|
|
if (!commands || !options) {
|
|
this._shouldReportCommandsDeprecation = true;
|
|
this.processedInput = resolveCliInput();
|
|
} else {
|
|
this.processedInput = { commands, options };
|
|
}
|
|
this.hasResolvedCommandsExternally = Boolean(configObject.hasResolvedCommandsExternally);
|
|
this.isTelemetryReportedExternally = Boolean(configObject.isTelemetryReportedExternally);
|
|
|
|
// Due to design flaw properties of configObject (which is to be merged onto `this.config`)
|
|
// also are subject to variables resolution.
|
|
// To avoid that we clear configObject after consuming it's properties.
|
|
// Once new variables engine is in, we can remove that patch
|
|
delete configObject.configurationPath;
|
|
delete configObject.configuration;
|
|
delete configObject.serviceDir;
|
|
delete configObject._isInvokedByGlobalInstallation;
|
|
delete configObject.commands;
|
|
delete configObject.isConfigurationResolved;
|
|
delete configObject.hasResolvedCommandsExternally;
|
|
delete configObject.isTelemetryReportedExternally;
|
|
delete configObject.options;
|
|
|
|
this.providers = {};
|
|
|
|
this.version = version;
|
|
|
|
this.yamlParser = new YamlParser(this);
|
|
this.utils = new Utils(this);
|
|
this.service = new Service(this);
|
|
this.variables = new Variables(this);
|
|
this.pluginManager = new PluginManager(this);
|
|
this.configSchemaHandler = new ConfigSchemaHandler(this);
|
|
|
|
this.config = new Config(this, configObject);
|
|
|
|
this.classes = {};
|
|
this.classes.CLI = CLI;
|
|
this.classes.YamlParser = YamlParser;
|
|
this.classes.Utils = Utils;
|
|
this.classes.Service = Service;
|
|
this.classes.Variables = Variables;
|
|
this.classes.Error = ServerlessError;
|
|
this.classes.PluginManager = PluginManager;
|
|
this.classes.ConfigSchemaHandler = ConfigSchemaHandler;
|
|
|
|
this.serverlessDirPath = path.join(os.homedir(), '.serverless');
|
|
this.isStandaloneExecutable = isStandaloneExecutable;
|
|
this.isLocallyInstalled = false;
|
|
this.triggeredDeprecations = logDeprecation.triggeredDeprecations;
|
|
|
|
// TODO: Remove once "@serverless/dashboard-plugin" is integrated into this repository
|
|
this._commandsSchema = commmandsSchema;
|
|
}
|
|
|
|
async init() {
|
|
if (!this._isInvokedByGlobalInstallation) {
|
|
if (this._shouldReportMissingServiceDeprecation) {
|
|
this._logDeprecation(
|
|
'MISSING_SERVICE_CONFIGURATION',
|
|
'Serverless constructor expects service configuration details to be provided.\n' +
|
|
'Starting from next major Serverless will no longer auto resolve it internally.'
|
|
);
|
|
}
|
|
if (this._shouldReportCommandsDeprecation) {
|
|
this._logDeprecation(
|
|
'MISSING_COMMANDS_OR_OPTIONS_AT_CONSTRUCTION',
|
|
'Serverless constructor expects resolved CLI commands and options to be provided ' +
|
|
'via "config.commands" and "config.options".\n' +
|
|
'Starting from next major Serverless will no longer auto resolve CLI arguments internally.'
|
|
);
|
|
}
|
|
}
|
|
if (this._shouldResolveConfigurationInternally) {
|
|
const configurationPath = await resolveConfigurationPath();
|
|
if (configurationPath) {
|
|
this.serviceDir = process.cwd();
|
|
this.configurationFilename = configurationPath.slice(this.serviceDir.length + 1);
|
|
}
|
|
}
|
|
if (this.configurationFilename && !this.configurationInput) {
|
|
this.configurationInput = await (async () => {
|
|
try {
|
|
return await readConfiguration(path.resolve(this.serviceDir, this.configurationFilename));
|
|
} catch (error) {
|
|
if (resolveCliInput().isHelpRequest) return null;
|
|
throw error;
|
|
}
|
|
})();
|
|
}
|
|
|
|
// create an instanceId (can be e.g. used when a predictable random value is needed)
|
|
this.instanceId = new Date().getTime().toString();
|
|
|
|
// create a new CLI instance
|
|
this.cli = new this.classes.CLI(this);
|
|
|
|
await this.eventuallyFallbackToLocal();
|
|
if (this.isOverridenByLocal) return;
|
|
eventuallyUpdate(this);
|
|
// set the options and commands which were processed by the CLI
|
|
this.pluginManager.setCliOptions(this.processedInput.options);
|
|
this.pluginManager.setCliCommands(this.processedInput.commands);
|
|
|
|
// TODO: Remove with next major
|
|
await this.loadEnvVariables();
|
|
|
|
await this.service.load(this.processedInput.options);
|
|
// load all plugins
|
|
await this.pluginManager.loadAllPlugins(this.service.plugins);
|
|
// give the CLI the plugins and commands so that it can print out
|
|
// information such as options when the user enters --help
|
|
this.cli.setLoadedPlugins(this.pluginManager.getPlugins());
|
|
this.cli.setLoadedCommands(this.pluginManager.getCommands());
|
|
await this.pluginManager.updateAutocompleteCacheFile();
|
|
}
|
|
async eventuallyFallbackToLocal() {
|
|
if (
|
|
this.configurationInput &&
|
|
this.configurationInput.enableLocalInstallationFallback != null
|
|
) {
|
|
this._logDeprecation(
|
|
'DISABLE_LOCAL_INSTALLATION_FALLBACK_SETTING',
|
|
'Starting with next major version, "enableLocalInstallationFallback" setting will no longer be supported.' +
|
|
'CLI will unconditionally fallback to service local installation when its found.\n' +
|
|
'Remove this setting to clear this deprecation warning'
|
|
);
|
|
}
|
|
if (this.isLocallyInstalled) return;
|
|
const localServerlessPath = await resolveLocalServerlessPath();
|
|
if (!localServerlessPath) return;
|
|
if (localServerlessPath === serverlessPath) {
|
|
this.isLocallyInstalled = true;
|
|
return;
|
|
}
|
|
if (
|
|
this.configurationInput &&
|
|
this.configurationInput.enableLocalInstallationFallback != null &&
|
|
!this.configurationInput.enableLocalInstallationFallback
|
|
) {
|
|
return;
|
|
}
|
|
this.cli.log('Running "serverless" installed locally (in service node_modules)');
|
|
// TODO: Replace below fallback logic with more straightforward one at top of the CLI
|
|
// when we willl drop support for the "disableLocalInstallationFallback" setting
|
|
this.isOverridenByLocal = true;
|
|
const ServerlessLocal = require(localServerlessPath);
|
|
const serverlessLocal = new ServerlessLocal({
|
|
serviceDir: this.serviceDir || null,
|
|
configurationFilename: this.configurationFilename,
|
|
configurationPath:
|
|
(this.configurationFilename && path.resolve(this.serviceDir, this.configurationFilename)) ||
|
|
null,
|
|
configuration: this.configurationInput,
|
|
isConfigurationResolved: this.isConfigurationInputResolved,
|
|
hasResolvedCommandsExternally: this.hasResolvedCommandsExternally,
|
|
isTelemetryReportedExternally: this.isTelemetryReportedExternally,
|
|
commands: this.processedInput.commands,
|
|
options: this.processedInput.options,
|
|
_isInvokedByGlobalInstallation: true,
|
|
});
|
|
serverlessLocal.isLocallyInstalled = true;
|
|
if (!serverlessLocal._isInvokedByGlobalInstallation) {
|
|
// if we fallback to older version it may recognize "isInvokedByGlobalInstallation" instead
|
|
// of "_isInvokedByGlobalInstallation". Ensure to set it, in such case
|
|
serverlessLocal.isInvokedByGlobalInstallation = true;
|
|
}
|
|
this.invokedInstance = serverlessLocal;
|
|
await serverlessLocal.init();
|
|
}
|
|
|
|
async loadEnvVariables() {
|
|
const configurationInput = this.configurationInput;
|
|
if (this.configurationInput == null) return;
|
|
await conditionallyLoadDotenv(this.processedInput.options, configurationInput);
|
|
}
|
|
|
|
async run() {
|
|
if (this._isInvokedByGlobalInstallation) {
|
|
// TODO: Remove with next major
|
|
// Ensure to have resolve-input populated with right result
|
|
const commandsSchema = require('./cli/commands-schema/resolve-final')(
|
|
this.pluginManager.externalPlugins,
|
|
{
|
|
providerName: this.service.provider.name,
|
|
configuration: this.configurationInput,
|
|
}
|
|
);
|
|
const resolveInput = require('./cli/resolve-input');
|
|
resolveInput.clear();
|
|
const { options, isHelpRequest } = resolveInput(commandsSchema);
|
|
if (options.version) {
|
|
require('./cli/render-version')();
|
|
return;
|
|
}
|
|
if (
|
|
!this.isConfigurationInputResolved &&
|
|
this.serviceDir &&
|
|
!_.get(this.configurationInput, 'provider.variableSyntax')
|
|
) {
|
|
// We're in a local fallback from other version which may not have a new variables engine
|
|
// (or have it incomplete). Therefore resolve variables with a new resolver
|
|
const resolveVariablesMeta = require('./configuration/variables/resolve-meta');
|
|
const isPropertyResolved = require('./configuration/variables/is-property-resolved');
|
|
const resolveVariables = require('./configuration/variables/resolve');
|
|
const eventuallyReportVariableResolutionErrors = require('./configuration/variables/eventually-report-resolution-errors');
|
|
const resolveProviderName = require('./configuration/resolve-provider-name');
|
|
const filterSupportedOptions = require('./cli/filter-supported-options');
|
|
|
|
const variablesMeta = resolveVariablesMeta(this.configurationInput);
|
|
// IIFE for maintanance convinience
|
|
await (async () => {
|
|
if (!variablesMeta.size) return;
|
|
const configurationPath = path.resolve(this.serviceDir, this.configurationFilename);
|
|
if (
|
|
eventuallyReportVariableResolutionErrors(
|
|
configurationPath,
|
|
this.configurationInput,
|
|
variablesMeta
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
for (const coreProperty of [
|
|
'app',
|
|
'disabledDeprecations',
|
|
'org',
|
|
'provider\0name',
|
|
'useDotenv',
|
|
'variablesResolutionMode',
|
|
]) {
|
|
if (!isPropertyResolved(variablesMeta, coreProperty)) return;
|
|
}
|
|
const providerName = resolveProviderName(this.configurationInput);
|
|
const resolverConfiguration = {
|
|
serviceDir: this.serviceDir,
|
|
configuration: this.configurationInput,
|
|
variablesMeta,
|
|
sources: {
|
|
env: require('./configuration/variables/sources/env'),
|
|
file: require('./configuration/variables/sources/file'),
|
|
opt: require('./configuration/variables/sources/opt'),
|
|
self: require('./configuration/variables/sources/self'),
|
|
strToBool: require('./configuration/variables/sources/str-to-bool'),
|
|
sls: require('./configuration/variables/sources/instance-dependent/get-sls')(this),
|
|
},
|
|
options: filterSupportedOptions(options, {
|
|
commandSchema: resolveInput.commandSchema,
|
|
providerName,
|
|
}),
|
|
propertyPathsToResolve: isHelpRequest ? new Set(['plugins']) : null,
|
|
};
|
|
if (providerName === 'aws') {
|
|
Object.assign(resolverConfiguration.sources, {
|
|
cf: require('./configuration/variables/sources/instance-dependent/get-cf')(this),
|
|
s3: require('./configuration/variables/sources/instance-dependent/get-s3')(this),
|
|
ssm: require('./configuration/variables/sources/instance-dependent/get-ssm')(this),
|
|
});
|
|
}
|
|
if (this.configurationInput.org && this.pluginManager.dashboardPlugin) {
|
|
for (const [sourceName, sourceConfig] of Object.entries(
|
|
this.pluginManager.dashboardPlugin.configurationVariablesSources
|
|
)) {
|
|
resolverConfiguration.sources[sourceName] = sourceConfig;
|
|
}
|
|
}
|
|
resolverConfiguration.fulfilledSources = new Set(
|
|
Object.keys(resolverConfiguration.sources)
|
|
);
|
|
if (!(this.configurationInput.variablesResolutionMode >= 20210326)) {
|
|
// New resolver, resolves just recognized CLI options. Therefore we cannot assume
|
|
// we have full "opt" source data if user didn't explicitly switch to new resolver
|
|
resolverConfiguration.fulfilledSources.delete('opt');
|
|
}
|
|
|
|
const resolverExternalPluginSources = require('../lib/configuration/variables/sources/resolve-external-plugin-sources');
|
|
resolverExternalPluginSources(
|
|
this.configurationInput,
|
|
resolverConfiguration,
|
|
this.pluginManager.externalPlugins
|
|
);
|
|
await resolveVariables(resolverConfiguration);
|
|
if (!variablesMeta.size) {
|
|
this.isConfigurationInputResolved = true;
|
|
return;
|
|
}
|
|
eventuallyReportVariableResolutionErrors(
|
|
configurationPath,
|
|
this.configurationInput,
|
|
variablesMeta
|
|
);
|
|
})();
|
|
}
|
|
}
|
|
|
|
// Ensure to pick eventual variable resolution that happens after all plugins are loaded
|
|
if (this.configurationInput) this.service.reloadServiceFileParam();
|
|
|
|
if (this.cli.displayHelp(this.processedInput)) {
|
|
return;
|
|
}
|
|
this.cli.suppressLogIfPrintCommand(this.processedInput);
|
|
|
|
// make sure the command exists before doing anything else
|
|
this.pluginManager.validateCommand(this.processedInput.commands);
|
|
|
|
if (!this.isConfigurationInputResolved) {
|
|
// populate variables after --help, otherwise help may fail to print
|
|
// (https://github.com/serverless/serverless/issues/2041)
|
|
await this.variables.populateService(this.pluginManager.cliOptions);
|
|
} else {
|
|
// Some plugins resolve additional variables on their own by runnning `variables.populateObject`
|
|
// e.g. https://github.com/serverless-operations/serverless-step-functions/blob/016da8db78f1972ba80d37941c34c8fd038fd8ca/lib/yamlParser.js#L26
|
|
// and that requires `variableSyntax` and `options` initizalization which is guaranteed by
|
|
// `variables.populateService`. Below lines ensure they're set
|
|
this.variables.loadVariableSyntax();
|
|
this.variables.options = this.pluginManager.cliOptions;
|
|
if (process.env.SLS_DEBUG) {
|
|
this.cli.log(
|
|
'Skipping variables resolution with old resolver ' +
|
|
'(new resolver reported no more variables to resolve)'
|
|
);
|
|
}
|
|
}
|
|
|
|
// merge arrays after variables have been populated
|
|
// (https://github.com/serverless/serverless/issues/3511)
|
|
this.service.mergeArrays();
|
|
|
|
// populate function names after variables are loaded in case functions were externalized
|
|
// (https://github.com/serverless/serverless/issues/2997)
|
|
this.service.setFunctionNames(this.processedInput.options);
|
|
|
|
// If in context of service, validate the service configuration
|
|
if (this.serviceDir) this.service.validate();
|
|
|
|
// trigger the plugin lifecycle when there's something which should be processed
|
|
await this.pluginManager.run(this.processedInput.commands);
|
|
}
|
|
|
|
setProvider(name, provider) {
|
|
this.providers[name] = provider;
|
|
}
|
|
|
|
getProvider(name) {
|
|
return this.providers[name] ? this.providers[name] : false;
|
|
}
|
|
|
|
getVersion() {
|
|
return this.version;
|
|
}
|
|
|
|
// Only for internal use
|
|
_logDeprecation(code, message) {
|
|
return logDeprecation(code, message, { serviceConfig: this.configurationInput });
|
|
}
|
|
|
|
// To be used by external plugins
|
|
logDeprecation(code, message) {
|
|
return this._logDeprecation(`EXT_${ensureString(code)}`, ensureString(message));
|
|
}
|
|
|
|
// If this instance is initialized by older version of the Framework,
|
|
// it may set "isInvokedByGlobalInstallation" directly.
|
|
// This fallback ensures it ends at "_isInvokedByGlobalInstallation"
|
|
set isInvokedByGlobalInstallation(value) {
|
|
this._isInvokedByGlobalInstallation = value;
|
|
}
|
|
get isInvokedByGlobalInstallation() {
|
|
return this._isInvokedByGlobalInstallation;
|
|
}
|
|
}
|
|
|
|
module.exports = Serverless;
|