1 | const fs = require('fs');
|
2 | const util = require('util');
|
3 | const readFile = util.promisify(fs.readFile);
|
4 | const writeFile = util.promisify(fs.writeFile);
|
5 | const path = require('path');
|
6 | const log = require('../logger').config;
|
7 | const chokidar = require('chokidar');
|
8 | const yamlOrJson = require('js-yaml');
|
9 | const eventBus = require('../eventBus');
|
10 | const schemas = require('../schemas');
|
11 |
|
12 | class 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 |
|
130 |
|
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 |
|
169 |
|
170 |
|
171 | function envReplace(str, vars) {
|
172 | return str.replace(/\$?\$\{([A-Za-z0-9_]+)(:-(.*?))?\}/g, function (varStr, varName, _, defValue) {
|
173 |
|
174 | if (varStr.indexOf('$$') === 0) {
|
175 | return varStr;
|
176 | }
|
177 |
|
178 | if (vars.hasOwnProperty(varName)) {
|
179 | log.debug(`${varName} replaced in configuration file`);
|
180 | return vars[varName];
|
181 | }
|
182 |
|
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 |
|
192 | module.exports = Config;
|