Просмотр исходного кода

Merge pull request #711 from weseek/feat/config-loader-and-manager

Feat/config loader and manager
Yuki Takei 7 лет назад
Родитель
Сommit
f8ef53a9fe

+ 9 - 0
src/server/crowi/index.js

@@ -33,6 +33,7 @@ function Crowi(rootdir) {
   this.cacheDir    = path.join(this.tmpDir, 'cache');
 
   this.config = {};
+  this.configManager = null;
   this.searcher = null;
   this.mailer = {};
   this.passportService = null;
@@ -78,6 +79,8 @@ Crowi.prototype.init = function() {
       return self.setupSessionConfig();
     }).then(function() {
       return self.setupAppConfig();
+    }).then(function() {
+      return self.setupConfigManager();
     }).then(function() {
       return self.scanRuntimeVersions();
     }).then(function() {
@@ -205,6 +208,12 @@ Crowi.prototype.setupAppConfig = function() {
   });
 };
 
+Crowi.prototype.setupConfigManager = async function() {
+  const ConfigManager = require('../service/config-manager');
+  this.configManager = new ConfigManager(this.model('Config'));
+  return await this.configManager.loadConfigs();
+};
+
 Crowi.prototype.setupModels = function() {
   var self = this
     ;

+ 14 - 0
src/server/models/config.js

@@ -149,6 +149,20 @@ module.exports = function(crowi) {
     return config.markdown[key];
   }
 
+  /**
+   * It is deprecated to use this for anything other than ConfigLoader#load.
+   */
+  configSchema.statics.getDefaultCrowiConfigsObject = function() {
+    return getDefaultCrowiConfigs();
+  };
+
+  /**
+   * It is deprecated to use this for anything other than ConfigLoader#load.
+   */
+  configSchema.statics.getDefaultMarkdownConfigsObject = function() {
+    return getDefaultMarkdownConfigs();
+  };
+
   configSchema.statics.getRestrictGuestModeLabels = function() {
     var labels = {};
     labels[SECURITY_RESTRICT_GUEST_MODE_DENY]     = 'security_setting.guest_mode.deny';

+ 200 - 0
src/server/service/config-loader.js

@@ -0,0 +1,200 @@
+const debug = require('debug')('growi:service:ConfigLoader');
+
+const TYPES = {
+  NUMBER:  { parse: (v) => parseInt(v) },
+  STRING:  { parse: (v) => v },
+  BOOLEAN: { parse: (v) => /^(true|1)$/i.test(v) }
+};
+
+/**
+ * The following env vars are excluded because these are currently used before the configuration setup.
+ * - MONGO_URI
+ * - NODE_ENV
+ * - PORT
+ * - REDIS_URI
+ * - SESSION_NAME
+ * - PASSWORD_SEED
+ * - SECRET_TOKEN
+ *
+ *  The commented out item has not yet entered the migration work.
+ *  So, parameters of these are under consideration.
+ */
+const ENV_VAR_NAME_TO_CONFIG_INFO = {
+  // ELASTICSEARCH_URI: {
+  //   ns:      ,
+  //   key:     ,
+  //   type:    ,
+  //   default:
+  // },
+  // FILE_UPLOAD: {
+  //   ns:      ,
+  //   key:     ,
+  //   type:    ,
+  //   default:
+  // },
+  // HACKMD_URI: {
+  //   ns:      ,
+  //   key:     ,
+  //   type:    ,
+  //   default:
+  // },
+  // HACKMD_URI_FOR_SERVER: {
+  //   ns:      ,
+  //   key:     ,
+  //   type:    ,
+  //   default:
+  // },
+  // PLANTUML_URI: {
+  //   ns:      ,
+  //   key:     ,
+  //   type:    ,
+  //   default:
+  // },
+  // BLOCKDIAG_URI: {
+  //   ns:      ,
+  //   key:     ,
+  //   type:    ,
+  //   default:
+  // },
+  // OAUTH_GOOGLE_CLIENT_ID: {
+  //   ns:      'crowi',
+  //   key:     'security:passport-google:clientId',
+  //   type:    ,
+  //   default:
+  // },
+  // OAUTH_GOOGLE_CLIENT_SECRET: {
+  //   ns:      'crowi',
+  //   key:     'security:passport-google:clientSecret',
+  //   type:    ,
+  //   default:
+  // },
+  // OAUTH_GOOGLE_CALLBACK_URI: {
+  //   ns:      'crowi',
+  //   key:     'security:passport-google:callbackUrl',
+  //   type:    ,
+  //   default:
+  // },
+  // OAUTH_GITHUB_CLIENT_ID: {
+  //   ns:      'crowi',
+  //   key:     'security:passport-github:clientId',
+  //   type:    ,
+  //   default:
+  // },
+  // OAUTH_GITHUB_CLIENT_SECRET: {
+  //   ns:      'crowi',
+  //   key:     'security:passport-github:clientSecret',
+  //   type:    ,
+  //   default:
+  // },
+  // OAUTH_GITHUB_CALLBACK_URI: {
+  //   ns:      'crowi',
+  //   key:     'security:passport-github:callbackUrl',
+  //   type:    ,
+  //   default:
+  // },
+  // OAUTH_TWITTER_CONSUMER_KEY: {
+  //   ns:      'crowi',
+  //   key:     'security:passport-twitter:consumerKey',
+  //   type:    ,
+  //   default:
+  // },
+  // OAUTH_TWITTER_CONSUMER_SECRET: {
+  //   ns:      'crowi',
+  //   key:     'security:passport-twitter:consumerSecret',
+  //   type:    ,
+  //   default:
+  // },
+  // OAUTH_TWITTER_CALLBACK_URI: {
+  //   ns:      'crowi',
+  //   key:     'security:passport-twitter:callbackUrl',
+  //   type:    ,
+  //   default:
+  // },
+  SAML_ENTRY_POINT: {
+    ns:      'crowi',
+    key:     'security:passport-saml:entryPoint',
+    type:    TYPES.STRING,
+    default: null
+  },
+  SAML_CALLBACK_URI: {
+    ns:      'crowi',
+    key:     'security:passport-saml:callbackUrl',
+    type:    TYPES.STRING,
+    default: null
+  },
+  SAML_ISSUER: {
+    ns:      'crowi',
+    key:     'security:passport-saml:issuer',
+    type:    TYPES.STRING,
+    default: null
+  },
+  SAML_CERT: {
+    ns:      'crowi',
+    key:     'security:passport-saml:cert',
+    type:    TYPES.STRING,
+    default: null
+  }
+};
+
+class ConfigLoader {
+
+  constructor(configModel) {
+    this.configModel = configModel;
+  }
+
+  /**
+   * return a config object
+   */
+  async load() {
+    const configFromDB = await this.loadFromDB();
+    const configFromEnvVars = this.loadFromEnvVars();
+
+    // merge defaults
+    let mergedConfigFromDB = Object.assign({'crowi': this.configModel.getDefaultCrowiConfigsObject()}, configFromDB);
+    mergedConfigFromDB = Object.assign({'markdown': this.configModel.getDefaultMarkdownConfigsObject()}, mergedConfigFromDB);
+
+    return {
+      fromDB: mergedConfigFromDB,
+      fromEnvVars: configFromEnvVars
+    };
+  }
+
+  async loadFromDB() {
+    const config = {};
+    const docs = await this.configModel.find().exec();
+
+    for (const doc of docs) {
+      if (!config[doc.ns]) {
+        config[doc.ns] = {};
+      }
+      config[doc.ns][doc.key] = JSON.parse(doc.value);
+    }
+
+    debug('ConfigLoader#loadFromDB', config);
+
+    return config;
+  }
+
+  loadFromEnvVars() {
+    const config = {};
+    for (const ENV_VAR_NAME of Object.keys(ENV_VAR_NAME_TO_CONFIG_INFO)) {
+      const configInfo = ENV_VAR_NAME_TO_CONFIG_INFO[ENV_VAR_NAME];
+      if (config[configInfo.ns] === undefined) {
+        config[configInfo.ns] = {};
+      }
+
+      if (process.env[ENV_VAR_NAME] === undefined) {
+        config[configInfo.ns][configInfo.key] = configInfo.default;
+      }
+      else {
+        config[configInfo.ns][configInfo.key] = configInfo.type.parse(process.env[ENV_VAR_NAME]);
+      }
+    }
+
+    debug('ConfigLoader#loadFromEnvVars', config);
+
+    return config;
+  }
+}
+
+module.exports = ConfigLoader;

+ 125 - 0
src/server/service/config-manager.js

@@ -0,0 +1,125 @@
+const ConfigLoader = require('../service/config-loader')
+  , debug = require('debug')('growi:service:ConfigManager');
+
+class ConfigManager {
+
+  constructor(configModel) {
+    this.configModel = configModel;
+    this.configLoader = new ConfigLoader(this.configModel);
+    this.configObject = null;
+  }
+
+  /**
+   * load configs from the database and the environment variables
+   */
+  async loadConfigs() {
+    this.configObject = await this.configLoader.load();
+
+    debug('ConfigManager#loadConfigs', this.configObject);
+  }
+
+  /**
+   * get a config specified by namespace & key
+   *
+   * Basically, search a specified config from configs loaded from database at first
+   * and then from configs loaded from env vars.
+   *
+   * In some case, this search method changes.(not yet implemented)
+   */
+  getConfig(namespace, key) {
+    return this.defaultSearch(namespace, key);
+  }
+
+  /**
+   * private api
+   *
+   * Search a specified config from configs loaded from database at first
+   * and then from configs loaded from env vars.
+   *
+   * the followings are the meanings of each special return value.
+   * - null:      a specified config is not set.
+   * - undefined: a specified config does not exist.
+   */
+  defaultSearch(namespace, key) {
+    if (!this.configExistsInDB(namespace, key) && !this.configExistsInEnvVars(namespace, key)) {
+      return undefined;
+    }
+
+    if (this.configExistsInDB(namespace, key) && !this.configExistsInEnvVars(namespace, key) ) {
+      return this.configObject.fromDB[namespace][key];
+    }
+
+    if (!this.configExistsInDB(namespace, key) && this.configExistsInEnvVars(namespace, key) ) {
+      return this.configObject.fromEnvVars[namespace][key];
+    }
+
+    if (this.configExistsInDB(namespace, key) && this.configExistsInEnvVars(namespace, key) ) {
+      if (this.configObject.fromDB[namespace][key] !== null) {
+        return this.configObject.fromDB[namespace][key];
+      }
+      else {
+        return this.configObject.fromEnvVars[namespace][key];
+      }
+    }
+  }
+
+  /**
+   * check whether a specified config exists in configs loaded from the database
+   * @returns {boolean}
+   */
+  configExistsInDB(namespace, key) {
+    if (this.configObject.fromDB[namespace] === undefined) {
+      return false;
+    }
+
+    return this.configObject.fromDB[namespace][key] !== undefined;
+  }
+
+  /**
+   * check whether a specified config exists in configs loaded from the environment variables
+   * @returns {boolean}
+   */
+  configExistsInEnvVars(namespace, key) {
+    if (this.configObject.fromEnvVars[namespace] === undefined) {
+      return false;
+    }
+
+    return this.configObject.fromEnvVars[namespace][key] !== undefined;
+  }
+
+  /**
+   * update configs by a iterable object consisting of several objects with ns, key, value fields
+   *
+   * For example:
+   * ```
+   *  updateConfigs(
+   *   [{
+   *     ns:    'some namespace 1',
+   *     key:   'some key 1',
+   *     value: 'some value 1'
+   *   }, {
+   *     ns:    'some namespace 2',
+   *     key:   'some key 2',
+   *     value: 'some value 2'
+   *   }]
+   *  );
+   * ```
+   */
+  async updateConfigs(configs) {
+    const results = [];
+    for (const config of configs) {
+      results.push(
+        this.configModel.findOneAndUpdate(
+          { ns: config.ns, key: config.key },
+          { ns: config.ns, key: config.key, value: JSON.stringify(config.value) },
+          { upsert: true, }
+        ).exec()
+      );
+    }
+    await Promise.all(results);
+
+    await this.loadConfigs();
+  }
+}
+
+module.exports = ConfigManager;