diff --git a/lib/Serverless.js b/lib/Serverless.js index e43241b11..82dfa4a18 100644 --- a/lib/Serverless.js +++ b/lib/Serverless.js @@ -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) { diff --git a/lib/classes/PluginManager.js b/lib/classes/PluginManager.js index 0853ebdad..fed11f4de 100644 --- a/lib/classes/PluginManager.js +++ b/lib/classes/PluginManager.js @@ -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 = {}; diff --git a/lib/classes/Variables.js b/lib/classes/Variables.js index 685963cd1..5927df90e 100644 --- a/lib/classes/Variables.js +++ b/lib/classes/Variables.js @@ -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.',