Merge pull request #177 from strongloop/dynamic-config

Dynamic datasources.json from ENV and config.json
This commit is contained in:
Miroslav Bajtoš 2016-04-07 10:26:41 +02:00
commit 707214cac4
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) {