mirror of
https://github.com/serverless/serverless.git
synced 2025-12-08 19:46:03 +00:00
332 lines
12 KiB
JavaScript
332 lines
12 KiB
JavaScript
'use strict';
|
|
|
|
const SError = require('./Error').SError;
|
|
const path = require('path');
|
|
const _ = require('lodash');
|
|
const traverse = require('traverse');
|
|
const replaceall = require('replaceall');
|
|
const BbPromise = require('bluebird');
|
|
|
|
class Service {
|
|
|
|
constructor(serverless, data) {
|
|
this.serverless = serverless;
|
|
|
|
// Default properties
|
|
this.service = null;
|
|
this.provider = null;
|
|
this.defaults = {
|
|
stage: 'dev',
|
|
region: 'us-east-1',
|
|
};
|
|
this.variableSyntax = null;
|
|
this.custom = {};
|
|
this.plugins = [];
|
|
this.functions = {};
|
|
this.environment = {};
|
|
this.resources = {};
|
|
|
|
if (data) this.update(data);
|
|
}
|
|
|
|
load(opts) {
|
|
const that = this;
|
|
const options = opts || {};
|
|
const servicePath = that.serverless.config.servicePath;
|
|
|
|
// skip if the service path is not found
|
|
// because the user might be creating a new service
|
|
if (!servicePath) {
|
|
return BbPromise.resolve();
|
|
}
|
|
|
|
return that.serverless.yamlParser
|
|
.parse(path.join(servicePath, 'serverless.yaml'))
|
|
.then((serverlessYaml) => {
|
|
// basic service level validation
|
|
if (!serverlessYaml.service) {
|
|
throw new SError('"service" property is missing in serverless.yaml');
|
|
}
|
|
if (!serverlessYaml.provider) {
|
|
throw new SError('"provider" property is missing in serverless.yaml');
|
|
}
|
|
if (!serverlessYaml.functions) {
|
|
throw new SError('"functions" property is missing in serverless.yaml');
|
|
}
|
|
|
|
if (['aws', 'azure', 'google', 'ibm'].indexOf(serverlessYaml.provider)) {
|
|
const errorMessage = [
|
|
`Provider "${serverlessYaml.provider}" is not supported.`,
|
|
' Valid values for provider are: aws, azure, google, ibm.',
|
|
' Please provide one of those values to the "provider" property in serverless.yaml.',
|
|
].join('');
|
|
throw new SError(errorMessage);
|
|
}
|
|
|
|
that.service = serverlessYaml.service;
|
|
that.provider = serverlessYaml.provider;
|
|
that.variableSyntax = serverlessYaml.variableSyntax;
|
|
that.custom = serverlessYaml.custom;
|
|
that.plugins = serverlessYaml.plugins;
|
|
that.resources = serverlessYaml.resources;
|
|
that.functions = serverlessYaml.functions;
|
|
|
|
if (serverlessYaml.defaults && serverlessYaml.defaults.stage) {
|
|
this.defaults.stage = serverlessYaml.defaults.stage;
|
|
}
|
|
if (serverlessYaml.defaults && serverlessYaml.defaults.region) {
|
|
this.defaults.region = serverlessYaml.defaults.region;
|
|
}
|
|
})
|
|
.then(() => that.serverless.yamlParser
|
|
.parse(path.join(servicePath, 'serverless.env.yaml')))
|
|
.then((serverlessEnvYamlParam) => {
|
|
const serverlessEnvYaml = serverlessEnvYamlParam;
|
|
|
|
// safely load serverless.env.yaml while avoiding
|
|
// reference errors
|
|
serverlessEnvYaml.vars = serverlessEnvYaml.vars || {};
|
|
serverlessEnvYaml.stages = serverlessEnvYaml.stages || {};
|
|
Object.keys(serverlessEnvYaml.stages).forEach(stage => {
|
|
serverlessEnvYaml.stages[stage] = serverlessEnvYaml.stages[stage] || {};
|
|
serverlessEnvYaml.stages[stage].vars = serverlessEnvYaml.stages[stage].vars || {};
|
|
serverlessEnvYaml.stages[stage].regions = serverlessEnvYaml.stages[stage].regions || {};
|
|
Object.keys(serverlessEnvYaml.stages[stage].regions).forEach(region => {
|
|
serverlessEnvYaml.stages[stage].regions[region] =
|
|
serverlessEnvYaml.stages[stage].regions[region] || {};
|
|
serverlessEnvYaml.stages[stage].regions[region].vars =
|
|
serverlessEnvYaml.stages[stage].regions[region].vars || {};
|
|
});
|
|
});
|
|
|
|
that.environment = serverlessEnvYaml;
|
|
|
|
return BbPromise.resolve(that);
|
|
})
|
|
.then(() => {
|
|
if (!options.stage) {
|
|
options.stage = this.defaults.stage;
|
|
}
|
|
|
|
if (!options.region) {
|
|
options.region = this.defaults.region;
|
|
}
|
|
|
|
// Validate: Check stage exists
|
|
this.getStage(options.stage);
|
|
|
|
// Validate: Check region exists in stage
|
|
this.getRegionInStage(options.stage, options.region);
|
|
|
|
let varTemplateSyntax = /\${([\s\S]+?)}/g;
|
|
|
|
if (this.variableSyntax) {
|
|
varTemplateSyntax = RegExp(this.variableSyntax, 'g');
|
|
|
|
// temporally remove variable syntax from service otherwise it'll match
|
|
this.variableSyntax = true;
|
|
}
|
|
|
|
const commonVars = this.getVariables();
|
|
const stageVars = this.getVariables(options.stage);
|
|
const regionVars = this.getVariables(options.stage, options.region);
|
|
|
|
// temporally remove environment obj. Doesn't make sense to
|
|
// populate environment (stages, regions, vars)
|
|
const environment = _.cloneDeep(this.environment);
|
|
this.environment = null;
|
|
|
|
/*
|
|
* we can't use an arrow function in this case cause that would
|
|
* change the lexical scoping required by the traverse module
|
|
*/
|
|
traverse(this).forEach(function (valParam) {
|
|
const t = this;
|
|
let val = valParam;
|
|
|
|
// check if the current string is a variable
|
|
if (typeof(val) === 'string' && val.match(varTemplateSyntax)) {
|
|
// get all ${variable} in the string
|
|
val.match(varTemplateSyntax).forEach((variableSyntax) => {
|
|
const variableString = variableSyntax
|
|
.replace(varTemplateSyntax, (match, varName) => varName.trim());
|
|
|
|
const variableName = (variableString
|
|
.split('.').length > 1) ? variableString
|
|
.split('.')[0] : variableString;
|
|
|
|
let value;
|
|
|
|
/*
|
|
* we will manipulate the value later
|
|
* so we gotta clone otherwise we will
|
|
* corrupt the passed-by-reference variables object
|
|
*/
|
|
if (variableName in commonVars) {
|
|
value = _.cloneDeep(commonVars[variableName]);
|
|
}
|
|
|
|
if (variableName in stageVars) {
|
|
value = _.cloneDeep(stageVars[variableName]);
|
|
}
|
|
|
|
if (variableName in regionVars) {
|
|
value = _.cloneDeep(regionVars[variableName]);
|
|
}
|
|
|
|
// Populate
|
|
if (!value && !value !== '') {
|
|
const errorMessage = [
|
|
`Variable "${variableName}" doesn't exist in serverless.env.yaml.`,
|
|
' Please add it to serverless.env.yaml.',
|
|
].join('');
|
|
throw new that.serverless.classes
|
|
.Error(errorMessage);
|
|
} else if (typeof value === 'string') {
|
|
if (variableString.split('.').length > 1) {
|
|
const errorMessage = [
|
|
`Trying to access sub properties of a string variable "${variableName}".`,
|
|
' Please make sure the variable in serverless.env.yaml',
|
|
' is an object, otherwise you cannot use the',
|
|
' dot notation for that variable in serverless.yaml',
|
|
].join('');
|
|
throw new that.serverless.classes
|
|
.Error(errorMessage);
|
|
}
|
|
// for string variables, we use replaceall in case the user
|
|
// includes the variable as a substring (ie. "hello ${name}")
|
|
val = replaceall(variableSyntax, value, val);
|
|
} else {
|
|
// populate objects recursively
|
|
if (typeof value === 'object') {
|
|
const subProperties = variableString.split('.');
|
|
// remove first element. It's the variableName
|
|
subProperties.splice(0, 1);
|
|
subProperties.forEach(subProperty => {
|
|
if (!value[subProperty]) {
|
|
const errorMessage = [
|
|
`Variable "${variableName}" doesn't have sub property "${subProperty}".`,
|
|
' Please make sure the variable is',
|
|
' the intended object in serverless.env.yaml,',
|
|
' or reference the correct sub property in serverless.yaml',
|
|
].join('');
|
|
throw new that.serverless.classes
|
|
.Error(errorMessage);
|
|
}
|
|
value = value[subProperty];
|
|
});
|
|
|
|
if (typeof value === 'string') {
|
|
val = replaceall(variableSyntax, value, val);
|
|
} else {
|
|
if (val !== variableSyntax) {
|
|
const errorMessage = [
|
|
'Trying to populate non string variables into',
|
|
` a string for variable "${variableName}".`,
|
|
' Please make sure the variable value in',
|
|
' serverless.env.yaml is a string',
|
|
].join('');
|
|
throw new that.serverless.classes
|
|
.Error(errorMessage);
|
|
}
|
|
val = value;
|
|
}
|
|
} else if (variableString.split('.').length > 1) {
|
|
const errorMessage = [
|
|
`Trying to access sub properties of a non-object variable "${variableName}"`,
|
|
' Please make sure the variable is an object in serverless.env.yaml,',
|
|
' otherwise, you cannot use the dot notation',
|
|
' for that variable in serverless.yaml',
|
|
].join('');
|
|
throw new that.serverless.classes
|
|
.Error(errorMessage);
|
|
} else {
|
|
if (val !== variableSyntax) {
|
|
const errorMessage = [
|
|
'Trying to populate non string variables',
|
|
` into a string for variable "${variableName}".`,
|
|
' Please make sure the variable value in serverless.env.yaml is a string',
|
|
].join('');
|
|
throw new that.serverless.classes
|
|
.Error(errorMessage);
|
|
}
|
|
val = value; // not string nor object
|
|
}
|
|
}
|
|
});
|
|
|
|
// Replace
|
|
t.update(val);
|
|
}
|
|
});
|
|
|
|
// put back environment that we temporally removed earlier
|
|
this.environment = environment;
|
|
|
|
// put back variable syntax if we removed it for processing
|
|
if (this.variableSyntax) this.variableSyntax = varTemplateSyntax;
|
|
|
|
return this;
|
|
});
|
|
}
|
|
|
|
update(data) {
|
|
return _.merge(this, data);
|
|
}
|
|
|
|
getAllFunctions() {
|
|
return Object.keys(this.functions);
|
|
}
|
|
|
|
getFunction(functionName) {
|
|
if (functionName in this.functions) {
|
|
return this.functions[functionName];
|
|
}
|
|
throw new SError(`Function "${functionName}" doesn't exist in this Service`);
|
|
}
|
|
|
|
getEventInFunction(eventName, functionName) {
|
|
if (eventName in this.getFunction(functionName).events) {
|
|
return this.getFunction(functionName).events[eventName];
|
|
}
|
|
throw new SError(`Event "${eventName}" doesn't exist in function "${functionName}"`);
|
|
}
|
|
|
|
getAllEventsInFunction(functionName) {
|
|
return Object.keys(this.getFunction(functionName).events);
|
|
}
|
|
|
|
getStage(stageName) {
|
|
if (stageName in this.environment.stages) {
|
|
return this.environment.stages[stageName];
|
|
}
|
|
throw new SError(`Stage "${stageName}" doesn't exist in this service.`);
|
|
}
|
|
|
|
getAllStages() {
|
|
return Object.keys(this.environment.stages);
|
|
}
|
|
|
|
getRegionInStage(stageName, regionName) {
|
|
if (regionName in this.getStage(stageName).regions) {
|
|
return this.getStage(stageName).regions[regionName];
|
|
}
|
|
throw new SError(`Region "${regionName}" doesn't exist in stage "${stageName}"`);
|
|
}
|
|
|
|
getAllRegionsInStage(stageName) {
|
|
return Object.keys(this.getStage(stageName).regions);
|
|
}
|
|
|
|
getVariables(stageName, regionName) {
|
|
if (stageName && regionName) {
|
|
return this.getRegionInStage(stageName, regionName).vars || {};
|
|
} else if (stageName) {
|
|
return this.getStage(stageName).vars || {};
|
|
}
|
|
return this.environment.vars || {};
|
|
}
|
|
}
|
|
|
|
module.exports = Service;
|