1414
1515const util = require ( "util" ) ;
1616const pathlib = require ( "path" ) ;
17- const { readFile } = require ( "fs" ) ;
17+ const fs = require ( "fs" ) ;
1818const VError = require ( "verror" ) ;
1919const yaml = require ( "yaml" ) ;
2020const redis = require ( "./redis-adapter" ) ;
@@ -49,10 +49,17 @@ const CONFIG_SOURCE = Object.freeze({
4949 AUTO : "AUTO" ,
5050} ) ;
5151
52+ const CONFIG_MERGE_CONFLICT = Object . freeze ( {
53+ THROW : "THROW" ,
54+ PRESERVE : "PRESERVE" ,
55+ OVERRIDE : "OVERRIDE" ,
56+ } ) ;
57+
5258const CONFIG_KEY = Object . freeze ( {
5359 TYPE : "TYPE" ,
5460 ACTIVE : "ACTIVE" ,
5561 SOURCE : "SOURCE" ,
62+ SOURCE_FILEPATH : "SOURCE_FILEPATH" ,
5663 APP_URL : "APP_URL" ,
5764 APP_URL_ACTIVE : "APP_URL_ACTIVE" ,
5865 VALIDATIONS : "VALIDATIONS" ,
@@ -113,7 +120,7 @@ const SCOPE_PREFERENCE_ORDER_MASKS = [
113120] ;
114121
115122const cfEnv = CfEnv . getInstance ( ) ;
116- const readFileAsync = util . promisify ( readFile ) ;
123+ const readFileAsync = util . promisify ( fs . readFile ) ;
117124let logger = new Logger ( COMPONENT_NAME ) ;
118125
119126/**
@@ -219,7 +226,7 @@ class FeatureToggles {
219226 }
220227 }
221228
222- _processConfigSource ( source , configFromSource , configFilepath ) {
229+ _processConfigSource ( source , mergeConflictBehavior , configFromSource , sourceFilepath ) {
223230 let count = 0 ;
224231 if ( ! isObject ( configFromSource ) ) {
225232 return count ;
@@ -229,7 +236,31 @@ class FeatureToggles {
229236 const entries = Object . entries ( configFromSource ) ;
230237 for ( const [ featureKey , value ] of entries ) {
231238 if ( this . __config [ featureKey ] ) {
232- continue ;
239+ switch ( mergeConflictBehavior ) {
240+ case CONFIG_MERGE_CONFLICT . PRESERVE : {
241+ continue ;
242+ }
243+ case CONFIG_MERGE_CONFLICT . THROW : // eslint-disable-current-line no-fallthrough
244+ default : {
245+ const sourceExisting = this . __config [ featureKey ] [ CONFIG_KEY . SOURCE ] ;
246+ const sourceConflicting = source ;
247+ const sourceFilepathExisting = this . __config [ featureKey ] [ CONFIG_KEY . SOURCE_FILEPATH ] ;
248+ const sourceFilepathConflicting = sourceFilepath ;
249+ throw new VError (
250+ {
251+ name : VERROR_CLUSTER_NAME ,
252+ info : {
253+ featureKey,
254+ sourceExisting,
255+ sourceConflicting,
256+ ...( sourceFilepathExisting && { sourceFilepathExisting } ) ,
257+ ...( sourceFilepathConflicting && { sourceFilepathConflicting } ) ,
258+ } ,
259+ } ,
260+ "feature is configured twice"
261+ ) ;
262+ }
263+ }
233264 }
234265 count ++ ;
235266
@@ -240,6 +271,7 @@ class FeatureToggles {
240271 info : {
241272 featureKey,
242273 source,
274+ ...( sourceFilepath && { sourceFilepath } ) ,
243275 } ,
244276 } ,
245277 "feature configuration is not an object"
@@ -254,6 +286,10 @@ class FeatureToggles {
254286
255287 this . __config [ featureKey ] [ CONFIG_KEY . SOURCE ] = source ;
256288
289+ if ( sourceFilepath ) {
290+ this . __config [ featureKey ] [ CONFIG_KEY . SOURCE_FILEPATH ] = sourceFilepath ;
291+ }
292+
257293 if ( type ) {
258294 this . __config [ featureKey ] [ CONFIG_KEY . TYPE ] = type ;
259295 }
@@ -272,7 +308,7 @@ class FeatureToggles {
272308
273309 if ( validations ) {
274310 this . __config [ featureKey ] [ CONFIG_KEY . VALIDATIONS ] = validations ;
275- this . _processValidations ( featureKey , validations , configFilepath ) ;
311+ this . _processValidations ( featureKey , validations , sourceFilepath ) ;
276312 }
277313 }
278314
@@ -282,10 +318,19 @@ class FeatureToggles {
282318 /**
283319 * Populate this.__config.
284320 */
285- _processConfig ( [ configRuntime , configFromFile , configAuto ] , configFilepath ) {
286- const configRuntimeCount = this . _processConfigSource ( CONFIG_SOURCE . RUNTIME , configRuntime , configFilepath ) ;
287- const configFromFileCount = this . _processConfigSource ( CONFIG_SOURCE . FILE , configFromFile , configFilepath ) ;
288- const configAutoCount = this . _processConfigSource ( CONFIG_SOURCE . AUTO , configAuto , configFilepath ) ;
321+ _processConfig ( { configRuntime, configFromFilesEntries, configAuto } = { } ) {
322+ const configRuntimeCount = this . _processConfigSource (
323+ CONFIG_SOURCE . RUNTIME ,
324+ CONFIG_MERGE_CONFLICT . THROW ,
325+ configRuntime
326+ ) ;
327+ const configFromFileCount = configFromFilesEntries . reduce (
328+ ( count , [ configFilepath , configFromFile ] ) =>
329+ count +
330+ this . _processConfigSource ( CONFIG_SOURCE . FILE , CONFIG_MERGE_CONFLICT . THROW , configFromFile , configFilepath ) ,
331+ 0
332+ ) ;
333+ const configAutoCount = this . _processConfigSource ( CONFIG_SOURCE . AUTO , CONFIG_MERGE_CONFLICT . PRESERVE , configAuto ) ;
289334
290335 this . __isConfigProcessed = true ;
291336 return {
@@ -735,55 +780,74 @@ class FeatureToggles {
735780 name : VERROR_CLUSTER_NAME ,
736781 info : { configFilepath } ,
737782 } ,
738- "configFilepath with unsupported extension, allowed extensions are .yaml and .json"
783+ "config filepath with unsupported extension, allowed extensions are .yaml and .json"
739784 ) ;
740785 }
741786
787+ static async _consolidatedConfigFilepaths ( configFilepath , configFilepaths ) {
788+ let result = [ ] ;
789+ if ( configFilepath ) {
790+ result . push ( configFilepath ) ;
791+ }
792+ if ( configFilepaths ) {
793+ result = result . concat ( Object . values ( configFilepaths ) ) ;
794+ }
795+ if ( result . length === 0 && ( await tryPathReadable ( DEFAULT_CONFIG_FILEPATH ) ) ) {
796+ result . push ( DEFAULT_CONFIG_FILEPATH ) ;
797+ }
798+ return result ;
799+ }
800+
742801 /**
743802 * Implementation for {@link initializeFeatures}.
744803 *
745804 * @param {InitializeOptions } [options]
746805 */
747- async _initializeFeatures ( { config : configRuntime , configFile : configFilepath , configAuto } = { } ) {
806+ async _initializeFeatures ( {
807+ config : configRuntime ,
808+ configFile : configFilepath ,
809+ configFiles : configFilepaths ,
810+ configAuto,
811+ } = { } ) {
748812 if ( this . __isInitialized ) {
749813 return ;
750814 }
751815
752- let configFromFile ;
753- try {
754- if ( ! configFilepath && ( await tryPathReadable ( DEFAULT_CONFIG_FILEPATH ) ) ) {
755- configFilepath = DEFAULT_CONFIG_FILEPATH ;
756- }
757- if ( configFilepath ) {
758- configFromFile = await FeatureToggles . readConfigFromFile ( configFilepath ) ;
759- }
760- } catch ( err ) {
761- throw new VError (
762- {
763- name : VERROR_CLUSTER_NAME ,
764- cause : err ,
765- info : {
766- configFilepath,
767- } ,
768- } ,
769- "initialization aborted, could not read config file"
770- ) ;
771- }
816+ const consolidatedConfigFilepaths = await FeatureToggles . _consolidatedConfigFilepaths (
817+ configFilepath ,
818+ configFilepaths
819+ ) ;
820+ const configFromFilesEntries = await Promise . all (
821+ consolidatedConfigFilepaths . map ( async ( configFilepath ) => {
822+ try {
823+ return [ configFilepath , await FeatureToggles . readConfigFromFile ( configFilepath ) ] ;
824+ } catch ( err ) {
825+ throw new VError (
826+ {
827+ name : VERROR_CLUSTER_NAME ,
828+ cause : err ,
829+ info : {
830+ configFilepath,
831+ } ,
832+ } ,
833+ "initialization aborted, could not read config file"
834+ ) ;
835+ }
836+ } )
837+ ) ;
772838
773839 let toggleCounts ;
774840 try {
775- toggleCounts = this . _processConfig ( [ configRuntime , configFromFile , configAuto ] , configFilepath ) ;
841+ toggleCounts = this . _processConfig ( {
842+ configRuntime,
843+ configFromFilesEntries,
844+ configAuto,
845+ } ) ;
776846 } catch ( err ) {
777847 throw new VError (
778848 {
779849 name : VERROR_CLUSTER_NAME ,
780850 cause : err ,
781- info : {
782- ...( configAuto && { configAuto : JSON . stringify ( configAuto ) } ) ,
783- ...( configFromFile && { configFromFile : JSON . stringify ( configFromFile ) } ) ,
784- ...( configRuntime && { configRuntime : JSON . stringify ( configRuntime ) } ) ,
785- configFilepath,
786- } ,
787851 } ,
788852 "initialization aborted, could not process configuration"
789853 ) ;
@@ -1662,6 +1726,7 @@ module.exports = {
16621726 ENV ,
16631727 DEFAULT_REDIS_CHANNEL ,
16641728 DEFAULT_REDIS_KEY ,
1729+ DEFAULT_CONFIG_FILEPATH ,
16651730 SCOPE_ROOT_KEY ,
16661731 FeatureToggles,
16671732
0 commit comments