osls
Version:
Open-source alternative to Serverless Framework
690 lines (611 loc) • 27.8 kB
JavaScript
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 } = require('@serverless/utils/log');
const processLog = log.get('process');
const handleError = require('../lib/cli/handle-error');
const logDeprecation = require('../lib/utils/log-deprecation');
let command;
let isHelpRequest;
let options;
let commandSchema;
let serviceDir = null;
let configuration = null;
let serverless;
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);
let hasBeenFinalized = false;
const finalize = async ({ error, shouldBeSync } = {}) => {
processLog.debug('finalize %o', { error, shouldBeSync });
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) handleError(error, { serverless });
if (!shouldBeSync) {
await logDeprecation.printSummary();
}
return null;
};
process.once('uncaughtException', (error) => {
log.error('Uncaught exception');
finalize({ error }).then(() => process.exit());
});
(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,
});
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');
(() => {
// 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;
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');
let configurationPath = null;
let providerName;
let variablesMeta;
let resolverConfiguration;
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')
));
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 () => {
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,
};
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('service')) return;
})();
// 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', '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({});
return;
}
}
processLog.debug('construct Serverless instance');
serverless = new Serverless({
configuration,
serviceDir,
configurationFilename,
commands,
options,
variablesMeta,
});
try {
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')(),
param: require('../lib/configuration/variables/sources/instance-dependent/param')(),
},
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 =
require('../lib/configuration/variables/sources/instance-dependent/param')(serverless);
resolverConfiguration.fulfilledSources.add('param');
// 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)
);
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 {
processLog.debug('run Serverless instance');
// Run command
await serverless.run();
}
await finalize({});
} catch (error) {
processLog.debug('handle error');
throw error;
}
} catch (error) {
await finalize({ error });
}
})();
;