Add plugin hooks to define config variable getters

This commit is contained in:
Daniel Schep 2019-08-20 11:23:42 -04:00
parent 5dd35b7ba2
commit 02fa190d0b
3 changed files with 84 additions and 44 deletions

View File

@ -100,21 +100,29 @@ class Serverless {
// populate variables after --help, otherwise help may fail to print
// (https://github.com/serverless/serverless/issues/2041)
return this.variables.populateService(this.pluginManager.cliOptions).then(() => {
// merge arrays after variables have been populated
// (https://github.com/serverless/serverless/issues/3511)
this.service.mergeArrays();
return this.variables
.populateService(this.pluginManager.cliOptions)
.then(() => {
// merge arrays after variables have been populated
// (https://github.com/serverless/serverless/issues/3511)
this.service.mergeArrays();
// populate function names after variables are loaded in case functions were externalized
// (https://github.com/serverless/serverless/issues/2997)
this.service.setFunctionNames(this.processedInput.options);
// populate function names after variables are loaded in case functions were externalized
// (https://github.com/serverless/serverless/issues/2997)
this.service.setFunctionNames(this.processedInput.options);
// validate the service configuration, now that variables are loaded
this.service.validate();
// initialize hooks
return BbPromise.mapSeries(this.pluginManager.getHooks(['initialize']), ({ hook }) =>
hook()
);
})
.then(() => {
// validate the service configuration, now that variables are loaded
this.service.validate();
// trigger the plugin lifecycle when there's something which should be processed
return this.pluginManager.run(this.processedInput.commands);
});
// trigger the plugin lifecycle when there's something which should be processed
return this.pluginManager.run(this.processedInput.commands);
});
}
setProvider(name, provider) {

View File

@ -86,6 +86,7 @@ class PluginManager {
this.loadCommands(pluginInstance);
this.loadHooks(pluginInstance);
this.loadVariableGetters(pluginInstance);
this.plugins.push(pluginInstance);
}
@ -309,6 +310,21 @@ class PluginManager {
});
}
loadVariableGetters(pluginInstance) {
_.forEach(pluginInstance.variableGetters || [], ([regex, getterFunc, options]) => {
this.serverless.variables.customVariableResolverFuncs[getterFunc.name] = getterFunc.bind(
this.serverless.variables
);
this.serverless.variables.variableResolvers.push([regex, getterFunc.name]);
if (options && options.dependendServiceName) {
this.serverless.variables.dependentServices.push({
name: options.dependendServiceName,
method: getterFunc.name,
});
}
});
}
getCommands() {
const result = {};

View File

@ -55,6 +55,25 @@ class Variables {
this.cfRefSyntax = RegExp(/^(?:\${)?cf(\.[a-zA-Z0-9-]+)?:/g);
this.s3RefSyntax = RegExp(/^(?:\${)?s3:(.+?)\/(.+)$/);
this.ssmRefSyntax = RegExp(/^(?:\${)?ssm:([a-zA-Z0-9_.\-/]+)[~]?(true|false|split)?/);
this.customVariableResolverFuncs = {};
this.variableResolvers = [
[this.slsRefSyntax, 'getValueFromSls'],
[this.envRefSyntax, 'getValueFromEnv'],
[this.optRefSyntax, 'getValueFromOptions'],
[this.selfRefSyntax, 'getValueFromSelf'],
[this.fileRefSyntax, 'getValueFromFile'],
[this.cfRefSyntax, 'getValueFromCf'],
[this.s3RefSyntax, 'getValueFromS3'],
[this.stringRefSyntax, 'getValueFromString'],
[this.ssmRefSyntax, 'getValueFromSsm'],
[this.deepRefSyntax, 'getValueFromDeep'],
];
this.dependentServices = [
{ name: 'CloudFormation', method: 'getValueFromCf' },
{ name: 'S3', method: 'getValueFromS3' },
{ name: 'SSM', method: 'getValueFromSsm' },
];
}
loadVariableSyntax() {
@ -74,26 +93,33 @@ class Variables {
// ## SERVICE ##
// #############
disableDepedentServices(func) {
const dependentServices = [
{ name: 'CloudFormation', method: 'getValueFromCf', original: this.getValueFromCf },
{ name: 'S3', method: 'getValueFromS3', original: this.getValueFromS3 },
{ name: 'SSM', method: 'getValueFromSsm', original: this.getValueFromSsm },
];
const dependencyMessage = (configValue, serviceName) =>
`Variable dependency failure: variable '${configValue}' references service ${serviceName} but using that service requires a concrete value to be called.`;
`Variable dependency failure: variable '${configValue}' references ${serviceName} but using that service requires a concrete value to be called.`;
// replace and then restore the methods for obtaining values from dependent services. the
// replacement naturally rejects dependencies on these services that occur during prepopulation.
// prepopulation is, of course, the process of obtaining the required configuration for using
// these services.
dependentServices.forEach(dependentService => {
this.dependentServices.forEach(dependentService => {
// save original
dependentService.original =
this[dependentService.method] || this.customVariableResolverFuncs[dependentService.method];
// knock out
this[dependentService.method] = variableString =>
BbPromise.reject(dependencyMessage(variableString, dependentService.name));
if (this[dependentService.method]) {
this[dependentService.method] = variableString =>
BbPromise.reject(dependencyMessage(variableString, dependentService.name));
} else {
this.customVariableResolverFuncs[dependentService.method] = variableString =>
BbPromise.reject(dependencyMessage(variableString, dependentService.name));
}
});
return func().finally(() => {
dependentServices.forEach(dependentService => {
this.dependentServices.forEach(dependentService => {
// restore
this[dependentService.method] = dependentService.original;
if (this[dependentService.method]) {
this[dependentService.method] = dependentService.original;
} else {
this.customVariableResolverFuncs[dependentService.method] = dependentService.original;
}
});
});
}
@ -525,27 +551,17 @@ class Variables {
if (this.tracker.contains(variableString)) {
ret = this.tracker.get(variableString, propertyString);
} else {
if (variableString.match(this.slsRefSyntax)) {
ret = this.getValueFromSls(variableString);
} else if (variableString.match(this.envRefSyntax)) {
ret = this.getValueFromEnv(variableString);
} else if (variableString.match(this.optRefSyntax)) {
ret = this.getValueFromOptions(variableString);
} else if (variableString.match(this.selfRefSyntax)) {
ret = this.getValueFromSelf(variableString);
} else if (variableString.match(this.fileRefSyntax)) {
ret = this.getValueFromFile(variableString);
} else if (variableString.match(this.cfRefSyntax)) {
ret = this.getValueFromCf(variableString);
} else if (variableString.match(this.s3RefSyntax)) {
ret = this.getValueFromS3(variableString);
} else if (variableString.match(this.stringRefSyntax)) {
ret = this.getValueFromString(variableString);
} else if (variableString.match(this.ssmRefSyntax)) {
ret = this.getValueFromSsm(variableString);
} else if (variableString.match(this.deepRefSyntax)) {
ret = this.getValueFromDeep(variableString);
} else {
for (const [regex, getter] of this.variableResolvers) {
if (variableString.match(regex)) {
if (this[getter]) {
ret = this[getter].bind(this)(variableString);
} else {
ret = this.customVariableResolverFuncs[getter](variableString);
}
break;
}
}
if (!ret) {
const errorMessage = [
`Invalid variable reference syntax for variable ${variableString}.`,
' You can only reference env vars, options, & files.',