mirror of
https://github.com/serverless/serverless.git
synced 2025-12-08 19:46:03 +00:00
* fix: do not create an integration if observability property is not set * fix: only create integration if observability enabled * chores: add comments * chores: update comments
354 lines
12 KiB
JavaScript
354 lines
12 KiB
JavaScript
import path from 'path'
|
|
import os from 'os'
|
|
import _ from 'lodash'
|
|
import ensureString from 'type/string/ensure.js'
|
|
import ensureValue from 'type/value/ensure.js'
|
|
import ensureArray from 'type/array/ensure.js'
|
|
import ensureIterable from 'type/iterable/ensure.js'
|
|
import ensurePlainObject from 'type/plain-object/ensure.js'
|
|
import CLI from './classes/cli.js'
|
|
import Config from './classes/config.js'
|
|
import YamlParser from './classes/yaml-parser.js'
|
|
import PluginManager from './classes/plugin-manager.js'
|
|
import Utils from './classes/utils.js'
|
|
import Service from './classes/service.js'
|
|
import ConfigSchemaHandler from './classes/config-schema-handler/index.js'
|
|
import ServerlessError from './serverless-error.js'
|
|
import logDeprecation from './utils/log-deprecation.js'
|
|
import commmandsSchema from './cli/commands-schema.js'
|
|
import utils from '@serverlessinc/sf-core/src/utils.js'
|
|
|
|
const { log, progress } = utils
|
|
const logger = log.get('sls:lib:serverless')
|
|
|
|
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.version - The version of 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.
|
|
* @throws {ServerlessError} If there's an error in validation or incompatible configurations.
|
|
*/
|
|
constructor({
|
|
version = null,
|
|
orgId = null,
|
|
orgName = null,
|
|
accessKey = null,
|
|
commands,
|
|
options,
|
|
servicePath = null,
|
|
serviceConfigFileName = null,
|
|
service = {},
|
|
credentialProviders = {},
|
|
} = {}) {
|
|
logger.debug(options)
|
|
|
|
this.providers = {}
|
|
this.version = version
|
|
this.orgId = orgId || null
|
|
this.orgName = orgName || null
|
|
this.accessKey = accessKey || null
|
|
this.credentialProviders = credentialProviders
|
|
|
|
/**
|
|
* 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, {
|
|
name: 'configurationFilename',
|
|
Error: ServerlessError,
|
|
errorCode: 'INVALID_NON_STRING_CONFIGURATION_FILENAME',
|
|
})
|
|
if (path.isAbsolute(this.configurationFilename)) {
|
|
throw new ServerlessError(
|
|
`"configurationFilename" cannot be absolute path. Received: ${this.configurationFilename}`,
|
|
'INVALID_ABSOLUTE_PATH_CONFIGURATION_FILENAME',
|
|
)
|
|
}
|
|
// Store the original Service Configuration
|
|
this.configurationInput = ensurePlainObject(service, {
|
|
name: 'configuration',
|
|
Error: ServerlessError,
|
|
errorCode: 'INVALID_NON_OBJECT_CONFIGURATION',
|
|
})
|
|
}
|
|
commands = ensureArray(commands)
|
|
// Ensure that original `options` are not mutated, can be removed after addressing:
|
|
// https://github.com/serverless/serverless/issues/2582
|
|
const cliOptions = { ...ensurePlainObject(options) }
|
|
this.processedInput = { commands, options: cliOptions }
|
|
this.yamlParser = new YamlParser(this)
|
|
this.utils = new Utils(this)
|
|
this.service = new Service(this)
|
|
// Old variables resolver is dropped, yet some plugins access service properties through
|
|
// `variables` class. Below patch ensures those plugins won't get broken
|
|
this.variables = { service: this.service }
|
|
this.pluginManager = new PluginManager(this)
|
|
this.configSchemaHandler = new ConfigSchemaHandler(this)
|
|
this.config = new Config(this, { serviceDir: this.serviceDir })
|
|
|
|
this.classes = {}
|
|
this.classes.CLI = CLI
|
|
this.classes.YamlParser = YamlParser
|
|
this.classes.Utils = Utils
|
|
this.classes.Service = Service
|
|
this.classes.Error = ServerlessError
|
|
this.classes.PluginManager = PluginManager
|
|
this.classes.ConfigSchemaHandler = ConfigSchemaHandler
|
|
|
|
this.serverlessDirPath = path.join(os.homedir(), '.serverless')
|
|
this.triggeredDeprecations = logDeprecation.triggeredDeprecations
|
|
this.isConfigurationExtendable = true
|
|
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() {
|
|
logger.debug('initializing')
|
|
// 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)
|
|
|
|
// set the options and commands which were processed by the CLI
|
|
this.pluginManager.setCliOptions(this.processedInput.options)
|
|
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
|
|
|
|
// 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())
|
|
}
|
|
|
|
/**
|
|
* 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()
|
|
|
|
// make sure the command exists before doing anything else
|
|
this.pluginManager.validateCommand(this.processedInput.commands)
|
|
|
|
// Some plugins acccess `options` through `this.variables`
|
|
this.variables.options = this.pluginManager.cliOptions
|
|
|
|
if (this.processedInput.commands[0] !== 'plugin') {
|
|
// 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) await this.service.validate()
|
|
}
|
|
|
|
this.serviceOutputs = new Map()
|
|
this.servicePluginOutputs = new Map()
|
|
|
|
/**
|
|
* Remove the progress spinner.
|
|
* Leave spinner responsibility to commands.
|
|
* Hide the spinner here so that no blank space is shown,
|
|
* between Framework init and a plugin using the spinner.
|
|
*/
|
|
const progressMain = progress.get('main')
|
|
progressMain.remove()
|
|
|
|
// trigger the plugin lifecycle when there's something which should be processed
|
|
await this.pluginManager.run(this.processedInput.commands)
|
|
}
|
|
|
|
/**
|
|
* Checks if observability is configured, which may or may not be enabled
|
|
* @returns boolean
|
|
*/
|
|
isObservabilityConfigured() {
|
|
const stageName = this.getProvider('aws').getStage()
|
|
|
|
let observabilityConfigured = false
|
|
|
|
if (
|
|
this.configurationInput.stages &&
|
|
(typeof this.configurationInput.stages?.default?.observability ===
|
|
'boolean' ||
|
|
typeof this.configurationInput.stages[stageName]?.observability ===
|
|
'boolean')
|
|
) {
|
|
observabilityConfigured = true
|
|
|
|
if (!Boolean(this.configurationInput.app)) {
|
|
throw new ServerlessError(
|
|
'To instrument your service, you must set the "app" property in your config file.',
|
|
'OBSERVABILITY_APP_NOT_SET',
|
|
)
|
|
}
|
|
}
|
|
|
|
return observabilityConfigured
|
|
}
|
|
|
|
/**
|
|
* Checks if observability is configured AND enabled
|
|
* @returns boolean
|
|
*/
|
|
isObservabilityEnabled() {
|
|
const stageName = this.getProvider('aws').getStage()
|
|
|
|
let observabilityEnabled = false
|
|
|
|
if (
|
|
typeof this.configurationInput.stages?.default?.observability ===
|
|
'boolean'
|
|
) {
|
|
observabilityEnabled =
|
|
this.configurationInput.stages?.default?.observability
|
|
}
|
|
|
|
if (
|
|
this.configurationInput.stages &&
|
|
typeof this.configurationInput.stages[stageName]?.observability ===
|
|
'boolean'
|
|
) {
|
|
observabilityEnabled =
|
|
this.configurationInput.stages[stageName]?.observability
|
|
}
|
|
|
|
if (observabilityEnabled && !Boolean(this.configurationInput.app)) {
|
|
throw new ServerlessError(
|
|
'To instrument your service, you must set the "app" property in your config file.',
|
|
'OBSERVABILITY_APP_NOT_SET',
|
|
)
|
|
}
|
|
|
|
return observabilityEnabled
|
|
}
|
|
|
|
addServiceOutputSection(sectionName, content) {
|
|
sectionName = ensureString(sectionName, { name: 'sectionName' })
|
|
if (typeof ensureValue(content, { name: 'content' }) !== 'string') {
|
|
content = ensureIterable(content, {
|
|
name: 'content',
|
|
denyEmpty: true,
|
|
ensureItem: ensureString,
|
|
})
|
|
} else if (!content) {
|
|
throw new TypeError('Section content cannot be empty string')
|
|
}
|
|
if (
|
|
this.serviceOutputs.has(sectionName) ||
|
|
this.servicePluginOutputs.has(sectionName)
|
|
) {
|
|
throw new TypeError(
|
|
`Section content for "${sectionName}" was already set`,
|
|
)
|
|
}
|
|
this.servicePluginOutputs.set(sectionName, content)
|
|
}
|
|
|
|
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),
|
|
)
|
|
}
|
|
|
|
extendConfiguration(configurationPathKeys, value) {
|
|
configurationPathKeys = ensureArray(configurationPathKeys, {
|
|
ensureItem: ensureString,
|
|
})
|
|
if (configurationPathKeys.length < 1) {
|
|
throw new Error(
|
|
'Cannot extend configuration: ConfigurationPathKeys needs to contain at least one element.',
|
|
)
|
|
}
|
|
|
|
if (!this.isConfigurationExtendable) {
|
|
throw new Error(
|
|
'Cannot extend configuration: It can only be extended during initialization phase.',
|
|
)
|
|
}
|
|
try {
|
|
value = JSON.parse(JSON.stringify(value))
|
|
} catch (error) {
|
|
throw new Error(
|
|
`Cannot extend configuration: Received non JSON value: ${value}`,
|
|
)
|
|
}
|
|
|
|
_.set(this.configurationInput, configurationPathKeys, value)
|
|
if (!_.isObject(value)) {
|
|
const lastKey = configurationPathKeys.pop()
|
|
value = { [lastKey]: value }
|
|
}
|
|
}
|
|
}
|
|
|
|
export default Serverless
|