UNPKG

backendless-consul-config-provider

Version:

Consul Configs provider for Backendless' JS Servers

236 lines (175 loc) 6.5 kB
'use strict' const BackendlessRequest = require('backendless-request') const Utils = require('./utils') const Logger = require('./logger') const DEFAULT_PREFIX = 'default' const REDIS_PREFIX = 'config/redis' const REDIS_DEFAULT_PREFIX = 'config/redis/default' const DEFAULT_SENTINEL_MASTER_NAME = 'mymaster' const SENTINEL_ADDRESSES = 'config/redis/sentinel/addresses' const RedisTypes = [ 'bl/debug', 'bl/production', 'cache', 'rt', 'events', 'user-session', 'properties', 'developer/session', 'analytics', 'sync', 'messaging', 'taskman', 'logservice', 'gamification' ] const getSentinelMasterConfigPath = redisType => `config/redis/sentinel/${ redisType }/master_name` const getSentinelPasswordConfigPath = () => `config/redis/sentinel/password` module.exports = class ConsulApi { constructor(url, aclToken) { Logger.info(`Consul URL: ${ url } ${ aclToken ? 'WITH' : 'WITHOUT' } ACL TOKEN`) this.url = url this.aclToken = aclToken } loadItemsTreeByKey(serviceKey, envConfigs) { Logger.info(`Load config tree from Consul by key: ${ serviceKey }`) const remainsEnvConfigs = Object.assign({}, envConfigs) const tree = {} return Promise.resolve() .then(() => this.loadItems(serviceKey)) .then(items => { Logger.info(`Loaded config tree from Consul by key [${ serviceKey }]: ${ JSON.stringify(items) }`) items.forEach(item => { if (item.Value === null) { Logger.warn(`Skipping Config item: ${ JSON.stringify(item) } due to item value is NULL`) } else { const key = item.Key.replace(serviceKey, '') const envConfigKey = serviceKey + key const value = adaptEnvValue(envConfigs[envConfigKey], parseSourceConsulValue(item.Value)) delete remainsEnvConfigs[envConfigKey] setItemValueIntoTree(key, value, tree) } }) }) .then(() => { Object.keys(remainsEnvConfigs).forEach(key => { if (key.startsWith(serviceKey)) { setItemValueIntoTree(key.replace(serviceKey, ''), remainsEnvConfigs[key], tree) } }) }) .then(() => tree) } loadItemsTreeBySchema(schema, envConfigs) { Logger.info(`Load config tree from Consul by schema: ${ JSON.stringify(schema, null, 2) }`) const tree = {} const requests = [] const processObject = (mapping, target) => Object.keys(mapping).forEach(key => { if (Utils.isObject(mapping[key])) { processObject(mapping[key], target[key] = Utils.ensureObject(target[key])) } else { const itemKey = mapping[key] const isRedisConfigItem = itemKey.includes(REDIS_PREFIX) const setKeyToTarget = value => { if (Utils.isObject(value)) { return target = Object.assign(target, value) } return target[key] = adaptEnvValue(envConfigs[itemKey], value) } let promise = isRedisConfigItem ? this.loadRedisConfigItem(itemKey) : this.loadItem(itemKey) promise = promise .then(setKeyToTarget) .catch(() => { // could not load value by passed key, it might be optional, just ignore the error }) requests.push(promise) } }) processObject(schema, tree) return Promise .all(requests) .then(() => tree) } loadItem(key, opts = {}) { let request = this.request(key, { raw: true }) if (opts.silent) { request = request.catch(() => undefined) } return request } loadItems(key) { return this.request(key, { recurse: true }) } async loadSentinels(isHostItem, redisType) { const sentinelAddresses = await this.loadItem(SENTINEL_ADDRESSES, { silent: true }) if (!isHostItem || !sentinelAddresses) { return } // if sentinel address specified, connect through sentinel const [defaultSentinelMaster, sentinelMasterName, sentinelPassword] = await Promise.all([ this.loadItem(getSentinelMasterConfigPath(DEFAULT_PREFIX), { silent: true }), this.loadItem(getSentinelMasterConfigPath(redisType), { silent: true }), this.loadItem(getSentinelPasswordConfigPath(), { silent: true }), ]) const sentinels = sentinelAddresses.split(';').map(address => { const [host, port] = address.split(':') return { host, port: Number(port) } }) return { sentinels, sentinelPassword, name: sentinelMasterName || defaultSentinelMaster || DEFAULT_SENTINEL_MASTER_NAME, } } async loadRedisConfigItem(key) { const value = await this.loadItem(key, { silent: true }) const isHostItem = key.includes('host') if (value !== '' && value !== undefined) { return value } const redisType = RedisTypes.find(type => key.startsWith(`${ REDIS_PREFIX }/${ type }`)) const keyPath = key.slice(`${ REDIS_PREFIX }/${ redisType }`.length) const defaultKey = `${ REDIS_DEFAULT_PREFIX }${ keyPath }` const sentinels = await this.loadSentinels(isHostItem, redisType) if (sentinels) { return sentinels } // else use direct connection (no sentinel) to default host from config or localhost return this.loadItem(defaultKey) } request(key, query) { const headers = {} if (this.aclToken) { headers['X-Consul-Token'] = this.aclToken } return BackendlessRequest .get(`${ this.url }/v1/kv/${ key }`) .set(headers) .query(query) } } function setItemValueIntoTree(itemKey, itemValue, tree) { const keys = itemKey.split('/') const targetKey = keys.pop() let parent = tree keys.forEach(key => { parent = parent[key] = Utils.ensureObject(parent[key]) }) parent[targetKey] = itemValue } function parseSourceConsulValue(source) { const str = Buffer.from(source, 'base64').toString() try { return JSON.parse(str) } catch (e) { //can't parse, that means it's just a string return str } } const ValueFormatter = { 'number': value => Number(value), 'boolean': value => { value = value && value.toLowerCase() if (value === 'true' || value === 'yes' || value === '1') { return true } if (value === 'false' || value === 'no' || value === '0') { return false } }, } function adaptEnvValue(envValue, consulValue) { if (envValue) { const formatter = ValueFormatter[typeof consulValue] return formatter ? formatter(envValue) : envValue } return consulValue }