Dynamic datasources.json from ENV and config.json

Let environment variables override configuration set by config.json
and/or app.set()

Behavior changes
- datasources.json now support dynamic configuration through
  env-vars and config.json
- component-config.json will first consider env-var
  for resolving dynamic conf, then fallback to config.json
- middleware.json will first consider env-var for resolving
  dynamic conf, then fallback to config.json
- for all the dynamic confg, unresolved conf will return as `undefined`

Example:

Consider the following server/datasources.json
```
{
  "mysql" : {
    "name" : "mysql_db",
    "host" : "${MYSQL_DB_HOST}",
    ...
  }
}
```

Now you can provide the parameter through
an environment variable:

```
$ MYSQL_DB_HOST=127.0.0.1 node .
```

or you can set the value in server/config.json

```
{
  "MYSQL_DB_HOST": "127.0.0.1"
}
```
This commit is contained in:
David Cheung 2016-03-28 15:46:01 -04:00
parent b3e5f23865
commit 4a815deb27
2 changed files with 182 additions and 7 deletions

View File

@ -171,6 +171,10 @@ function applyAppConfig(app, instructions) {
function setupDataSources(app, instructions) {
forEachKeyedObject(instructions.dataSources, function(key, obj) {
var opts = {
useEnvVars: true,
};
obj = getUpdatedConfigObject(app, obj, opts);
app.dataSource(key, obj);
});
}
@ -330,24 +334,42 @@ function setupMiddleware(app, instructions) {
}
assert(typeof factory === 'function',
'Middleware factory must be a function');
data.config = getUpdatedConfigObject(app, data.config);
var opts = {
useEnvVars: true,
};
data.config = getUpdatedConfigObject(app, data.config, opts);
app.middlewareFromConfig(factory, data.config);
});
}
function getUpdatedConfigObject(app, config) {
function getUpdatedConfigObject(app, config, opts) {
var DYNAMIC_CONFIG_PARAM = /\$\{(\w+)\}$/;
var useEnvVars = opts && opts.useEnvVars;
function getConfigVariable(param) {
var configVariable = param;
var match = configVariable.match(DYNAMIC_CONFIG_PARAM);
if (match) {
var appValue = app.get(match[1]);
if (appValue !== undefined) {
var varName = match[1];
if (useEnvVars && process.env[varName] !== undefined) {
debug('Dynamic Configuration: Resolved via process.env: %s as %s',
process.env[varName], param);
configVariable = process.env[varName];
} else if (app.get(varName) !== undefined) {
debug('Dynamic Configuration: Resolved via app.get(): %s as %s',
app.get(varName), param);
var appValue = app.get(varName);
configVariable = appValue;
} else {
console.warn('%s does not resolve to a valid value. ' +
'"%s" must be resolvable by app.get().', param, match[1]);
// previously it returns the original string such as "${restApiRoot}"
// it will now return `undefined`, for the use case of
// dynamic datasources url:`undefined` to fallback to other parameters
configVariable = undefined;
console.warn('%s does not resolve to a valid value, returned as %s. ' +
'"%s" must be resolvable in Environment variable or by app.get().',
param, configVariable, varName);
debug('Dynamic Configuration: Cannot resolve variable for `%s`, ' +
'returned as %s', varName, configVariable);
}
}
return configVariable;
@ -394,7 +416,10 @@ function setupComponents(app, instructions) {
instructions.components.forEach(function(data) {
debug('Configuring component %j', data.sourceFile);
var configFn = require(data.sourceFile);
data.config = getUpdatedConfigObject(app, data.config);
var opts = {
useEnvVars: true,
};
data.config = getUpdatedConfigObject(app, data.config, opts);
configFn(app, data.config);
});
}

View File

@ -442,6 +442,10 @@ describe('executor', function() {
});
describe('with middleware.json', function() {
beforeEach(function() {
delete process.env.restApiRoot;
});
it('should parse a simple config variable', function(done) {
boot.execute(app, simpleMiddlewareConfig('routes',
{ path: '${restApiRoot}' }
@ -454,6 +458,36 @@ describe('executor', function() {
});
});
it('should parse simple config variable from env var', function(done) {
process.env.restApiRoot = '/url-from-env-var';
boot.execute(app, simpleMiddlewareConfig('routes',
{ path: '${restApiRoot}' }
));
supertest(app).get('/url-from-env-var').end(function(err, res) {
if (err) return done(err);
expect(res.body.path).to.equal('/url-from-env-var');
done();
});
});
it('dynamic variable from `env var` should have' +
' precedence over app.get()', function(done) {
process.env.restApiRoot = '/url-from-env-var';
var bootInstructions;
bootInstructions = simpleMiddlewareConfig('routes',
{ path: '${restApiRoot}' });
bootInstructions.config = { restApiRoot: '/url-from-config' };
boot.execute(app, someInstructions(bootInstructions));
supertest(app).get('/url-from-env-var').end(function(err, res) {
if (err) return done(err);
expect(app.get('restApiRoot')).to.equal('/url-from-config');
expect(res.body.path).to.equal('/url-from-env-var');
done();
});
});
it('should parse multiple config variables', function(done) {
boot.execute(app, simpleMiddlewareConfig('routes',
{ path: '${restApiRoot}', env: '${env}' }
@ -556,6 +590,11 @@ describe('executor', function() {
});
describe('with component-config.json', function() {
beforeEach(function() {
delete process.env.DYNAMIC_ENVVAR;
delete process.env.DYNAMIC_VARIABLE;
});
it('should parse a simple config variable', function(done) {
boot.execute(app, simpleComponentConfig(
{ path: '${restApiRoot}' }
@ -568,6 +607,46 @@ describe('executor', function() {
});
});
it('should parse config from `env-var` and `config`', function(done) {
var bootInstructions = simpleComponentConfig(
{
path: '${restApiRoot}',
fromConfig: '${DYNAMIC_CONFIG}',
fromEnvVar: '${DYNAMIC_ENVVAR}',
}
);
// result should get value from config.json
bootInstructions.config['DYNAMIC_CONFIG'] = 'FOOBAR-CONFIG';
// result should get value from env var
process.env.DYNAMIC_ENVVAR = 'FOOBAR-ENVVAR';
boot.execute(app, bootInstructions);
supertest(app).get('/component').end(function(err, res) {
if (err) return done(err);
expect(res.body.fromConfig).to.equal('FOOBAR-CONFIG');
expect(res.body.fromEnvVar).to.equal('FOOBAR-ENVVAR');
done();
});
});
it('`env-var` should have precedence over `config`', function(done) {
var key = 'DYNAMIC_VARIABLE';
var bootInstructions = simpleComponentConfig({
path: '${restApiRoot}',
isDynamic: '${' + key + '}',
});
bootInstructions.config[key] = 'should be overwritten';
process.env[key] = 'successfully overwritten';
boot.execute(app, bootInstructions);
supertest(app).get('/component').end(function(err, res) {
if (err) return done(err);
expect(res.body.isDynamic).to.equal('successfully overwritten');
done();
});
});
it('should parse multiple config variables', function(done) {
boot.execute(app, simpleComponentConfig(
{ path: '${restApiRoot}', env: '${env}' }
@ -809,6 +888,77 @@ describe('executor', function() {
});
});
});
describe('dynamic configuration for datasources.json', function() {
beforeEach(function() {
delete process.env.DYNAMIC_HOST;
delete process.env.DYNAMIC_PORT;
});
it('should convert dynamic variable for datasource', function(done) {
var datasource = {
mydb: {
host: '${DYNAMIC_HOST}',
port: '${DYNAMIC_PORT}',
},
};
var bootInstructions = { dataSources: datasource };
process.env.DYNAMIC_PORT = '10007';
process.env.DYNAMIC_HOST = '123.321.123.132';
boot.execute(app, someInstructions(bootInstructions), function() {
expect(app.datasources.mydb.settings.host).to.equal('123.321.123.132');
expect(app.datasources.mydb.settings.port).to.equal('10007');
done();
});
});
it('should resolve dynamic config via app.get()', function(done) {
var datasource = {
mydb: { host: '${DYNAMIC_HOST}' },
};
var bootInstructions = {
config: { DYNAMIC_HOST: '127.0.0.4' },
dataSources: datasource,
};
boot.execute(app, someInstructions(bootInstructions), function() {
expect(app.get('DYNAMIC_HOST')).to.equal('127.0.0.4');
expect(app.datasources.mydb.settings.host).to.equal(
'127.0.0.4');
done();
});
});
it('should take ENV precedence over config.json', function(done) {
process.env.DYNAMIC_HOST = '127.0.0.2';
var datasource = {
mydb: { host: '${DYNAMIC_HOST}' },
};
var bootInstructions = {
config: { DYNAMIC_HOST: '127.0.0.3' },
dataSources: datasource,
};
boot.execute(app, someInstructions(bootInstructions), function() {
expect(app.get('DYNAMIC_HOST')).to.equal('127.0.0.3');
expect(app.datasources.mydb.settings.host).to.equal('127.0.0.2');
done();
});
});
it('empty dynamic conf should resolve as `undefined`', function(done) {
var datasource = {
mydb: { host: '${DYNAMIC_HOST}' },
};
var bootInstructions = { dataSources: datasource };
boot.execute(app, someInstructions(bootInstructions), function() {
expect(app.get('DYNAMIC_HOST')).to.be.undefined();
expect(app.datasources.mydb.settings.host).to.be.undefined();
done();
});
});
});
});
function simpleMiddlewareConfig(phase, paths, params) {