mirror of
https://github.com/serverless/serverless.git
synced 2025-12-08 19:46:03 +00:00
351 lines
11 KiB
JavaScript
351 lines
11 KiB
JavaScript
import ServerlessError from '../serverless-error.js'
|
|
import util from 'util'
|
|
import _ from 'lodash'
|
|
import semver from 'semver'
|
|
import utils from '@serverlessinc/sf-core/src/utils.js'
|
|
|
|
const { log } = utils
|
|
|
|
class Service {
|
|
constructor(serverless, data) {
|
|
// #######################################################################
|
|
// ## KEEP SYNCHRONIZED WITH EQUIVALENT IN ~/lib/plugins/print/print.js ##
|
|
// #######################################################################
|
|
this.serverless = serverless
|
|
|
|
// Default properties
|
|
this.service = null
|
|
this.serviceObject = null
|
|
this.provider = {
|
|
stage: 'dev',
|
|
}
|
|
this.custom = {}
|
|
this.plugins = []
|
|
this.pluginsData = {}
|
|
this.functions = {}
|
|
this.resources = {}
|
|
this.package = {}
|
|
this.configValidationMode = 'warn'
|
|
this.disabledDeprecations = []
|
|
|
|
if (data) this.update(data)
|
|
}
|
|
|
|
async load(rawOptions) {
|
|
const options = rawOptions || {}
|
|
if (!options.stage && options.s) options.stage = options.s
|
|
if (!options.region && options.r) options.region = options.r
|
|
const serviceDir = this.serverless.serviceDir
|
|
|
|
// skip if the service path is not found
|
|
// because the user might be creating a new service
|
|
if (!serviceDir) return
|
|
|
|
this.loadServiceFileParam()
|
|
}
|
|
|
|
loadServiceFileParam() {
|
|
// Not used internally, left set to not break plugins which depend on it
|
|
// TOOD: Remove with next major
|
|
this.serviceFilename = this.serverless.configurationFilename
|
|
|
|
const configurationInput = this.serverless.configurationInput
|
|
|
|
this.initialServerlessConfig = configurationInput
|
|
|
|
// TODO: Ideally below approach should be replaced at some point with:
|
|
// 1. In "initialization" phase: Minimize reliance on service configuration
|
|
// to few core properties
|
|
// 2. Before "run" phase: Validate and normalize (via AJV) `serverless.configurationInput`
|
|
// into `serverless.configuration`
|
|
// 3. In "run" phase: Instead of relying on `serverless.service` rely on
|
|
// `serverless.configuration` internally
|
|
|
|
// Below comments provide usage feedback helfpul for future refactor process.
|
|
|
|
// ## Properties currently accessed at "initialization" phase
|
|
|
|
this.disabledDeprecations = configurationInput.disabledDeprecations
|
|
this.deprecationNotificationMode =
|
|
configurationInput.deprecationNotificationMode
|
|
|
|
if (!_.isObject(configurationInput.provider)) {
|
|
const providerName = configurationInput.provider
|
|
this.provider = {
|
|
name: providerName,
|
|
}
|
|
} else {
|
|
this.provider = configurationInput.provider
|
|
}
|
|
if (this.provider.stage == null) {
|
|
this.provider.stage = 'dev'
|
|
}
|
|
|
|
if (_.isObject(configurationInput.service)) {
|
|
throw new ServerlessError(
|
|
'Object notation for "service" property is not supported. Set "service" property directly with service name.',
|
|
'SERVICE_OBJECT_NOTATION',
|
|
)
|
|
} else {
|
|
this.serviceObject = { name: configurationInput.service }
|
|
this.service = configurationInput.service
|
|
}
|
|
|
|
this.app = configurationInput.app
|
|
this.appId = configurationInput.appId || null
|
|
this.org = configurationInput.org
|
|
this.orgId = configurationInput.orgId || null
|
|
|
|
this.plugins = configurationInput.plugins
|
|
this.build = configurationInput.build
|
|
|
|
// `package.path` is read by few core plugins at initialization
|
|
if (configurationInput.package) {
|
|
this.package = configurationInput.package
|
|
}
|
|
|
|
// ## Properties accessed at "run" phase
|
|
this.custom = configurationInput.custom
|
|
this.resources = configurationInput.resources
|
|
this.functions = configurationInput.functions || {}
|
|
this.configValidationMode =
|
|
configurationInput.configValidationMode || 'warn'
|
|
if (this.provider.name === 'aws') {
|
|
this.layers = configurationInput.layers || {}
|
|
}
|
|
this.outputs = configurationInput.outputs
|
|
|
|
// basic service level validation
|
|
const version = this.serverless.utils.getVersion()
|
|
let ymlVersion = configurationInput.frameworkVersion
|
|
if (ymlVersion && !semver.validRange(ymlVersion)) {
|
|
if (configurationInput.configValidationMode === 'error') {
|
|
throw new ServerlessError(
|
|
'Configured "frameworkVersion" does not represent a valid semver version range.',
|
|
'INVALID_FRAMEWORK_VERSION',
|
|
)
|
|
}
|
|
log.warning(
|
|
'Configured "frameworkVersion" does not represent a valid semver version range, version validation is skipped',
|
|
)
|
|
ymlVersion = null
|
|
}
|
|
|
|
if (
|
|
ymlVersion &&
|
|
version &&
|
|
version !== ymlVersion &&
|
|
semver.coerce(version).major !== semver.coerce(ymlVersion).major
|
|
) {
|
|
const errorMessage = [
|
|
`The Serverless version (${version}) does not satisfy the`,
|
|
` "frameworkVersion" (${ymlVersion}) in ${this.serverless.configurationFilename}`,
|
|
].join('')
|
|
throw new ServerlessError(errorMessage, 'FRAMEWORK_VERSION_MISMATCH')
|
|
}
|
|
|
|
if (!configurationInput.service) {
|
|
throw new ServerlessError(
|
|
`"service" property is missing in ${this.serverless.configurationFilename}`,
|
|
'SERVICE_NAME_MISSING',
|
|
)
|
|
}
|
|
if (!configurationInput.provider) {
|
|
throw new ServerlessError(
|
|
`"provider" property is missing in ${this.serverless.configurationFilename}`,
|
|
'PROVIDER_NAME_MISSING',
|
|
)
|
|
}
|
|
if (!_.isObject(configurationInput.provider)) {
|
|
// Schema uncoditionally expects `provider` to be an object.
|
|
// Ideally if it's fixed at some point, and either we do not support string notation for
|
|
// provider, or we support string by schema
|
|
configurationInput.provider = this.provider
|
|
}
|
|
|
|
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)) {
|
|
this.provider = configurationInput.provider
|
|
if (this.provider.stage == null) this.provider.stage = 'dev'
|
|
}
|
|
if (configurationInput.package) {
|
|
this.package = configurationInput.package
|
|
}
|
|
this.custom = configurationInput.custom
|
|
this.resources = configurationInput.resources
|
|
this.functions = configurationInput.functions || {}
|
|
this.configValidationMode =
|
|
configurationInput.configValidationMode || 'warn'
|
|
this.layers = configurationInput.layers || {}
|
|
this.outputs = configurationInput.outputs
|
|
}
|
|
|
|
setFunctionNames(rawOptions) {
|
|
const options = rawOptions || {}
|
|
options.stage = options.stage || options.s
|
|
options.region = options.region || options.r
|
|
|
|
// Ensure that function is an object and setup function.name property
|
|
const stageNameForFunction = options.stage || this.provider.stage
|
|
|
|
if (!_.isObject(this.functions)) {
|
|
throw new ServerlessError(
|
|
`Unexpected "functions" configuration: Expected object received: ${util.inspect(
|
|
this.functions,
|
|
)}`,
|
|
'NON_OBJECT_FUNCTIONS_CONFIGURATION',
|
|
)
|
|
}
|
|
Object.entries(this.functions).forEach(([functionName, functionObj]) => {
|
|
if (functionObj == null) {
|
|
delete this.functions[functionName]
|
|
return
|
|
}
|
|
if (!_.isObject(functionObj)) {
|
|
throw new ServerlessError(
|
|
`Unexpected "${functionName}" function configuration: Expected object received ${util.inspect(
|
|
functionObj,
|
|
)})`,
|
|
'NON_OBJECT_FUNCTION_CONFIGURATION_ERROR',
|
|
)
|
|
}
|
|
if (!functionObj.events) {
|
|
this.functions[functionName].events = []
|
|
}
|
|
|
|
if (!functionObj.name) {
|
|
this.functions[functionName].name =
|
|
`${this.service}-${stageNameForFunction}-${functionName}`
|
|
}
|
|
})
|
|
}
|
|
|
|
mergeArrays() {
|
|
;['resources', 'functions'].forEach((key) => {
|
|
if (Array.isArray(this[key])) {
|
|
this[key] = this[key].reduce((memo, value) => {
|
|
if (value) {
|
|
if (typeof value === 'object') {
|
|
return _.merge(memo, value)
|
|
}
|
|
throw new ServerlessError(
|
|
`Non-object value specified in ${key} array: ${value}`,
|
|
'LEGACY_CONFIGURATION_PROPERTY_MERGE_INVALID_INPUT',
|
|
)
|
|
}
|
|
|
|
return memo
|
|
}, {})
|
|
}
|
|
})
|
|
}
|
|
|
|
async validate() {
|
|
const userConfig = this.initialServerlessConfig
|
|
|
|
// Ensure to validate normalized (after mergeArrays) input
|
|
if (userConfig.functions) userConfig.functions = this.functions
|
|
if (userConfig.resources) userConfig.resources = this.resources
|
|
|
|
await this.serverless.configSchemaHandler.validateConfig(userConfig)
|
|
|
|
if (userConfig.projectDir != null) {
|
|
this.serverless._logDeprecation(
|
|
'PROJECT_DIR',
|
|
'The "projectDir" option is no longer used and is ignored. ' +
|
|
'You can safely remove it from the configuration',
|
|
)
|
|
}
|
|
if (userConfig.console != null) {
|
|
this.serverless._logDeprecation(
|
|
'CONSOLE_CONFIGURATION',
|
|
'Starting with v3.24.0, the "console" option is no longer recognized. ' +
|
|
'Please remove it from the configuration',
|
|
)
|
|
}
|
|
return this
|
|
}
|
|
|
|
update(data) {
|
|
return _.merge(this, data)
|
|
}
|
|
|
|
getServiceName() {
|
|
return this.serviceObject.name
|
|
}
|
|
|
|
getServiceObject() {
|
|
return this.serviceObject
|
|
}
|
|
|
|
getAllFunctions() {
|
|
return Object.keys(this.functions)
|
|
}
|
|
|
|
getAllLayers() {
|
|
return this.layers ? Object.keys(this.layers) : []
|
|
}
|
|
|
|
getAllFunctionsNames() {
|
|
return this.getAllFunctions().map((func) => this.getFunction(func).name)
|
|
}
|
|
|
|
getFunction(functionName) {
|
|
if (functionName in this.functions) {
|
|
return this.functions[functionName]
|
|
}
|
|
throw new ServerlessError(
|
|
`Function "${functionName}" doesn't exist in this Service`,
|
|
'FUNCTION_MISSING_IN_SERVICE',
|
|
)
|
|
}
|
|
|
|
getLayer(layerName) {
|
|
if (layerName in this.layers) {
|
|
return this.layers[layerName]
|
|
}
|
|
throw new ServerlessError(
|
|
`Layer "${layerName}" doesn't exist in this Service`,
|
|
'LAYER_MISSING_IN_SERVICE',
|
|
)
|
|
}
|
|
|
|
getEventInFunction(eventName, functionName) {
|
|
const event = this.getFunction(functionName).events.find(
|
|
(e) => Object.keys(e)[0] === eventName,
|
|
)
|
|
if (event) {
|
|
return event
|
|
}
|
|
throw new ServerlessError(
|
|
`Event "${eventName}" doesn't exist in function "${functionName}"`,
|
|
'EVENT_MISSING_FOR_FUNCTION',
|
|
)
|
|
}
|
|
|
|
getAllEventsInFunction(functionName) {
|
|
return this.getFunction(functionName).events
|
|
}
|
|
|
|
publish(dataParam) {
|
|
const data = dataParam || {}
|
|
this.pluginsData = _.merge(this.pluginsData, data)
|
|
}
|
|
}
|
|
|
|
export default Service
|