mirror of
https://github.com/serverless/serverless.git
synced 2025-12-08 19:46:03 +00:00
397 lines
16 KiB
JavaScript
397 lines
16 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 = {};
|
|
this.defaults = {
|
|
stage: 'dev',
|
|
region: 'us-east-1',
|
|
variableSyntax: '\\${([a-zA-Z0-9._\\-\\/\\(\\)]+?)}',
|
|
};
|
|
this.custom = {};
|
|
this.plugins = [];
|
|
this.functions = {};
|
|
this.environment = {};
|
|
this.resources = {};
|
|
this.package = {};
|
|
|
|
if (data) this.update(data);
|
|
}
|
|
|
|
load(rawOptions) {
|
|
const that = this;
|
|
const options = rawOptions || {};
|
|
options.stage = options.stage || options.s;
|
|
options.region = options.region || options.r;
|
|
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();
|
|
}
|
|
|
|
let serverlessYmlPath = path.join(servicePath, 'serverless.yml');
|
|
// change to serverless.yaml if the file could not be found
|
|
if (!this.serverless.utils.fileExistsSync(serverlessYmlPath)) {
|
|
serverlessYmlPath = path
|
|
.join(this.serverless.config.servicePath, 'serverless.yaml');
|
|
}
|
|
|
|
return that.serverless.yamlParser
|
|
.parse(serverlessYmlPath)
|
|
.then((serverlessFileParam) => {
|
|
const serverlessFile = serverlessFileParam;
|
|
// basic service level validation
|
|
if (!serverlessFile.service) {
|
|
throw new SError('"service" property is missing in serverless.yml');
|
|
}
|
|
if (!serverlessFile.provider) {
|
|
throw new SError('"provider" property is missing in serverless.yml');
|
|
}
|
|
if (!serverlessFile.functions) {
|
|
throw new SError('"functions" property is missing in serverless.yml');
|
|
}
|
|
|
|
if (typeof serverlessFile.provider !== 'object') {
|
|
const providerName = serverlessFile.provider;
|
|
serverlessFile.provider = {
|
|
name: providerName,
|
|
};
|
|
}
|
|
|
|
if (['aws', 'azure', 'google', 'ibm'].indexOf(serverlessFile.provider.name)) {
|
|
const errorMessage = [
|
|
`Provider "${serverlessFile.provider.name}" is not supported.`,
|
|
' Valid values for provider are: aws, azure, google, ibm.',
|
|
' Please provide one of those values to the "provider" property in serverless.yml.',
|
|
].join('');
|
|
throw new SError(errorMessage);
|
|
}
|
|
|
|
that.service = serverlessFile.service;
|
|
that.provider = serverlessFile.provider;
|
|
that.custom = serverlessFile.custom;
|
|
that.plugins = serverlessFile.plugins;
|
|
that.resources = serverlessFile.resources;
|
|
that.functions = serverlessFile.functions;
|
|
|
|
// setup function.name property
|
|
_.forEach(that.functions, (functionObj, functionName) => {
|
|
if (!functionObj.events) {
|
|
that.functions[functionName].events = [];
|
|
}
|
|
|
|
if (!functionObj.name) {
|
|
that.functions[functionName].name = `${that.service}-${options.stage}-${functionName}`;
|
|
}
|
|
});
|
|
|
|
if (serverlessFile.package && serverlessFile.package.artifact) {
|
|
that.package.artifact = serverlessFile.package.artifact;
|
|
}
|
|
if (serverlessFile.package && serverlessFile.package.exclude) {
|
|
that.package.exclude = serverlessFile.package.exclude;
|
|
}
|
|
if (serverlessFile.package && serverlessFile.package.include) {
|
|
that.package.include = serverlessFile.package.include;
|
|
}
|
|
|
|
if (serverlessFile.defaults && serverlessFile.defaults.stage) {
|
|
this.defaults.stage = serverlessFile.defaults.stage;
|
|
}
|
|
if (serverlessFile.defaults && serverlessFile.defaults.region) {
|
|
this.defaults.region = serverlessFile.defaults.region;
|
|
}
|
|
if (serverlessFile.defaults && serverlessFile.defaults.variableSyntax) {
|
|
this.defaults.variableSyntax = serverlessFile.defaults.variableSyntax;
|
|
}
|
|
|
|
if (serverlessFile.defaults) {
|
|
const warningMessage = [
|
|
'Deprecation Notice: the "defaults" property in serverless.yml',
|
|
' is deprecated. The "stage", "region" & "variableSyntax" properties',
|
|
' has been moved to the "provider" property instead. Please update',
|
|
' your serverless.yml file asap. For more info, you can check our docs.',
|
|
].join('');
|
|
this.serverless.cli.log(warningMessage);
|
|
|
|
if (serverlessFile.defaults.stage) {
|
|
this.defaults.stage = serverlessFile.defaults.stage;
|
|
}
|
|
if (serverlessFile.defaults.region) {
|
|
this.defaults.region = serverlessFile.defaults.region;
|
|
}
|
|
if (serverlessFile.defaults.variableSyntax) {
|
|
this.defaults.variableSyntax = serverlessFile.defaults.variableSyntax;
|
|
}
|
|
}
|
|
|
|
// Moving defaults into provider obj
|
|
if (serverlessFile.provider.stage) {
|
|
this.defaults.stage = serverlessFile.provider.stage;
|
|
}
|
|
if (serverlessFile.provider.region) {
|
|
this.defaults.region = serverlessFile.provider.region;
|
|
}
|
|
if (serverlessFile.provider.variableSyntax) {
|
|
this.defaults.variableSyntax = serverlessFile.provider.variableSyntax;
|
|
}
|
|
|
|
return this;
|
|
});
|
|
}
|
|
|
|
populate(processedOptions) {
|
|
const that = this;
|
|
const options = processedOptions || {};
|
|
const variableSyntaxProperty = this.defaults.variableSyntax;
|
|
const variableSyntax = RegExp(variableSyntaxProperty, 'g');
|
|
const fileRefSyntax = RegExp(/^file\(([a-zA-Z0-9._\-\/]+?)\)/g);
|
|
|
|
// temporally remove variable syntax from service otherwise it'll match
|
|
this.defaults.variableSyntax = true;
|
|
this.serverless.service.defaults.variableSyntax = true;
|
|
|
|
|
|
/*
|
|
* 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 (property) {
|
|
const t = this;
|
|
|
|
if (typeof property === 'string') {
|
|
const nestedPopulate = (updatedPropertyParam) => {
|
|
let updatedProperty = updatedPropertyParam;
|
|
if (typeof updatedProperty === 'string' && updatedProperty.match(variableSyntax)) {
|
|
updatedProperty.match(variableSyntax).forEach((matchedString) => {
|
|
const variableString = matchedString
|
|
.replace(variableSyntax, (match, varName) => varName.trim());
|
|
|
|
/*
|
|
* File Reference
|
|
*/
|
|
if (variableString.match(fileRefSyntax)) {
|
|
const matchedFileRefString = variableString.match(fileRefSyntax)[0];
|
|
const referencedFileRelativePath = matchedFileRefString
|
|
.replace(fileRefSyntax, (match, varName) => varName.trim());
|
|
const referencedFileFullPath = path.join(that.serverless.config.servicePath,
|
|
referencedFileRelativePath);
|
|
|
|
let value = that.serverless.utils.readFileSync(referencedFileFullPath);
|
|
if (matchedFileRefString !== variableString) {
|
|
let deepProperties = variableString
|
|
.replace(matchedFileRefString, '');
|
|
if (deepProperties.substring(0, 1) !== '.') {
|
|
const errorMessage = [
|
|
'Invalid variable syntax when referencing',
|
|
` file "${referencedFileRelativePath}"`,
|
|
' Please use valid dot notation when referencing sub properties.',
|
|
].join('');
|
|
throw new that.serverless.classes
|
|
.Error(errorMessage);
|
|
}
|
|
deepProperties = deepProperties.slice(1);
|
|
const selfSubProperties = deepProperties.split('.');
|
|
selfSubProperties.forEach(selfSubProperty => {
|
|
if (!value[selfSubProperty]) {
|
|
const errorMessage = [
|
|
`file "${referencedFileRelativePath}" doesn't`,
|
|
` have sub property "${selfSubProperty}".`,
|
|
' Please make sure you are referencing the correct sub property',
|
|
].join('');
|
|
throw new that.serverless.classes
|
|
.Error(errorMessage);
|
|
}
|
|
value = value[selfSubProperty];
|
|
});
|
|
}
|
|
|
|
if (typeof value === 'string') {
|
|
updatedProperty = replaceall(matchedString, value, updatedProperty);
|
|
} else {
|
|
if (updatedProperty !== matchedString) {
|
|
const errorMessage = [
|
|
'Trying to populate non string value into',
|
|
` a string when referencing file "${referencedFileRelativePath}".`,
|
|
' Please make sure the value of the property',
|
|
' is a string',
|
|
].join('');
|
|
throw new that.serverless.classes
|
|
.Error(errorMessage);
|
|
}
|
|
updatedProperty = value;
|
|
}
|
|
/*
|
|
* Env Var Reference
|
|
*/
|
|
} else if (variableString.split('.')[0] === 'env') {
|
|
if (variableString.split('.').length !== 2) {
|
|
const errorMessage = [
|
|
'Trying to access sub properties of environment',
|
|
' variable strings, or trying to reference all environment variable.',
|
|
].join('');
|
|
throw new SError(errorMessage);
|
|
}
|
|
const requestedEnvVar = variableString.split('.')[1];
|
|
const propertyValue = process.env[requestedEnvVar];
|
|
if (typeof propertyValue === 'undefined') {
|
|
const errorMessage = [
|
|
`Environment variable ${requestedEnvVar} is not set on your machine.`,
|
|
' Please set this env var before referencing it as a variable.',
|
|
].join('');
|
|
throw new SError(errorMessage);
|
|
}
|
|
updatedProperty = replaceall(matchedString, propertyValue, updatedProperty);
|
|
|
|
/*
|
|
* Options Reference
|
|
*/
|
|
} else if (variableString.split('.')[0] === 'opt') {
|
|
if (variableString.split('.').length === 1) {
|
|
// load all options object
|
|
if (updatedProperty === matchedString) {
|
|
updatedProperty = options;
|
|
} else {
|
|
const errorMessage = [
|
|
'Trying to reference all options object as a substring.',
|
|
' Please make sure the string referencing the variable',
|
|
' Does not contain any other sub-strings,',
|
|
' or reference a specific option string.',
|
|
].join('');
|
|
throw new SError(errorMessage);
|
|
}
|
|
} else if (variableString.split('.').length === 2) {
|
|
// load specific option
|
|
const requestedOption = variableString.split('.')[1];
|
|
const propertyValue = options[requestedOption];
|
|
if (typeof propertyValue === 'undefined') {
|
|
const errorMessage = [
|
|
`Option ${requestedOption} was not passed in the CLI.`,
|
|
' Please pass this variable in the CLI to use in serverless.yml.',
|
|
].join('');
|
|
throw new SError(errorMessage);
|
|
}
|
|
updatedProperty = replaceall(matchedString, propertyValue, updatedProperty);
|
|
} else {
|
|
const errorMessage = [
|
|
'Trying to reference a specific option sub properties.',
|
|
' Each passed option can only be a string, not objects.',
|
|
' Please make sure you only reference the option string',
|
|
' without any other dot notation.',
|
|
].join('');
|
|
throw new SError(errorMessage);
|
|
}
|
|
|
|
/*
|
|
* Self Reference
|
|
*/
|
|
} else if (variableString.split('.')[0] === 'self') {
|
|
if (variableString.split('.').length === 1) {
|
|
const errorMessage = [
|
|
'You can\'t reference the entire "self" serverless.yml file.',
|
|
' Please reference a sub property with ${self.subProp}',
|
|
].join('');
|
|
throw new that.serverless.classes
|
|
.Error(errorMessage);
|
|
}
|
|
let value = _.cloneDeep(that);
|
|
const selfSubProperties = variableString.split('.');
|
|
// remove first element. It's the "self" keyword
|
|
selfSubProperties.splice(0, 1);
|
|
selfSubProperties.forEach(selfSubProperty => {
|
|
if (!value[selfSubProperty]) {
|
|
const errorMessage = [
|
|
`serverless.yml doesn't have sub property "${selfSubProperty}".`,
|
|
' Please make sure you are referencing the correct sub property',
|
|
].join('');
|
|
throw new that.serverless.classes
|
|
.Error(errorMessage);
|
|
}
|
|
value = value[selfSubProperty];
|
|
});
|
|
|
|
if (typeof value === 'string') {
|
|
updatedProperty = replaceall(matchedString, value, updatedProperty);
|
|
} else {
|
|
if (updatedProperty !== matchedString) {
|
|
const errorMessage = [
|
|
'Trying to populate non string value into',
|
|
' a string when referencing "self".',
|
|
' Please make sure the value of the property',
|
|
' is a string',
|
|
].join('');
|
|
throw new that.serverless.classes
|
|
.Error(errorMessage);
|
|
}
|
|
updatedProperty = value;
|
|
}
|
|
} else if () {
|
|
const variableStringProcessed = variableString.split(',');
|
|
} else {
|
|
const errorMessage = [
|
|
`Invalid variable reference syntax for variable ${matchedString}.`,
|
|
' You can only reference env vars, options, & files.',
|
|
' You can check our docs for more info.',
|
|
].join('');
|
|
throw new SError(errorMessage);
|
|
}
|
|
});
|
|
|
|
return nestedPopulate(updatedProperty);
|
|
}
|
|
return updatedProperty;
|
|
};
|
|
const updatedProperty = nestedPopulate(property);
|
|
t.update(updatedProperty);
|
|
}
|
|
});
|
|
|
|
// put back variable syntax that we removed earlier
|
|
this.defaults.variableSyntax = variableSyntaxProperty;
|
|
this.serverless.service.defaults.variableSyntax = variableSyntaxProperty;
|
|
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);
|
|
}
|
|
}
|
|
|
|
module.exports = Service;
|