mirror of
https://github.com/serverless/serverless.git
synced 2025-12-08 19:46:03 +00:00
1. Improve wording of documentation. Note the concrete object that will be written in its entirety 2. Expand documentation. Note a potential security risk for secrets 3. Move, without changes, the `warnIfNotFound` method because it was putting space between `getValueFromSource` and the various source type specific functions it uses 4. Implement unit tests for `warnIfNotFound` 5. Implement unit tests for `logWarning`
272 lines
9.2 KiB
JavaScript
272 lines
9.2 KiB
JavaScript
'use strict';
|
|
|
|
const _ = require('lodash');
|
|
const path = require('path');
|
|
const traverse = require('traverse');
|
|
const replaceall = require('replaceall');
|
|
const logWarning = require('./Error').logWarning;
|
|
|
|
class Variables {
|
|
|
|
constructor(serverless) {
|
|
this.serverless = serverless;
|
|
this.service = this.serverless.service;
|
|
|
|
this.overwriteSyntax = RegExp(/,/g);
|
|
this.fileRefSyntax = RegExp(/^file\(([a-zA-Z0-9._\-/]+?)\)/g);
|
|
this.envRefSyntax = RegExp(/^env:/g);
|
|
this.optRefSyntax = RegExp(/^opt:/g);
|
|
this.selfRefSyntax = RegExp(/^self:/g);
|
|
}
|
|
|
|
loadVariableSyntax() {
|
|
this.variableSyntax = RegExp(this.service.provider.variableSyntax, 'g');
|
|
}
|
|
|
|
populateService(processedOptions) {
|
|
const that = this;
|
|
this.options = processedOptions || {};
|
|
|
|
this.loadVariableSyntax();
|
|
|
|
const variableSyntaxProperty = this.service.provider.variableSyntax;
|
|
|
|
// temporally remove variable syntax from service otherwise it'll match
|
|
this.service.provider.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.service).forEach(function (propertyParam) {
|
|
const t = this;
|
|
let property = propertyParam;
|
|
|
|
if (typeof property === 'string') {
|
|
property = that.populateProperty(property, true);
|
|
t.update(property);
|
|
}
|
|
});
|
|
|
|
this.service.provider.variableSyntax = variableSyntaxProperty;
|
|
return this.service;
|
|
}
|
|
|
|
populateProperty(propertyParam, populateInPlace) {
|
|
let property;
|
|
if (populateInPlace) {
|
|
property = propertyParam;
|
|
} else {
|
|
property = _.cloneDeep(propertyParam);
|
|
}
|
|
let valueToPopulate;
|
|
if (typeof property === 'string' && property.match(this.variableSyntax)) {
|
|
property.match(this.variableSyntax).forEach((matchedString) => {
|
|
const variableString = matchedString
|
|
.replace(this.variableSyntax, (match, varName) => varName.trim())
|
|
.replace(/\s/g, '');
|
|
|
|
if (variableString.match(this.overwriteSyntax)) {
|
|
valueToPopulate = this.overwrite(variableString);
|
|
} else {
|
|
valueToPopulate = this.getValueFromSource(variableString);
|
|
}
|
|
|
|
this.warnIfNotFound(variableString, valueToPopulate);
|
|
|
|
property = this.populateVariable(property, matchedString, valueToPopulate);
|
|
});
|
|
if (property !== this.service) {
|
|
property = this.populateProperty(property);
|
|
}
|
|
return property;
|
|
}
|
|
return property;
|
|
}
|
|
|
|
populateVariable(propertyParam, matchedString, valueToPopulate) {
|
|
let property = propertyParam;
|
|
if (typeof valueToPopulate === 'string') {
|
|
property = replaceall(matchedString, valueToPopulate, property);
|
|
} else {
|
|
if (property !== matchedString) {
|
|
if (typeof valueToPopulate === 'number') {
|
|
property = replaceall(matchedString, String(valueToPopulate), property);
|
|
} else {
|
|
const errorMessage = [
|
|
'Trying to populate non string value into',
|
|
` a string for variable ${matchedString}.`,
|
|
' Please make sure the value of the property is a string.',
|
|
].join('');
|
|
throw new this.serverless.classes
|
|
.Error(errorMessage);
|
|
}
|
|
return property;
|
|
}
|
|
property = valueToPopulate;
|
|
}
|
|
return property;
|
|
}
|
|
|
|
overwrite(variableStringsString) {
|
|
let finalValue;
|
|
const variableStringsArray = variableStringsString.split(',');
|
|
variableStringsArray.find(variableString => {
|
|
finalValue = this.getValueFromSource(variableString);
|
|
return (finalValue !== null && typeof finalValue !== 'undefined') &&
|
|
!(typeof finalValue === 'object' && _.isEmpty(finalValue));
|
|
});
|
|
|
|
return finalValue;
|
|
}
|
|
|
|
getValueFromSource(variableString) {
|
|
let valueToPopulate;
|
|
if (variableString.match(this.envRefSyntax)) {
|
|
valueToPopulate = this.getValueFromEnv(variableString);
|
|
} else if (variableString.match(this.optRefSyntax)) {
|
|
valueToPopulate = this.getValueFromOptions(variableString);
|
|
} else if (variableString.match(this.selfRefSyntax)) {
|
|
valueToPopulate = this.getValueFromSelf(variableString);
|
|
} else if (variableString.match(this.fileRefSyntax)) {
|
|
valueToPopulate = this.getValueFromFile(variableString);
|
|
} else {
|
|
const errorMessage = [
|
|
`Invalid variable reference syntax for variable ${variableString}.`,
|
|
' You can only reference env vars, options, & files.',
|
|
' You can check our docs for more info.',
|
|
].join('');
|
|
throw new this.serverless.classes.Error(errorMessage);
|
|
}
|
|
return valueToPopulate;
|
|
}
|
|
|
|
getValueFromEnv(variableString) {
|
|
const requestedEnvVar = variableString.split(':')[1];
|
|
let valueToPopulate;
|
|
if (requestedEnvVar !== '' || '' in process.env) {
|
|
valueToPopulate = process.env[requestedEnvVar];
|
|
} else {
|
|
valueToPopulate = process.env;
|
|
}
|
|
return valueToPopulate;
|
|
}
|
|
|
|
getValueFromOptions(variableString) {
|
|
const requestedOption = variableString.split(':')[1];
|
|
let valueToPopulate;
|
|
if (requestedOption !== '' || '' in this.options) {
|
|
valueToPopulate = this.options[requestedOption];
|
|
} else {
|
|
valueToPopulate = this.options;
|
|
}
|
|
return valueToPopulate;
|
|
}
|
|
|
|
getValueFromSelf(variableString) {
|
|
let valueToPopulate = this.service;
|
|
const deepProperties = variableString.split(':')[1].split('.');
|
|
valueToPopulate = this.getDeepValue(deepProperties, valueToPopulate);
|
|
return valueToPopulate;
|
|
}
|
|
|
|
getValueFromFile(variableString) {
|
|
const matchedFileRefString = variableString.match(this.fileRefSyntax)[0];
|
|
const referencedFileRelativePath = matchedFileRefString
|
|
.replace(this.fileRefSyntax, (match, varName) => varName.trim());
|
|
const referencedFileFullPath = path.join(this.serverless.config.servicePath,
|
|
referencedFileRelativePath);
|
|
let fileExtension = referencedFileRelativePath.split('.');
|
|
fileExtension = fileExtension[fileExtension.length - 1];
|
|
|
|
// Validate file exists
|
|
if (!this.serverless.utils.fileExistsSync(referencedFileFullPath)) {
|
|
return undefined;
|
|
}
|
|
|
|
let valueToPopulate;
|
|
|
|
// Process JS files
|
|
if (fileExtension === 'js') {
|
|
const jsFile = require(referencedFileFullPath); // eslint-disable-line global-require
|
|
let jsModule = variableString.split(':')[1];
|
|
jsModule = jsModule.split('.')[0];
|
|
valueToPopulate = jsFile[jsModule]();
|
|
let deepProperties = variableString.replace(matchedFileRefString, '');
|
|
deepProperties = deepProperties.slice(1).split('.');
|
|
deepProperties.splice(0, 1);
|
|
valueToPopulate = this.getDeepValue(deepProperties, valueToPopulate);
|
|
|
|
if (typeof valueToPopulate === 'undefined') {
|
|
const errorMessage = [
|
|
'Invalid variable syntax when referencing',
|
|
` file "${referencedFileRelativePath}".`,
|
|
' Check if your javascript is returning the correct data.',
|
|
].join('');
|
|
throw new this.serverless.classes
|
|
.Error(errorMessage);
|
|
}
|
|
}
|
|
|
|
// Process everything except JS
|
|
if (fileExtension !== 'js') {
|
|
valueToPopulate = this.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}" sub properties`,
|
|
' Please use ":" to reference sub properties.',
|
|
].join('');
|
|
throw new this.serverless.classes
|
|
.Error(errorMessage);
|
|
}
|
|
deepProperties = deepProperties.slice(1).split('.');
|
|
valueToPopulate = this.getDeepValue(deepProperties, valueToPopulate);
|
|
}
|
|
}
|
|
return valueToPopulate;
|
|
}
|
|
|
|
getDeepValue(deepProperties, valueToPopulateParam) {
|
|
let valueToPopulate = valueToPopulateParam;
|
|
deepProperties.forEach(subProperty => {
|
|
if (typeof valueToPopulate === 'undefined') {
|
|
valueToPopulate = {};
|
|
} else if (subProperty !== '' || '' in valueToPopulate) {
|
|
valueToPopulate = valueToPopulate[subProperty];
|
|
}
|
|
if (typeof valueToPopulate === 'string' && valueToPopulate.match(this.variableSyntax)) {
|
|
valueToPopulate = this.populateProperty(valueToPopulate);
|
|
}
|
|
});
|
|
return valueToPopulate;
|
|
}
|
|
|
|
warnIfNotFound(variableString, valueToPopulate) {
|
|
if (
|
|
valueToPopulate === null ||
|
|
typeof valueToPopulate === 'undefined' ||
|
|
(typeof valueToPopulate === 'object' && _.isEmpty(valueToPopulate))
|
|
) {
|
|
let varType;
|
|
if (variableString.match(this.envRefSyntax)) {
|
|
varType = 'environment variable';
|
|
} else if (variableString.match(this.optRefSyntax)) {
|
|
varType = 'option';
|
|
} else if (variableString.match(this.selfRefSyntax)) {
|
|
varType = 'service attribute';
|
|
} else if (variableString.match(this.fileRefSyntax)) {
|
|
varType = 'file';
|
|
}
|
|
logWarning(
|
|
`A valid ${varType} to satisfy the declaration '${variableString}' could not be found.`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = Variables;
|