Make it possible to work without config.yml

Most of the basic configuration could be done via ENV
This commit is contained in:
Alexander Kukushkin
2016-06-09 14:44:29 +02:00
parent 7244739e26
commit 49efb371f9
5 changed files with 42 additions and 41 deletions
+3 -17
View File
@@ -1,7 +1,5 @@
import logging import logging
import os
import signal import signal
import sys
import time import time
from patroni.api import RestApiServer from patroni.api import RestApiServer
@@ -17,11 +15,10 @@ logger = logging.getLogger(__name__)
class Patroni(object): class Patroni(object):
PATRONI_CONFIG_VARIABLE = 'PATRONI_CONFIGURATION'
def __init__(self, config_file=None, config_env=None): def __init__(self):
self.version = __version__ self.version = __version__
self.config = Config(config_file=config_file, config_env=config_env) self.config = Config()
self.dcs = get_dcs(self.config) self.dcs = get_dcs(self.config)
self.load_dynamic_configuration() self.load_dynamic_configuration()
@@ -116,18 +113,7 @@ def main():
logging.getLogger('requests').setLevel(logging.WARNING) logging.getLogger('requests').setLevel(logging.WARNING)
setup_signal_handlers() setup_signal_handlers()
# Patroni reads the configuration from the command-line argument if it exists, and from the environment otherwise. patroni = Patroni()
config_env = False
config_file = len(sys.argv) >= 2 and os.path.isfile(sys.argv[1]) and sys.argv[1]
if not config_file:
config_env = os.environ.pop(Patroni.PATRONI_CONFIG_VARIABLE, None)
if config_env is None:
print('Usage: {0} config.yml'.format(sys.argv[0]))
print('\tPatroni may also read the configuration from the {} environment variable'.
format(Patroni.PATRONI_CONFIG_VARIABLE))
return
patroni = Patroni(config_file, config_env)
try: try:
patroni.run() patroni.run()
except KeyboardInterrupt: except KeyboardInterrupt:
+22 -8
View File
@@ -1,6 +1,7 @@
import json import json
import logging import logging
import os import os
import sys
import tempfile import tempfile
import yaml import yaml
@@ -33,6 +34,9 @@ class Config(object):
to work with it as with the old `config` object. to work with it as with the old `config` object.
""" """
PATRONI_ENV_PREFIX = 'PATRONI_'
PATRONI_CONFIG_VARIABLE = PATRONI_ENV_PREFIX + 'CONFIGURATION'
__CACHE_FILENAME = 'patroni.dynamic.json' __CACHE_FILENAME = 'patroni.dynamic.json'
__DEFAULT_CONFIG = { __DEFAULT_CONFIG = {
'ttl': 30, 'loop_wait': 10, 'retry_timeout': 10, 'ttl': 30, 'loop_wait': 10, 'retry_timeout': 10,
@@ -42,15 +46,25 @@ class Config(object):
} }
} }
def __init__(self, config_file=None, config_env=None): def __init__(self):
self._config_file = None if config_env else config_file
self._modify_index = -1 self._modify_index = -1
self._dynamic_configuration = {} self._dynamic_configuration = {}
if config_env:
self._local_configuration = yaml.safe_load(config_env) self.__environment_configuration = self._build_environment_configuration()
else:
self.__environment_configuration = self._build_environment_configuration() # Patroni reads the configuration from the command-line argument if it exists, otherwise from the environment
self._config_file = len(sys.argv) >= 2 and os.path.isfile(sys.argv[1]) and sys.argv[1]
if self._config_file:
self._local_configuration = self._load_config_file() self._local_configuration = self._load_config_file()
else:
config_env = os.environ.pop(self.PATRONI_CONFIG_VARIABLE, None)
self._local_configuration = config_env and yaml.safe_load(config_env) or self.__environment_configuration
if not self._local_configuration:
print('Usage: {0} config.yml'.format(sys.argv[0]))
print('\tPatroni may also read the configuration from the {0} environment variable'.
format(self.PATRONI_CONFIG_VARIABLE))
exit(1)
self.__effective_configuration = self._build_effective_configuration(self._dynamic_configuration, self.__effective_configuration = self._build_effective_configuration(self._dynamic_configuration,
self._local_configuration) self._local_configuration)
self._data_dir = self.__effective_configuration['postgresql']['data_dir'] self._data_dir = self.__effective_configuration['postgresql']['data_dir']
@@ -168,7 +182,7 @@ class Config(object):
ret = defaultdict(dict) ret = defaultdict(dict)
def _popenv(name): def _popenv(name):
return os.environ.pop('PATRONI_' + name.upper(), None) return os.environ.pop(Config.PATRONI_ENV_PREFIX + name.upper(), None)
for param in ('name', 'namespace', 'scope'): for param in ('name', 'namespace', 'scope'):
value = _popenv(param) value = _popenv(param)
@@ -216,7 +230,7 @@ class Config(object):
return None return None
for param in list(os.environ.keys()): for param in list(os.environ.keys()):
if param.startswith('PATRONI_'): if param.startswith(Config.PATRONI_ENV_PREFIX):
name, suffix = (param[8:].rsplit('_', 1) + [''])[:2] name, suffix = (param[8:].rsplit('_', 1) + [''])[:2]
if name and suffix: if name and suffix:
# PATRONI_(ETCD|CONSUL|ZOOKEEPER|EXHIBITOR|...)_(HOSTS?|PORT) # PATRONI_(ETCD|CONSUL|ZOOKEEPER|EXHIBITOR|...)_(HOSTS?|PORT)
+9 -2
View File
@@ -1,5 +1,6 @@
import os import os
import unittest import unittest
import sys
from mock import MagicMock, Mock, patch from mock import MagicMock, Mock, patch
from patroni.config import Config from patroni.config import Config
@@ -12,7 +13,12 @@ class TestConfig(unittest.TestCase):
@patch('json.load', Mock(side_effect=Exception)) @patch('json.load', Mock(side_effect=Exception))
@patch.object(builtins, 'open', MagicMock()) @patch.object(builtins, 'open', MagicMock())
def setUp(self): def setUp(self):
self.config = Config(config_env='restapi: {}\npostgresql: {data_dir: foo}') sys.argv = ['patroni.py']
os.environ[Config.PATRONI_CONFIG_VARIABLE] = 'restapi: {}\npostgresql: {data_dir: foo}'
self.config = Config()
def test_no_config(self):
self.assertRaises(SystemExit, Config)
@patch.object(Config, '_build_effective_configuration', Mock(side_effect=Exception)) @patch.object(Config, '_build_effective_configuration', Mock(side_effect=Exception))
def test_set_dynamic_configuration(self): def test_set_dynamic_configuration(self):
@@ -46,7 +52,8 @@ class TestConfig(unittest.TestCase):
'PATRONI_admin_PASSWORD': 'admin', 'PATRONI_admin_PASSWORD': 'admin',
'PATRONI_admin_OPTIONS': 'createrole,createdb' 'PATRONI_admin_OPTIONS': 'createrole,createdb'
}) })
config = Config(config_file='postgres0.yml') sys.argv = ['patroni.py', 'postgres0.yml']
config = Config()
with patch.object(Config, '_load_config_file', Mock(return_value={'restapi': {}})): with patch.object(Config, '_load_config_file', Mock(return_value={'restapi': {}})):
with patch.object(Config, '_build_effective_configuration', Mock(side_effect=Exception)): with patch.object(Config, '_build_effective_configuration', Mock(side_effect=Exception)):
self.assertRaises(Exception, config.reload_local_configuration, True) self.assertRaises(Exception, config.reload_local_configuration, True)
+6 -4
View File
@@ -1,7 +1,8 @@
import etcd
import unittest
import datetime import datetime
import etcd
import os
import pytz import pytz
import unittest
from mock import Mock, MagicMock, patch from mock import Mock, MagicMock, patch
from patroni.config import Config from patroni.config import Config
@@ -49,7 +50,7 @@ def get_cluster_initialized_with_only_leader(failover=None):
class MockPatroni(object): class MockPatroni(object):
def __init__(self, p, d): def __init__(self, p, d):
self.config = Config(config_env=""" os.environ[Config.PATRONI_CONFIG_VARIABLE] = """
restapi: restapi:
listen: 0.0.0.0:8008 listen: 0.0.0.0:8008
bootstrap: bootstrap:
@@ -68,7 +69,8 @@ zookeeper:
exhibitor: exhibitor:
hosts: [localhost] hosts: [localhost]
port: 8181 port: 8181
""") """
self.config = Config()
self.postgresql = p self.postgresql = p
self.dcs = d self.dcs = d
self.api = Mock() self.api = Mock()
+2 -10
View File
@@ -1,5 +1,4 @@
import etcd import etcd
import os
import sys import sys
import time import time
import unittest import unittest
@@ -34,7 +33,8 @@ class TestPatroni(unittest.TestCase):
RestApiServer.socket = 0 RestApiServer.socket = 0
with patch.object(etcd.Client, 'machines') as mock_machines: with patch.object(etcd.Client, 'machines') as mock_machines:
mock_machines.__get__ = Mock(return_value=['http://remotehost:2379']) mock_machines.__get__ = Mock(return_value=['http://remotehost:2379'])
self.p = Patroni('postgres0.yml') sys.argv = ['patroni.py', 'postgres0.yml']
self.p = Patroni()
@patch('patroni.dcs.AbstractDCS.get_cluster', Mock(side_effect=[None, DCSError('foo'), None])) @patch('patroni.dcs.AbstractDCS.get_cluster', Mock(side_effect=[None, DCSError('foo'), None]))
def test_load_dynamic_configuration(self): def test_load_dynamic_configuration(self):
@@ -47,7 +47,6 @@ class TestPatroni(unittest.TestCase):
@patch.object(etcd.Client, 'machines') @patch.object(etcd.Client, 'machines')
def test_patroni_main(self, mock_machines): def test_patroni_main(self, mock_machines):
with patch('subprocess.call', Mock(return_value=1)): with patch('subprocess.call', Mock(return_value=1)):
_main()
sys.argv = ['patroni.py', 'postgres0.yml'] sys.argv = ['patroni.py', 'postgres0.yml']
mock_machines.__get__ = Mock(return_value=['http://remotehost:2379']) mock_machines.__get__ = Mock(return_value=['http://remotehost:2379'])
@@ -55,13 +54,6 @@ class TestPatroni(unittest.TestCase):
self.assertRaises(SleepException, _main) self.assertRaises(SleepException, _main)
with patch.object(Patroni, 'run', Mock(side_effect=KeyboardInterrupt())): with patch.object(Patroni, 'run', Mock(side_effect=KeyboardInterrupt())):
_main() _main()
sys.argv = ['patroni.py']
# read the content of the yaml configuration file into the environment variable
# in order to test how does patroni handle the configuration passed from the environment.
with open('postgres0.yml', 'r') as f:
os.environ[Patroni.PATRONI_CONFIG_VARIABLE] = f.read()
with patch.object(Patroni, 'run', Mock(side_effect=SleepException())):
self.assertRaises(SleepException, _main)
@patch('patroni.config.Config.save_cache', Mock()) @patch('patroni.config.Config.save_cache', Mock())
@patch('patroni.config.Config.reload_local_configuration', Mock(return_value=True)) @patch('patroni.config.Config.reload_local_configuration', Mock(return_value=True))