@opuscapita/config
Advanced tools
Comparing version 3.1.7 to 3.1.11
@@ -1,3 +0,3 @@ | ||
const { ConfigClient } = require('./lib'); | ||
const {ConfigClient} = require('./lib'); | ||
module.exports = new ConfigClient(); |
const ConfigClientBase = require('./ConfigClientBase'); | ||
const crypto = require('crypto'); | ||
const Promise = require('bluebird'); | ||
const Cache = require('@opuscapita/cache'); | ||
const extend = require('extend'); | ||
@@ -20,10 +19,8 @@ const Logger = require('@opuscapita/logger'); | ||
*/ | ||
class ConfigClient extends ConfigClientBase | ||
{ | ||
constructor() | ||
{ | ||
super(); | ||
} | ||
class ConfigClient extends ConfigClientBase { | ||
constructor() { | ||
super(); | ||
} | ||
/** | ||
/** | ||
* Prepares all required data, connects to consul and starts all event processing. | ||
@@ -34,19 +31,20 @@ * | ||
*/ | ||
init(config) | ||
{ | ||
if(this.initialized) | ||
return Promise.resolve(this); | ||
init(config) { | ||
if (this.initialized) { | ||
return Promise.resolve(this); | ||
} | ||
config = extend(true, { }, ConfigClient.DefaultConfig, config); | ||
config = extend(true, { }, ConfigClient.DefaultConfig, config); | ||
const initConf = { | ||
config : config, | ||
logger : config.logger || new Logger({ context : { serviceName : this.serviceName, name: '@opuscapita/config' } }), | ||
cache : new Cache({ defaultExpire : Number.MAX_SAFE_INTEGER }) | ||
}; | ||
const initConf = { | ||
config: config, | ||
logger: config.logger || new Logger({context: {serviceName: this.serviceName, name: '@opuscapita/config'}}), | ||
}; | ||
return Promise.resolve(this._init(initConf)).catch(e => { this.logger.error(e.message, e); throw e; }); | ||
} | ||
return Promise.resolve(this._init(initConf)).catch(e => { | ||
this.logger.error(e.message, e); throw e; | ||
}); | ||
} | ||
/** | ||
/** | ||
* Closes all connections and cleans up the whole internal state. | ||
@@ -56,8 +54,9 @@ * | ||
*/ | ||
dispose() | ||
{ | ||
return Promise.resolve(this._dispose()).catch(e => { this.logger.error(e.message, e); throw e; }); | ||
} | ||
dispose() { | ||
return Promise.resolve(this._dispose()).catch(e => { | ||
this.logger.error(e.message, e); throw e; | ||
}); | ||
} | ||
/** | ||
/** | ||
* Gets the full qualified name for a key including the prefixed [serviceName]{@link serviceName}. | ||
@@ -67,8 +66,7 @@ * @param {string} key - Key to check. | ||
*/ | ||
getFullKey(key) | ||
{ | ||
return `${this.serviceName}/${key}`; | ||
} | ||
getFullKey(key) { | ||
return `${this.serviceName}/${key}`; | ||
} | ||
/** | ||
/** | ||
* Checks whenever the passed key follows the rules for key naming and is therefor a valid key. | ||
@@ -83,8 +81,7 @@ * Requesting an invalid key from the system will throw an error. | ||
*/ | ||
checkKey(key) | ||
{ | ||
return key ? /^([0-9a-zA-Z-]+\/?)+[0-9a-zA-Z-]+$/.test(this.getFullKey(key)) : false; | ||
} | ||
checkKey(key) { | ||
return key ? /^([0-9a-zA-Z-]+\/?)+[0-9a-zA-Z-]+$/.test(this.getFullKey(key)) : false; | ||
} | ||
/** | ||
/** | ||
* Checks whenever the passed prefix follows the rules for key prefixing. | ||
@@ -99,8 +96,7 @@ * Requesting an invalid prefix from the system will throw an error. | ||
*/ | ||
checkKeyPrefix(prefix) | ||
{ | ||
return prefix ? /^([0-9a-zA-Z-]+\/)+$/.test(prefix) : false; | ||
} | ||
checkKeyPrefix(prefix) { | ||
return prefix ? /^([0-9a-zA-Z-]+\/)+$/.test(prefix) : false; | ||
} | ||
/** | ||
/** | ||
* Encrypts a given object using an internal secret. | ||
@@ -110,9 +106,8 @@ * @param {object} data Object to be encrypted. | ||
*/ | ||
encryptData(data) | ||
{ | ||
const cipher = crypto.createCipheriv('aes-256-cbc', this.secret, '9f3663cc2e8a47dd'); | ||
return cipher.update(JSON.stringify(data), 'utf8', 'base64') + cipher.final('base64'); | ||
} | ||
encryptData(data) { | ||
const cipher = crypto.createCipheriv('aes-256-cbc', this.secret, '9f3663cc2e8a47dd'); | ||
return cipher.update(JSON.stringify(data), 'utf8', 'base64') + cipher.final('base64'); | ||
} | ||
/** | ||
/** | ||
* Decrypts a given string using an internal secret. | ||
@@ -122,9 +117,8 @@ * @param {string} encrypted Encrypted data. | ||
*/ | ||
decryptData(encrypted) | ||
{ | ||
const cipher = crypto.createDecipheriv('aes-256-cbc', this.secret, '9f3663cc2e8a47dd'); | ||
return JSON.parse(cipher.update(encrypted, 'base64', 'utf8') + cipher.final('utf8')); | ||
} | ||
decryptData(encrypted) { | ||
const cipher = crypto.createDecipheriv('aes-256-cbc', this.secret, '9f3663cc2e8a47dd'); | ||
return JSON.parse(cipher.update(encrypted, 'base64', 'utf8') + cipher.final('utf8')); | ||
} | ||
/** | ||
/** | ||
* Gets a single or a list of values from consul's key-value store. | ||
@@ -147,28 +141,26 @@ * To get a list of values you can either use key-prefixing or an array of keys or key-prefixes. | ||
*/ | ||
getProperty(keyOrPrefix, recursive = false, silent = false) | ||
{ | ||
if(Array.isArray(keyOrPrefix)) | ||
return Promise.all(keyOrPrefix.map(k => this.getProperty(k, recursive, silent))); | ||
getProperty(keyOrPrefix, recursive = false, silent = false) { | ||
if (Array.isArray(keyOrPrefix)) { | ||
return Promise.all(keyOrPrefix.map(k => this.getProperty(k, recursive, silent))); | ||
} | ||
const keyName = this.getFullKey(keyOrPrefix); | ||
const keyName = this.getFullKey(keyOrPrefix); | ||
const isValidKey = !recursive && this.checkKey(keyOrPrefix); | ||
const isValidPrefix = recursive && this.checkKeyPrefix(keyOrPrefix); | ||
const isValidKey = !recursive && this.checkKey(keyOrPrefix); | ||
const isValidPrefix = recursive && this.checkKeyPrefix(keyOrPrefix); | ||
if(isValidKey || isValidPrefix) | ||
{ | ||
const task = () => retry(() => this._getConsulValues(keyName, recursive), { max_tries : 5, interval : 500 }); | ||
if (isValidKey || isValidPrefix) { | ||
const task = () => retry(() => this._getConsulValues(keyName, recursive), {max_tries: 5, interval: 500}); | ||
if(silent) | ||
return task().catch(e => this.logger.warn('Cannot get property', e)); | ||
else | ||
return task(); | ||
} | ||
else | ||
{ | ||
return Promise.reject(new Error('The passed key or prefix is invalid: ' + keyOrPrefix)); | ||
} | ||
if (silent) { | ||
return task().catch(e => this.logger.warn('Cannot get property', e)); | ||
} else { | ||
return task(); | ||
} | ||
} else { | ||
return Promise.reject(new Error(`The passed key or prefix is invalid: ${keyOrPrefix}`)); | ||
} | ||
} | ||
/** | ||
/** | ||
* Sets a value to consul's key-value store. | ||
@@ -179,11 +171,11 @@ * @param {string} key - Key to set. Will automatically get prefixed with [serviceName]{@link serviceName}. | ||
*/ | ||
setProperty(key, value) | ||
{ | ||
if(this.checkKey(key)) | ||
return Promise.resolve(this._setConsulValue(this.getFullKey(key), value)); | ||
else | ||
return Promise.reject(new Error('The passed key is invalid: ' + key)); | ||
setProperty(key, value) { | ||
if (this.checkKey(key)) { | ||
return Promise.resolve(this._setConsulValue(this.getFullKey(key), value)); | ||
} else { | ||
return Promise.reject(new Error(`The passed key is invalid: ${key}`)); | ||
} | ||
} | ||
/** | ||
/** | ||
* Removes a single value or a list of values from consul's key-value store. | ||
@@ -195,28 +187,26 @@ * @param {string|string[]} keyOrPrefix - Key to remove. Will automatically get prefixed with [serviceName]{@link serviceName}. | ||
*/ | ||
deleteProperty(keyOrPrefix, recursive = false, silent = false) | ||
{ | ||
if(Array.isArray(keyOrPrefix)) | ||
return Promise.all(keyOrPrefix.map(k => this.deleteProperty(k, recursive))); | ||
deleteProperty(keyOrPrefix, recursive = false, silent = false) { | ||
if (Array.isArray(keyOrPrefix)) { | ||
return Promise.all(keyOrPrefix.map(k => this.deleteProperty(k, recursive))); | ||
} | ||
const keyName = this.getFullKey(keyOrPrefix); | ||
const keyName = this.getFullKey(keyOrPrefix); | ||
const isValidKey = !recursive && this.checkKey(keyOrPrefix); | ||
const isValidPrefix = recursive && this.checkKeyPrefix(keyOrPrefix); | ||
const isValidKey = !recursive && this.checkKey(keyOrPrefix); | ||
const isValidPrefix = recursive && this.checkKeyPrefix(keyOrPrefix); | ||
if(isValidKey || isValidPrefix) | ||
{ | ||
const task = () => retry(() => this._deleteConsulValue(keyName, recursive), { max_tries : 5, interval : 500 }); | ||
if (isValidKey || isValidPrefix) { | ||
const task = () => retry(() => this._deleteConsulValue(keyName, recursive), {max_tries: 5, interval: 500}); | ||
if(silent) | ||
return task().catch(e => this.logger.warn('Cannot delete property', e)); | ||
else | ||
return task(); | ||
} | ||
else | ||
{ | ||
return Promise.reject(new Error('The passed key or prefix is invalid: ' + keyOrPrefix)); | ||
} | ||
if (silent) { | ||
return task().catch(e => this.logger.warn('Cannot delete property', e)); | ||
} else { | ||
return task(); | ||
} | ||
} else { | ||
return Promise.reject(new Error(`The passed key or prefix is invalid: ${keyOrPrefix}`)); | ||
} | ||
} | ||
/** | ||
/** | ||
* Gets an endpoint from consul's service registry. | ||
@@ -232,18 +222,20 @@ * Be aware, that this method only returns service endpoints that are marked *healthy* by consul. | ||
*/ | ||
getEndPoint(serviceName, silent = false) | ||
{ | ||
getEndPoint(serviceName, silent = false) { | ||
if (Array.isArray(serviceName)) { | ||
return Promise.all(serviceName.map(k => this.getEndPoint(k, silent))); | ||
} | ||
if(Array.isArray(serviceName)) | ||
return Promise.all(serviceName.map(k => this.getEndPoint(k, silent))); | ||
const task = () => this._getConsulEndpoint(serviceName); | ||
const task = () => this._getConsulEndpoint(serviceName); | ||
if(silent) | ||
return task().catch(e => this.logger.warn('Cannot get endpoint', e)); | ||
else | ||
return retry(task, { max_tries: this.config.retryCount, interval: this.config.retryTimeout }) | ||
.catch(e => { this.logger.warn('Cannot get endpoint', e); throw e; }); | ||
if (silent) { | ||
return task().catch(e => this.logger.warn('Cannot get endpoint', e)); | ||
} else { | ||
return retry(task, {max_tries: this.config.retryCount, interval: this.config.retryTimeout}). | ||
catch(e => { | ||
this.logger.warn('Cannot get endpoint', e); throw e; | ||
}); | ||
} | ||
} | ||
/** | ||
/** | ||
* Gets a list of endpoints from consul's service registry. | ||
@@ -259,18 +251,22 @@ * Be aware, that this method only returns service endpoints that are marked *healthy* by consul. | ||
*/ | ||
getEndPoints(serviceName, silent = false) | ||
{ | ||
getEndPoints(serviceName, silent = false) { | ||
if (Array.isArray(serviceName)) { | ||
return Promise.all(serviceName.map(k => this.getEndPoints(k, silent))); | ||
} | ||
if(Array.isArray(serviceName)) | ||
return Promise.all(serviceName.map(k => this.getEndPoints(k, silent))); | ||
const task = () => this._getConsulEndpoints(serviceName); | ||
const task = () => this._getConsulEndpoints(serviceName); | ||
if(silent) | ||
return task().catch(e => { this.logger.warn('Cannot get endpoints', e); return [ ]; }); | ||
else | ||
return retry(task, { max_tries: this.config.retryCount, interval: this.config.retryTimeout }) | ||
.catch(e => { this.logger.warn('Cannot get endpoint', e); throw e; }); | ||
if (silent) { | ||
return task().catch(e => { | ||
this.logger.warn('Cannot get endpoints', e); return []; | ||
}); | ||
} else { | ||
return retry(task, {max_tries: this.config.retryCount, interval: this.config.retryTimeout}). | ||
catch(e => { | ||
this.logger.warn('Cannot get endpoint', e); throw e; | ||
}); | ||
} | ||
} | ||
/** | ||
/** | ||
* Gets a single or a list of values from consul's key-value store. Alias for [getProperty()]{@link getProperty}. | ||
@@ -294,8 +290,7 @@ * To get a list of values you can either use key-prefixing or and array of keys and key-prefixes. | ||
*/ | ||
get(keyOrPrefix, recursive = false, silent = false) | ||
{ | ||
return this.getProperty(keyOrPrefix, recursive, silent); | ||
} | ||
get(keyOrPrefix, recursive = false, silent = false) { | ||
return this.getProperty(keyOrPrefix, recursive, silent); | ||
} | ||
/** | ||
/** | ||
* Sets a value to consul's key-value store. Alias for [setProperty()]{@link setProperty}. | ||
@@ -307,8 +302,7 @@ * @deprecated since v3.1.5 - use setProperty instead | ||
*/ | ||
set(key, value) | ||
{ | ||
return this.setProperty(key, value); | ||
} | ||
set(key, value) { | ||
return this.setProperty(key, value); | ||
} | ||
/** | ||
/** | ||
* Removes a value from consul's key-value store. Alias for [deleteProperty()]{@link deleteProperty}. | ||
@@ -321,8 +315,7 @@ * @deprecated since v3.1.5 - use deleteProperty instead | ||
*/ | ||
delete(key, recursive = false, silent = false) | ||
{ | ||
return this.deleteProperty(key, recursive, silent); | ||
} | ||
delete(key, recursive = false, silent = false) { | ||
return this.deleteProperty(key, recursive, silent); | ||
} | ||
/** | ||
/** | ||
* Gets an encrypted value from consul's key-value store. | ||
@@ -336,14 +329,15 @@ * This method uses retry in order to bypass problems with network connections, service latencies or | ||
*/ | ||
getPassword(keyOrPrefix, recursive = false, silent = false) | ||
{ | ||
return this.get(keyOrPrefix, recursive, silent).then(values => | ||
{ | ||
if(Array.isArray(keyOrPrefix) || recursive) | ||
return Object.keys(values).reduce((all, key) => { all[key] = this.decryptData(values[key]); return all }, { }); | ||
getPassword(keyOrPrefix, recursive = false, silent = false) { | ||
return this.get(keyOrPrefix, recursive, silent).then(values => { | ||
if (Array.isArray(keyOrPrefix) || recursive) { | ||
return Object.keys(values).reduce((all, key) => { | ||
all[key] = this.decryptData(values[key]); return all; | ||
}, { }); | ||
} | ||
return this.decryptData(values); | ||
}); | ||
} | ||
return this.decryptData(values); | ||
}); | ||
} | ||
/** | ||
/** | ||
* Sets an encrypted value to consul's key-value store. | ||
@@ -354,9 +348,8 @@ * @param {string} key - Key to set. Will automatically get prefixed with [serviceName]{@link serviceName}. | ||
*/ | ||
setPassword(key, value) | ||
{ | ||
const encrypted = this.encryptData(value); | ||
return this.set(key, encrypted); | ||
}; | ||
setPassword(key, value) { | ||
const encrypted = this.encryptData(value); | ||
return this.set(key, encrypted); | ||
} | ||
/** | ||
/** | ||
* Waits a certain time for a list of endpoints to be available. If all endpoints are available before the passed timeout is reached, this method will return a promise | ||
@@ -369,6 +362,9 @@ * resolving to *true*. Otherwise the returned promise gets rejected with an error. | ||
*/ | ||
waitForEndpoints(serviceNames, timeout = 5000) | ||
{ | ||
return retry(() => this.getEndPoint(serviceNames, false).then(() => true), { interval : 500, backOff : 1.2, max_interval : timeout, max_tries : Number.MAX_SAFE_INTEGER, timeout : timeout }); | ||
} | ||
waitForEndpoints(serviceNames, timeout = 5000) { | ||
return retry( | ||
() => this.getEndPoint(serviceNames, false). | ||
then(() => true), | ||
{interval: 500, backOff: 1.2, max_interval: timeout, max_tries: Number.MAX_SAFE_INTEGER, timeout: timeout} | ||
); | ||
} | ||
} | ||
@@ -387,10 +383,10 @@ | ||
ConfigClient.DefaultConfig = { | ||
host : 'consul', | ||
port : 8500, | ||
retryCount : 50, | ||
retryTimeout : 1000, | ||
logger : null, | ||
serviceSecretPath : `/run/secrets/${ConfigClientBase.serviceName}-consul-key` | ||
} | ||
host: 'consul', | ||
port: 8500, | ||
retryCount: 50, | ||
retryTimeout: 1000, | ||
logger: null, | ||
serviceSecretPath: `/run/secrets/${ConfigClientBase.serviceName}-consul-key` | ||
}; | ||
module.exports = ConfigClient; |
@@ -7,3 +7,4 @@ const EventEmitter = require('events'); | ||
const cwd = process.cwd(); | ||
const serviceName = cwd.slice(cwd.lastIndexOf('/') + 1); | ||
const serviceName = cwd.slice(cwd.lastIndexOf('/') + 1); | ||
const NodeCache = require('node-cache'); | ||
@@ -17,22 +18,19 @@ /** | ||
*/ | ||
class ConfigClientBase extends EventEmitter | ||
{ | ||
constructor() | ||
{ | ||
super(); | ||
class ConfigClientBase extends EventEmitter { | ||
constructor() { | ||
super(); | ||
this.initialized = false; | ||
this.setMaxListeners(100); | ||
} | ||
this.initialized = false; | ||
this.setMaxListeners(100); | ||
} | ||
/** | ||
/** | ||
* Gets the name of the current service. It simply takes the name of the inner least directory the process is running in. | ||
* @returns {string} Service name. | ||
*/ | ||
get serviceName() | ||
{ | ||
return serviceName; | ||
} | ||
get serviceName() { | ||
return serviceName; | ||
} | ||
/** | ||
/** | ||
* Gets a promise containing a list of all services available in consul. | ||
@@ -42,164 +40,153 @@ * | ||
*/ | ||
get allServices() | ||
{ | ||
return Promise.resolve((async () => this.consul && Object.keys(await this.consul.catalog.service.list()))()); | ||
get allServices() { | ||
return Promise.resolve((async () => this.consul && Object.keys(await this.consul.catalog.service.list()))()); | ||
} | ||
async _init({config, logger}) { | ||
if (this.initialized) { | ||
if (config && logger) { | ||
logger.warn('ConfigClientBase already initialized. Ignoring new config.'); | ||
} | ||
return this; | ||
} | ||
async _init({ config, logger, cache }) | ||
{ | ||
if(this.initialized) | ||
return this; | ||
this.config = config; | ||
this.logger = logger; | ||
this.cache = new NodeCache({ | ||
stdTTL: 0 // 0 = unlimited | ||
}); | ||
this.epWatches = {}; | ||
this.keyWatches = {}; | ||
this.knownKeys = {}; | ||
this.config = config; | ||
this.logger = logger; | ||
this.cache = cache; | ||
this.epWatches = {}; | ||
this.keyWatches = {}; | ||
this.knownKeys = {}; | ||
const [secret, consul] = await Promise.all([ | ||
this._loadEncryptionKey(this.config.serviceSecretPath), | ||
this._connect(this.config) | ||
]); | ||
const [ secret, consul ] = await Promise.all([ | ||
this._loadEncryptionKey(this.config.serviceSecretPath), | ||
this._connect(this.config) | ||
]); | ||
await Promise.all([ | ||
// this._watchServices(consul, (allServices) => Promise.all(allServices.map((serviceName) => this._watchServiceHealth(consul, serviceName))).catch(e => this.logger.error(e))), | ||
this._watchKey(consul, `${this.serviceName}/`) | ||
]); | ||
await Promise.all([ | ||
//this._watchServices(consul, (allServices) => Promise.all(allServices.map((serviceName) => this._watchServiceHealth(consul, serviceName))).catch(e => this.logger.error(e))), | ||
this._watchKey(consul, this.serviceName + '/') | ||
]); | ||
this.secret = secret; | ||
this.consul = consul; | ||
this.initialized = true; | ||
this.secret = secret; | ||
this.consul = consul; | ||
this.initialized = true; | ||
return this; | ||
} | ||
return this; | ||
async _dispose() { | ||
this.config = null; | ||
this.logger = null; | ||
this.cache = null; | ||
for (const k in this.epWatches) { | ||
this.epWatches[k].removeAllListeners('change').removeAllListeners('error'); | ||
this.epWatches[k].end(); | ||
} | ||
async _dispose() | ||
{ | ||
this.config = null; | ||
this.logger = null; | ||
this.cache = null; | ||
for (const k in this.keyWatches) { | ||
this.keyWatches[k].removeAllListeners('change').removeAllListeners('error'); | ||
this.keyWatches[k].end(); | ||
} | ||
for(const k in this.epWatches) | ||
{ | ||
this.epWatches[k].removeAllListeners('change').removeAllListeners('error'); | ||
this.epWatches[k].end(); | ||
} | ||
this.epWatches = {}; | ||
this.keyWatches = {}; | ||
this.knownKeys = {}; | ||
for(const k in this.keyWatches) | ||
{ | ||
this.keyWatches[k].removeAllListeners('change').removeAllListeners('error'); | ||
this.keyWatches[k].end(); | ||
} | ||
this.secret = null; | ||
this.consul = null; | ||
this.initialized = false; | ||
} | ||
this.epWatches = {}; | ||
this.keyWatches = {}; | ||
this.knownKeys = {}; | ||
async _connect({host, port, retryCount, retryTimeout}) { | ||
this.logger.info('Connecting to consul', {host, port}); | ||
this.secret = null; | ||
this.consul = null; | ||
this.initialized = false; | ||
const connection = consul({ | ||
host: host, | ||
port: port, | ||
promisify: true | ||
}); | ||
try { | ||
await retry(() => connection.status.leader(), {max_tries: retryCount, interval: retryTimeout}); | ||
this.logger.info('Connected to consul.'); | ||
} catch (e) { | ||
this.logger.error('Could not connect to consul', {e}); | ||
throw e; | ||
} | ||
async _connect({ host, port, retryCount, retryTimeout }) | ||
{ | ||
this.logger.info('Connecting to consul', {host, port}); | ||
return connection; | ||
} | ||
const connection = consul({ | ||
host : host, | ||
port : port, | ||
promisify : true | ||
}); | ||
async _loadEncryptionKey(serviceSecretPath) { | ||
this.logger.info('Reading encryption key', {serviceSecretPath}); | ||
try | ||
{ | ||
await retry(() => connection.status.leader(), { max_tries : retryCount, interval : retryTimeout }); | ||
this.logger.info('Connected to consul.'); | ||
} | ||
catch(e) | ||
{ | ||
this.logger.error('Could not connect to consul', {e}); | ||
throw e; | ||
} | ||
let secret = 'b2eb88a9cd0e45cf890a1505df17a718'; | ||
return connection; | ||
try { | ||
secret = fs.readFileSync(serviceSecretPath, 'utf8').trim(); | ||
} catch (e) { | ||
this.logger.warn('Cannot read encryption key - falling back to default key.', {serviceSecretPath}); | ||
} | ||
async _loadEncryptionKey(serviceSecretPath) | ||
{ | ||
this.logger.info('Reading encryption key', {serviceSecretPath}); | ||
return secret; | ||
} | ||
let secret = 'b2eb88a9cd0e45cf890a1505df17a718'; | ||
// async _watchServices(consul, callback) | ||
// { | ||
// return new Promise((resolve, reject) => | ||
// { | ||
// const watch = consul.watch({ method : consul.catalog.service.list, maxAttempts : 3 }); | ||
try | ||
{ | ||
secret = fs.readFileSync(serviceSecretPath, 'utf8').trim(); | ||
} | ||
catch(e) | ||
{ | ||
this.logger.warn('Cannot read encryption key - falling back to default key.', {serviceSecretPath}); | ||
} | ||
// this.epWatches['all-services'] = watch; | ||
return secret; | ||
} | ||
// watch.on('change', (data, res) => callback(Object.keys(data))); | ||
// watch.on('error', (err) => { console.error('Error watching services.', err); reject(err) }); | ||
// async _watchServices(consul, callback) | ||
// { | ||
// return new Promise((resolve, reject) => | ||
// { | ||
// const watch = consul.watch({ method : consul.catalog.service.list, maxAttempts : 3 }); | ||
// resolve(watch); | ||
// }); | ||
// } | ||
// this.epWatches['all-services'] = watch; | ||
async _watchServiceHealth(consul, serviceName) { | ||
return new Promise((resolve, reject) => { | ||
const cacheKey = `${this.serviceName}.ep.${serviceName}`; | ||
// watch.on('change', (data, res) => callback(Object.keys(data))); | ||
// watch.on('error', (err) => { console.error('Error watching services.', err); reject(err) }); | ||
if (this.epWatches[cacheKey]) { | ||
return resolve(this.epWatches[cacheKey]); | ||
} | ||
// resolve(watch); | ||
// }); | ||
// } | ||
const watch = consul.watch({method: consul.health.service, options: {service: serviceName}}); | ||
async _watchServiceHealth(consul, serviceName) | ||
{ | ||
return new Promise((resolve, reject) => | ||
{ | ||
const cacheKey = this.serviceName + '.ep.' + serviceName; | ||
this.epWatches[cacheKey] = watch; | ||
if(this.epWatches[cacheKey]) | ||
return resolve(this.epWatches[cacheKey]); | ||
const mapService = (s) => ({ | ||
host: s.Service.ServiceAddress || s.Service.Address || s.Node.Address, | ||
port: s.Service.Port, | ||
modifyIndex: s.Service.ModifyIndex | ||
}); | ||
const watch = consul.watch({ method : consul.health.service, options : { service : serviceName } }); | ||
watch.on('error', (err) => { | ||
// This is not crititcal as the watch will be automatically restarted/retried. | ||
console.log(`Error watching service "${serviceName}". Retrying...`, err); | ||
}); | ||
this.epWatches[cacheKey] = watch; | ||
watch.on('end', () => { | ||
console.warn(`The watch for service "${serviceName}" has been stopped.`); this._disposeServiceHealthWatch(serviceName); | ||
}); | ||
watch.on('change', async (items) => { | ||
if (items.length === 0) { | ||
await this.cache.del(serviceName); | ||
this.emit('endpointChanged', serviceName, null); | ||
} else { | ||
const newEndpoints = items.map((service) => mapService(service)); | ||
const oldEndpoints = await this.cache.get(cacheKey); | ||
const hasChanged = oldEndpoints && | ||
oldEndpoints.filter(oe => newEndpoints.filter(ne => ne.modifyIndex > oe.modifyIndex).length); | ||
const mapService = (s) => ({ | ||
host : s.Service.ServiceAddress || s.Service.Address || s.Node.Address, | ||
port : s.Service.Port, | ||
modifyIndex : s.Service.ModifyIndex | ||
}); | ||
if (!hasChanged || (Array.isArray(hasChanged) && hasChanged.length > 0)) { | ||
await this.cache.set(cacheKey, newEndpoints); | ||
watch.on('error', (err) => | ||
{ | ||
// This is not crititcal as the watch will be automatically restarted/retried. | ||
console.log(`Error watching service "${serviceName}". Retrying...`, err); | ||
}); | ||
watch.on('end', () => { console.warn(`The watch for service "${serviceName}" has been stopped.`); this._disposeServiceHealthWatch(serviceName); }); | ||
watch.on('change', async (items) => | ||
{ | ||
if(items.length === 0) | ||
{ | ||
await this.cache.delete(serviceName); | ||
this.emit('endpointChanged', serviceName, null); | ||
} | ||
else | ||
{ | ||
const newEndpoints = items.map((service) => mapService(service)); | ||
const oldEndpoints = await this.cache.get(cacheKey); | ||
const hasChanged = oldEndpoints && oldEndpoints.filter(oe => newEndpoints.filter(ne => ne.modifyIndex > oe.modifyIndex).length); | ||
if(!hasChanged || (Array.isArray(hasChanged) && hasChanged.length > 0)) | ||
{ | ||
await this.cache.put(cacheKey, newEndpoints); | ||
/** | ||
/** | ||
* Change event for endpoint data. | ||
@@ -212,61 +199,59 @@ * | ||
*/ | ||
this.emit('endpointChanged', serviceName, await this.cache.get(cacheKey)); | ||
} | ||
} | ||
}); | ||
this.emit('endpointChanged', serviceName, await this.cache.get(cacheKey)); | ||
} | ||
} | ||
}); | ||
resolve(watch); | ||
}); | ||
} | ||
resolve(watch); | ||
}); | ||
} | ||
async _disposeServiceHealthWatch(serviceName) | ||
{ | ||
const cacheKey = this.serviceName + '.ep.' + serviceName; | ||
async _disposeServiceHealthWatch(serviceName) { | ||
const cacheKey = `${this.serviceName}.ep.${serviceName}`; | ||
if(this.epWatches[cacheKey]) | ||
{ | ||
this.epWatches[cacheKey].end(); | ||
this.epWatches[cacheKey].removeAllListeners('error').removeAllListeners('end').removeAllListeners('change'); | ||
if (this.epWatches[cacheKey]) { | ||
this.epWatches[cacheKey].end(); | ||
this.epWatches[cacheKey].removeAllListeners('error').removeAllListeners('end').removeAllListeners('change'); | ||
delete this.epWatches[cacheKey]; | ||
this.cache && await this.cache.delete(serviceName); | ||
delete this.epWatches[cacheKey]; | ||
this.cache && await this.cache.del(serviceName); | ||
this.emit('endpointChanged', serviceName, null); | ||
} | ||
this.emit('endpointChanged', serviceName, null); | ||
} | ||
} | ||
async _watchKey(consul, key) | ||
{ | ||
return new Promise((resolve, reject) => | ||
{ | ||
if(this.keyWatches[key]) | ||
return resolve(this.keyWatches[key]); | ||
async _watchKey(consul, key) { | ||
return new Promise((resolve, reject) => { | ||
if (this.keyWatches[key]) { | ||
return resolve(this.keyWatches[key]); | ||
} | ||
const watch = consul.watch({ method : consul.kv.keys, options : { key : key } }); | ||
const watch = consul.watch({method: consul.kv.keys, options: {key: key}}); | ||
this.keyWatches[key] = watch; | ||
this.keyWatches[key] = watch; | ||
watch.on('error', (err) => { console.error(`Error watching key "${key}".`, err) ; reject(err); }); | ||
watch.on('end', () => { console.warn(`The watch for key "${key}" has been stopped.`); this._disposeKeyWatch(key); }); | ||
watch.on('change', async (keys) => | ||
{ | ||
const oldKeys = Object.keys(this.knownKeys); | ||
const missingKeys = oldKeys.filter(key => !keys.includes(key)); | ||
watch.on('error', (err) => { | ||
console.error(`Error watching key "${key}".`, err); reject(err); | ||
}); | ||
watch.on('end', () => { | ||
console.warn(`The watch for key "${key}" has been stopped.`); this._disposeKeyWatch(key); | ||
}); | ||
watch.on('change', async (keys) => { | ||
const oldKeys = Object.keys(this.knownKeys); | ||
const missingKeys = oldKeys.filter(key => !keys.includes(key)); | ||
await Promise.all(missingKeys.map(async key => this._disposeKeyWatch(key))).catch(e => null); | ||
await Promise.all(keys.map(async (key) => | ||
{ | ||
this.knownKeys[key] = true; | ||
await Promise.all(missingKeys.map(async key => this._disposeKeyWatch(key))).catch(e => null); | ||
await Promise.all(keys.map(async (key) => { | ||
this.knownKeys[key] = true; | ||
const result = await consul.kv.get({ key }); | ||
const value = result && result.Value; | ||
const oldValue = await this.cache.get(key); | ||
const result = await consul.kv.get({key}); | ||
const value = result && result.Value; | ||
const oldValue = await this.cache.get(key); | ||
if(value != oldValue) | ||
{ | ||
await this.cache.put(key, value); | ||
if (value != oldValue) { | ||
await this.cache.set(key, value); | ||
const shortKey = key.substr(key.indexOf(this.serviceName) + this.serviceName.length + 1); | ||
const shortKey = key.substr(key.indexOf(this.serviceName) + this.serviceName.length + 1); | ||
/** | ||
/** | ||
* Change event for properties (kv). | ||
@@ -279,153 +264,133 @@ * | ||
*/ | ||
this.emit('propertyChanged', shortKey, value); | ||
} | ||
})).catch(e => null); | ||
}); | ||
this.emit('propertyChanged', shortKey, value); | ||
} | ||
})).catch(e => null); | ||
}); | ||
resolve(watch); | ||
}); | ||
} | ||
resolve(watch); | ||
}); | ||
} | ||
async _disposeKeyWatch(key) | ||
{ | ||
if(this.keyWatches[key]) | ||
{ | ||
this.keyWatches[key].end(); | ||
this.keyWatches[key].removeAllListeners('error').removeAllListeners('end').removeAllListeners('change'); | ||
async _disposeKeyWatch(key) { | ||
if (this.keyWatches[key]) { | ||
this.keyWatches[key].end(); | ||
this.keyWatches[key].removeAllListeners('error').removeAllListeners('end').removeAllListeners('change'); | ||
delete this.keyWatches[key]; | ||
delete this.knownKeys[key]; | ||
delete this.keyWatches[key]; | ||
delete this.knownKeys[key]; | ||
this.cache && await this.cache.delete(key); | ||
this.cache && await this.cache.del(key); | ||
const shortKey = key.substr(key.indexOf(this.serviceName) + this.serviceName.length + 1); | ||
const shortKey = key.substr(key.indexOf(this.serviceName) + this.serviceName.length + 1); | ||
this.emit('propertyChanged', shortKey); | ||
} | ||
this.emit('propertyChanged', shortKey); | ||
} | ||
} | ||
async _getConsulEndpoint(serviceName) | ||
{ | ||
return this._getRandomValueThrow(await this._getConsulEndpoints(serviceName)); | ||
async _getConsulEndpoint(serviceName) { | ||
return this._getRandomValueThrow(await this._getConsulEndpoints(serviceName)); | ||
} | ||
async _getConsulEndpoints(serviceName) { | ||
if (process.env.KUBERNETES_SERVICE_DISCOVERY) { | ||
const port = process.env[`${serviceName.toUpperCase()}_PORT`] || 80; | ||
return [{host: serviceName, port}]; | ||
} | ||
async _getConsulEndpoints(serviceName) | ||
{ | ||
if (process.env.KUBERNETES_SERVICE_DISCOVERY) | ||
{ | ||
const port = process.env[`${serviceName.toUpperCase()}_PORT`] || 80; | ||
return [{host: serviceName, port}]; | ||
} | ||
const cacheName = `${this.serviceName}.ep.${serviceName}`; | ||
const results = await this.cache.get(cacheName); | ||
const cacheName = this.serviceName + '.ep.' + serviceName; | ||
const results = await this.cache.get(cacheName); | ||
if (results === undefined) { | ||
await this._watchServiceHealth(this.consul, serviceName); | ||
if(results === undefined) | ||
{ | ||
await this._watchServiceHealth(this.consul, serviceName); | ||
const getValuesOrThrow = async (values) => { | ||
if (!values || (Array.isArray(values) && values.length === 0)) { | ||
throw new Error('Could not get values.'); | ||
} | ||
const getValuesOrThrow = async (values) => | ||
{ | ||
if(!values || (Array.isArray(values) && values.length === 0)) | ||
throw new Error('Could not get values.'); | ||
return values; | ||
}; | ||
return values; | ||
} | ||
return retry(async () => getValuesOrThrow(await this.cache.get(cacheName)), { max_tries : 5, interval : 500 }) | ||
.catch(e => { throw new Error('The requested endpoint could not be found or did not pass health checks: ' + serviceName); }); | ||
} | ||
else | ||
{ | ||
return results; | ||
} | ||
return retry(async () => getValuesOrThrow(await this.cache.get(cacheName)), {max_tries: 5, interval: 500}). | ||
catch(e => { | ||
throw new Error(`The requested endpoint could not be found or did not pass health checks: ${serviceName}`); | ||
}); | ||
} else { | ||
return results; | ||
} | ||
} | ||
async _getConsulValues(keyName, recursive) | ||
{ | ||
if(recursive) | ||
{ | ||
const keys = (await this.cache.keys()).filter(k => k.startsWith(keyName)); | ||
async _getConsulValues(keyName, recursive) { | ||
if (recursive) { | ||
const keys = (await this.cache.keys()).filter(k => k.startsWith(keyName)); | ||
if(keys.length === 0) | ||
throw new Error('Key was not found: ' + keyName); | ||
if (keys.length === 0) { | ||
throw new Error(`Key was not found: ${keyName}`); | ||
} | ||
const values = await Promise.all(keys.map(k => this.cache.get(k))); | ||
const results = { }; | ||
const values = await Promise.all(keys.map(k => this.cache.get(k))); | ||
const results = { }; | ||
for(const i in keys) | ||
{ | ||
const key = keys[i].substr(keys[i].indexOf(this.serviceName) + this.serviceName.length + 1); | ||
results[key] = values[i]; | ||
} | ||
for (const i in keys) { | ||
const key = keys[i].substr(keys[i].indexOf(this.serviceName) + this.serviceName.length + 1); | ||
results[key] = values[i]; | ||
} | ||
return results; | ||
} | ||
else | ||
{ | ||
const value = await this.cache.get(keyName); | ||
return results; | ||
} else { | ||
const value = await this.cache.get(keyName); | ||
if(value) | ||
return value; | ||
else | ||
throw new Error('Key was not found: ' + keyName); | ||
} | ||
if (value) { | ||
return value; | ||
} else { | ||
throw new Error(`Key was not found: ${keyName}`); | ||
} | ||
} | ||
} | ||
async _setConsulValue(keyName, value) | ||
{ | ||
try | ||
{ | ||
await this.cache.delete(keyName); | ||
await this.consul.kv.set(keyName, value); | ||
} | ||
catch(e) | ||
{ | ||
this.logger.warn('Config key could not be set', {keyName, ...e}); | ||
} | ||
async _setConsulValue(keyName, value) { | ||
try { | ||
await this.cache.del(keyName); | ||
await this.consul.kv.set(keyName, value); | ||
} catch (e) { | ||
this.logger.warn('Config key could not be set', {keyName, ...e}); | ||
} | ||
} | ||
async _deleteConsulValue(keyName, recursive) | ||
{ | ||
try | ||
{ | ||
if(recursive) | ||
{ | ||
const keys = (await this.cache.keys()).filter(k => k.startsWith(keyName)); | ||
await Promise.all(keys.map(k => this.cache.delete(k))); | ||
} | ||
else | ||
{ | ||
await this.cache.delete(keyName); | ||
} | ||
await this.consul.kv.del({ key : keyName, recurse : recursive }); | ||
} | ||
catch(e) | ||
{ | ||
this.logger.warn('Config key could not be deleted', {keyName, ...e}); | ||
return false; | ||
} | ||
async _deleteConsulValue(keyName, recursive) { | ||
try { | ||
if (recursive) { | ||
const keys = (await this.cache.keys()).filter(k => k.startsWith(keyName)); | ||
await Promise.all(keys.map(k => this.cache.del(k))); | ||
} else { | ||
await this.cache.del(keyName); | ||
} | ||
return true; | ||
await this.consul.kv.del({key: keyName, recurse: recursive}); | ||
} catch (e) { | ||
this.logger.warn('Config key could not be deleted', {keyName, ...e}); | ||
return false; | ||
} | ||
_getRandomValue(array) | ||
{ | ||
if(Array.isArray(array) && array.length) | ||
return array[Math.floor(Math.random() * 1e10 % array.length)]; | ||
else | ||
return null; | ||
return true; | ||
} | ||
_getRandomValue(array) { | ||
if (Array.isArray(array) && array.length) { | ||
return array[Math.floor(Math.random() * 1e10 % array.length)]; | ||
} else { | ||
return null; | ||
} | ||
} | ||
_getRandomValueThrow(array) | ||
{ | ||
const result = this._getRandomValue(array); | ||
_getRandomValueThrow(array) { | ||
const result = this._getRandomValue(array); | ||
if(!result) | ||
throw new Error('Cannot select random value from empty array.'); | ||
if (!result) { | ||
throw new Error('Cannot select random value from empty array.'); | ||
} | ||
return result; | ||
} | ||
return result; | ||
} | ||
} | ||
@@ -432,0 +397,0 @@ |
const ConfigClient = require('./ConfigClient'); | ||
module.exports = { ConfigClient }; | ||
module.exports = {ConfigClient}; |
{ | ||
"name": "@opuscapita/config", | ||
"version": "3.1.7", | ||
"version": "3.1.11", | ||
"description": "Configuration API connector module for OpusCapita Business Network Portal.", | ||
@@ -28,18 +28,19 @@ "main": "index.js", | ||
"dependencies": { | ||
"@opuscapita/cache": "^1.2.0", | ||
"@opuscapita/logger": "^2.0.0", | ||
"@opuscapita/logger": "^2.0.2", | ||
"bluebird": "^3.7.2", | ||
"bluebird-retry": "~0.11.0", | ||
"consul": "^0.40.0", | ||
"extend": "~3.0.1" | ||
"extend": "~3.0.1", | ||
"node-cache": "^5.1.2" | ||
}, | ||
"devDependencies": { | ||
"@types/mocha": "^9.1.1", | ||
"@types/node": "^18.0.6", | ||
"eslint": "^8.20.0", | ||
"jsdoc-to-markdown": "^7.1.1", | ||
"mocha": "^10.0.0", | ||
"mocha-junit-reporter": "^2.0.2", | ||
"@opuscapita/eslint-config-opuscapita-bnapp": "^1.3.5", | ||
"@types/mocha": "^10.0.1", | ||
"@types/node": "^18.11.15", | ||
"eslint": "^8.29.0", | ||
"jsdoc-to-markdown": "^8.0.0", | ||
"mocha": "^10.2.0", | ||
"mocha-junit-reporter": "^2.2.0", | ||
"nyc": "^15.1.0" | ||
} | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Deprecated
MaintenanceThe maintainer of the package marked it as deprecated. This could indicate that a single version should not be used, or that the package is no longer maintained and any new vulnerabilities will not be fixed.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
38018
8
656
1
+ Addednode-cache@^5.1.2
- Removed@opuscapita/cache@^1.2.0
- Removed@opuscapita/cache@1.3.0(transitive)
- Removed@opuscapita/config@3.1.17(transitive)
- Removeddenque@1.5.1(transitive)
- Removedlodash@4.17.21(transitive)
- Removednode-cache@4.2.1(transitive)
- Removedredis@3.1.2(transitive)
- Removedredis-commands@1.7.0(transitive)
- Removedredis-errors@1.2.0(transitive)
- Removedredis-parser@3.0.0(transitive)
Updated@opuscapita/logger@^2.0.2