From 08524953bfb93429b51775efc7706287b7c971d3 Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 13 Aug 2020 16:50:30 -0500 Subject: [PATCH] Add Environment Variable Override (#4085) * Add Environment Variable Override I need to be able to manage settings on this service via Environment Variables. I've added the ability to override anything in the settings schema with a corresponding environment variable. e.g. to override settings.serial.port you can set ZIGBEE2MQTT_SERIAL_PORT=/dev/ttyS0 to override the mqtt username you can set ZIGBEE2MQTT_MQTT_USER=testusername This new addition will not perform any action on existing installations unless the matching environment variable is set. I have tested this setting string, number, boolean, object and array values. * Adding test case for environment variables and slight modification to ensure 100% code coverage. * Adding a test to confirm that env variables will set non default values. Also realized that I was errantly applying the env variables to the defaults for testing. Understanding what this is doing more clearly I realize that should be clean. * Refactoring to 1. Remove the test variables from the schema and defaults and manually reflect the tests in the test. 2. Rename environment variable base from ZIGBEE2MQTT_ to ZIGBEE2MQTT_CONFIG_ * Small improvements * Removing the unneeded test. Co-authored-by: Koen Kanters --- lib/util/settings.js | 50 +++++++++++++++++++++++++++++++++++++++++-- test/settings.test.js | 32 +++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/lib/util/settings.js b/lib/util/settings.js index 4aaa82b5f..ffa8136c6 100644 --- a/lib/util/settings.js +++ b/lib/util/settings.js @@ -438,9 +438,52 @@ function read() { return s; } +function applyEnvironmentVariables(settings) { + const iterate = (obj, path) => { + Object.keys(obj).forEach((key) => { + if (key !== 'type') { + if (key !== 'properties') { + const type = (obj[key].type || 'object').toString(); + const envPart = path.reduce((acc, val) => `${acc}${val}_`, ''); + const envVariableName = (`ZIGBEE2MQTT_CONFIG_${envPart}${key}`).toUpperCase(); + if (process.env[envVariableName]) { + const setting = path.reduce((acc, val, index) => { + acc[val] = acc[val] || {}; + return acc[val]; + }, settings); + + if (type.indexOf('object') >= 0 || type.indexOf('array') >= 0) { + setting[key] = JSON.parse(process.env[envVariableName]); + } + if (type.indexOf('number') >= 0) { + setting[key] = process.env[envVariableName] * 1; + } + if (type.indexOf('boolean') >= 0) { + setting[key] = process.env[envVariableName].toLowerCase() === 'true'; + } + if (type.indexOf('string') >= 0) { + setting[key] = process.env[envVariableName]; + } + } + } + + if (typeof obj[key] === 'object') { + const newPath = [...path]; + if (key !== 'properties') { + newPath.push(key); + } + iterate(obj[key], newPath); + } + } + }); + }; + iterate(schema.properties, []); +} + function get() { if (!_settings) { _settings = read(); + applyEnvironmentVariables(_settings); } return _settings; @@ -743,11 +786,14 @@ module.exports = { // For tests only _write: write, _reRead: () => { - _settings = read(); - _settingsWithDefaults = objectAssignDeep.noMutate(defaults, get()); + _settings = null; + get(); + _settingsWithDefaults = null; + getWithDefaults(); }, _clear: () => { _settings = null; + _settingsWithDefaults = null; }, _getDefaults: () => defaults, }; diff --git a/test/settings.test.js b/test/settings.test.js index 1edf30405..e15bcd18b 100644 --- a/test/settings.test.js +++ b/test/settings.test.js @@ -9,6 +9,7 @@ const devicesFile = data.joinPath('devices.yaml'); const groupsFile = data.joinPath('groups.yaml'); const secretFile = data.joinPath('secret.yaml'); const yaml = require('js-yaml'); +const objectAssignDeep = require(`object-assign-deep`); const minimalConfig = { permit_join: true, @@ -27,11 +28,19 @@ describe('Settings', () => { const remove = (file) => { if (fs.existsSync(file)) fs.unlinkSync(file); } + const clearEnvironmentVariables = () => { + Object.keys(process.env).forEach((key) => { + if(key.indexOf('ZIGBEE2MQTT_CONFIG_') >= 0) { + delete process.env[key]; + } + }); + } beforeEach(() => { remove(configurationFile); remove(devicesFile); remove(groupsFile); + clearEnvironmentVariables(); }); it('Should return default settings', () => { @@ -53,6 +62,29 @@ describe('Settings', () => { expect(s).toStrictEqual(expected); }); + it('Should apply environment variables', () => { + process.env['ZIGBEE2MQTT_CONFIG_SERIAL_DISABLE_LED'] = 'true'; + process.env['ZIGBEE2MQTT_CONFIG_ADVANCED_SOFT_RESET_TIMEOUT'] = 1; + process.env['ZIGBEE2MQTT_CONFIG_EXPERIMENTAL_OUTPUT'] = 'csvtest'; + process.env['ZIGBEE2MQTT_CONFIG_ADVANCED_AVAILABILITY_BLOCKLIST'] = '["0x43597f0dac781b1e", "x223b0aef2ae8d1b0"]'; + process.env['ZIGBEE2MQTT_CONFIG_MAP_OPTIONS_GRAPHVIZ_COLORS_FILL'] = '{"enddevice": "#ff0000", "coordinator": "#00ff00", "router": "#0000ff"}'; + process.env['ZIGBEE2MQTT_CONFIG_MQTT_BASE_TOPIC'] = 'testtopic'; + + write(configurationFile, {}); + const s = settings.get(); + const expected = objectAssignDeep.noMutate({}, settings._getDefaults()); + expected.devices = {}; + expected.groups = {}; + expected.serial.disable_led = true; + expected.advanced.soft_reset_timeout = 1; + expected.experimental.output = 'csvtest'; + expected.advanced.availability_blocklist = ['0x43597f0dac781b1e', 'x223b0aef2ae8d1b0']; + expected.map_options.graphviz.colors.fill = {enddevice: '#ff0000', coordinator: '#00ff00', router: '#0000ff'}; + expected.mqtt.base_topic = 'testtopic'; + + expect(s).toStrictEqual(expected); + }); + it('Should add devices', () => { write(configurationFile, {}); settings.addDevice('0x12345678');