serverless/lib/classes/Service.js
2016-08-31 16:37:58 +09:00

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;