UNPKG

6.06 kBJavaScriptView Raw
1const fs = require('fs');
2const util = require('util');
3const readFile = util.promisify(fs.readFile);
4const writeFile = util.promisify(fs.writeFile);
5const path = require('path');
6const log = require('../logger').config;
7const chokidar = require('chokidar');
8const yamlOrJson = require('js-yaml');
9const eventBus = require('../eventBus');
10const schemas = require('../schemas');
11
12class Config {
13 constructor() {
14 this.models = {};
15
16 this.configTypes = {
17 system: {
18 baseFilename: 'system.config',
19 validator: schemas.register('config', 'system.config', require('./schemas/system.config.json')),
20 pathProperty: 'systemConfigPath',
21 configProperty: 'systemConfig'
22 },
23 gateway: {
24 baseFilename: 'gateway.config',
25 validator: schemas.register('config', 'gateway.config', require('./schemas/gateway.config.json')),
26 pathProperty: 'gatewayConfigPath',
27 configProperty: 'gatewayConfig'
28 }
29 };
30 }
31
32 loadConfig(type) {
33 const configType = this.configTypes[type];
34 let configPath = this[configType.pathProperty] || path.join(process.env.EG_CONFIG_DIR, `${configType.baseFilename}.yml`);
35 let config;
36
37 try {
38 fs.accessSync(configPath, fs.constants.R_OK);
39 } catch (e) {
40 log.verbose(`Unable to access ${configPath} file. Trying with the json counterpart.`);
41 configPath = path.join(process.env.EG_CONFIG_DIR, `${configType.baseFilename}.json`);
42 }
43
44 try {
45 config = yamlOrJson.load(envReplace(fs.readFileSync(configPath, 'utf8'), process.env));
46 } catch (err) {
47 log.error(`failed to (re)load ${type} config: ${err}`);
48 throw (err);
49 }
50
51 const { isValid, error } = configType.validator(config);
52
53 if (!isValid) {
54 throw new Error(error);
55 }
56
57 this[configType.pathProperty] = configPath;
58 this[configType.configProperty] = config;
59 log.debug(`ConfigPath: ${configPath}`);
60 }
61
62 loadGatewayConfig() { this.loadConfig('gateway'); }
63
64 loadModels() {
65 ['users.json', 'credentials.json', 'applications.json'].forEach(model => {
66 const module = path.resolve(process.env.EG_CONFIG_DIR, 'models', model);
67 const name = path.basename(module, '.json');
68 this.models[name] = require(module);
69 schemas.register('model', name, this.models[name]);
70 log.verbose(`Registered schema for ${name} model.`);
71 });
72 }
73
74 watch() {
75 if (typeof this.systemConfigPath !== 'string' || typeof this.gatewayConfigPath !== 'string') { return; }
76
77 const watchEvents = ['add', 'change'];
78
79 const watchOptions = {
80 awaitWriteFinish: true,
81 ignoreInitial: true
82 };
83
84 this.watcher = chokidar.watch([this.systemConfigPath, this.gatewayConfigPath], watchOptions);
85
86 watchEvents.forEach(watchEvent => {
87 this.watcher.on(watchEvent, name => {
88 const type = name === this.systemConfigPath ? 'system' : 'gateway';
89 log.info(`${watchEvent} event on ${name} file. Reloading ${type} config file`);
90
91 try {
92 this.loadConfig(type);
93 eventBus.emit('hot-reload', { type, config: this });
94 } catch (e) {
95 log.debug(`Failed hot reload of system config: ${e}`);
96 }
97 });
98 });
99 }
100
101 unwatch() {
102 this.watcher && this.watcher.close();
103 }
104
105 updateGatewayConfig(modifier) {
106 return this._updateConfigFile('gateway', modifier);
107 }
108
109 _updateConfigFile(type, modifier) {
110 const configType = this.configTypes[type];
111 const path = this[configType.pathProperty];
112
113 return readFile(path, 'utf8').then(data => {
114 const json = yamlOrJson.load(data);
115 const result = modifier(json);
116 const text = yamlOrJson.dump(result);
117 const candidateConfiguration = yamlOrJson.load(envReplace(String(text), process.env));
118
119 const { isValid, error } = configType.validator(candidateConfiguration);
120
121 if (!isValid) {
122 const e = new Error(error);
123 e.code = 'INVALID_CONFIG';
124 throw e;
125 }
126
127 try {
128 /*
129 This is really bad. It means we have circular dependencies that's a code smell, no matter what. This needs
130 to be refactored as soon as possible.
131 */
132 const { policies } = require('../policies');
133 for (const pipelineName in candidateConfiguration.pipelines) {
134 const pipeline = candidateConfiguration.pipelines[pipelineName];
135
136 pipeline.policies.forEach(policy => {
137 const policyName = Object.keys(policy)[0];
138 const policyDefinition = policies[policyName];
139
140 let policySteps = policy[policyName];
141
142 if (!policySteps) {
143 policySteps = [];
144 } else if (!Array.isArray(policySteps)) {
145 policySteps = [policySteps];
146 }
147
148 for (const step of policySteps) {
149 const { isValid, error } = schemas.validate(policyDefinition.schema.$id, step.action || {});
150 if (!isValid) {
151 throw new Error(error);
152 }
153 }
154 });
155 }
156
157 return writeFile(path, text);
158 } catch (err) {
159 log.error(`Invalid pipelines configuration: ${err}`);
160 err.code = 'INVALID_CONFIG';
161
162 throw err;
163 }
164 });
165 }
166}
167
168// Kindly borrowed from https://github.com/macbre/optimist-config-file/blob/master/lib/envvar-replace.js
169// Thanks a lot guys 🙌
170
171function envReplace(str, vars) {
172 return str.replace(/\$?\$\{([A-Za-z0-9_]+)(:-(.*?))?\}/g, function (varStr, varName, _, defValue) {
173 // Handle escaping:
174 if (varStr.indexOf('$$') === 0) {
175 return varStr;
176 }
177 // Handle simple variable replacement:
178 if (vars.hasOwnProperty(varName)) {
179 log.debug(`${varName} replaced in configuration file`);
180 return vars[varName];
181 }
182 // Handle default values:
183 if (defValue) {
184 log.debug(`${varName} replaced with default value in configuration file`);
185 return defValue;
186 }
187 log.warn(`Unknown variable: ${varName}. Returning null.`);
188 return null;
189 });
190};
191
192module.exports = Config;