diff --git a/.github/workflows/install_deps.py b/.github/workflows/install_deps.py index 700c4b23..d8ead869 100644 --- a/.github/workflows/install_deps.py +++ b/.github/workflows/install_deps.py @@ -46,7 +46,7 @@ def install_packages(what): packages['exhibitor'] = packages['zookeeper'] packages = packages.get(what, []) ver = versions.get(what) - if float(ver) >= 15: + if 15 <= float(ver) < 17: packages += ['postgresql-{0}-citus-12.1'.format(ver)] subprocess.call(['sudo', 'apt-get', 'update', '-y']) return subprocess.call(['sudo', 'apt-get', 'install', '-y', 'postgresql-' + ver, 'expect-dev'] + packages) diff --git a/.github/workflows/mapping.py b/.github/workflows/mapping.py index 279438b0..d377a408 100644 --- a/.github/workflows/mapping.py +++ b/.github/workflows/mapping.py @@ -1 +1 @@ -versions = {'etcd': '9.6', 'etcd3': '16', 'consul': '13', 'exhibitor': '12', 'raft': '14', 'kubernetes': '15'} +versions = {'etcd': '9.6', 'etcd3': '16', 'consul': '17', 'exhibitor': '12', 'raft': '14', 'kubernetes': '15'} diff --git a/README.rst b/README.rst index fd82531e..76952ac7 100644 --- a/README.rst +++ b/README.rst @@ -12,7 +12,7 @@ Patroni is a template for high availability (HA) PostgreSQL solutions using Pyth We call Patroni a "template" because it is far from being a one-size-fits-all or plug-and-play replication system. It will have its own caveats. Use wisely. -Currently supported PostgreSQL versions: 9.3 to 16. +Currently supported PostgreSQL versions: 9.3 to 17. **Note to Citus users**: Starting from 3.0 Patroni nicely integrates with the `Citus `__ database extension to Postgres. Please check the `Citus support page `__ in the Patroni documentation for more info about how to use Patroni high availability together with a Citus distributed cluster. diff --git a/docs/index.rst b/docs/index.rst index c6a9e0fc..b7af84ce 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -10,7 +10,7 @@ Patroni is a template for high availability (HA) PostgreSQL solutions using Pyth We call Patroni a "template" because it is far from being a one-size-fits-all or plug-and-play replication system. It will have its own caveats. Use wisely. There are many ways to run high availability with PostgreSQL; for a list, see the `PostgreSQL Documentation `__. -Currently supported PostgreSQL versions: 9.3 to 16. +Currently supported PostgreSQL versions: 9.3 to 17. **Note to Citus users**: Starting from 3.0 Patroni nicely integrates with the `Citus `__ database extension to Postgres. Please check the :ref:`Citus support page ` in the Patroni documentation for more info about how to use Patroni high availability together with a Citus distributed cluster. diff --git a/patroni/config.py b/patroni/config.py index 88f3a1a6..269bdfcb 100644 --- a/patroni/config.py +++ b/patroni/config.py @@ -34,7 +34,8 @@ _AUTH_ALLOWED_PARAMETERS = ( 'sslcrl', 'sslcrldir', 'gssencmode', - 'channel_binding' + 'channel_binding', + 'sslnegotiation' ) diff --git a/patroni/config_generator.py b/patroni/config_generator.py index 4977962d..6648dc15 100644 --- a/patroni/config_generator.py +++ b/patroni/config_generator.py @@ -39,7 +39,8 @@ _AUTH_ALLOWED_PARAMETERS_MAPPING = { 'sslcrl': 'PGSSLCRL', 'sslcrldir': 'PGSSLCRLDIR', 'gssencmode': 'PGGSSENCMODE', - 'channel_binding': 'PGCHANNELBINDING' + 'channel_binding': 'PGCHANNELBINDING', + 'sslnegotiation': 'PGSSLNEGOTIATION' } NO_VALUE_MSG = '#FIXME' diff --git a/patroni/postgresql/config.py b/patroni/postgresql/config.py index 5f8d043e..6d410b6d 100644 --- a/patroni/postgresql/config.py +++ b/patroni/postgresql/config.py @@ -121,13 +121,15 @@ def parse_dsn(value: str) -> Optional[Dict[str, str]]: >>> r = parse_dsn('postgresql://u%2Fse:pass@:%2f123,[::1]/db%2Fsdf?application_name=mya%2Fpp&ssl=true') >>> r == {'application_name': 'mya/pp', 'dbname': 'db/sdf', 'host': ',::1', 'sslmode': 'require',\ - 'password': 'pass', 'port': '/123,', 'user': 'u/se', 'gssencmode': 'prefer', 'channel_binding': 'prefer'} + 'password': 'pass', 'port': '/123,', 'user': 'u/se', 'gssencmode': 'prefer',\ + 'channel_binding': 'prefer', 'sslnegotiation': 'postgres'} True >>> r = parse_dsn(" host = 'host' dbname = db\\\\ name requiressl=1 ") >>> r == {'dbname': 'db name', 'host': 'host', 'sslmode': 'require',\ - 'gssencmode': 'prefer', 'channel_binding': 'prefer'} + 'gssencmode': 'prefer', 'channel_binding': 'prefer', 'sslnegotiation': 'postgres'} True - >>> parse_dsn('requiressl = 0\\\\') == {'sslmode': 'prefer', 'gssencmode': 'prefer', 'channel_binding': 'prefer'} + >>> parse_dsn('requiressl = 0\\\\') == {'sslmode': 'prefer', 'gssencmode': 'prefer',\ + 'channel_binding': 'prefer', 'sslnegotiation': 'postgres'} True >>> parse_dsn("host=a foo = '") is None True @@ -151,6 +153,7 @@ def parse_dsn(value: str) -> Optional[Dict[str, str]]: ret.setdefault('sslmode', 'prefer') ret.setdefault('gssencmode', 'prefer') ret.setdefault('channel_binding', 'prefer') + ret.setdefault('sslnegotiation', 'postgres') return ret @@ -587,6 +590,8 @@ class ConfigHandler(object): ret.setdefault('gssencmode', 'prefer') if self._postgresql.major_version >= 130000: ret.setdefault('channel_binding', 'prefer') + if self._postgresql.major_version >= 170000: + ret.setdefault('sslnegotiation', 'postgres') if self._krbsrvname: ret['krbsrvname'] = self._krbsrvname if not ret.get('dbname'): @@ -607,7 +612,7 @@ class ConfigHandler(object): keywords = ('dbname', 'user', 'passfile' if params.get('passfile') else 'password', 'host', 'port', 'sslmode', 'sslcompression', 'sslcert', 'sslkey', 'sslpassword', 'sslrootcert', 'sslcrl', 'sslcrldir', 'application_name', 'krbsrvname', 'gssencmode', 'channel_binding', - 'target_session_attrs') + 'target_session_attrs', 'sslnegotiation') def escape(value: Any) -> str: return re.sub(r'([\'\\ ])', r'\\\1', str(value)) diff --git a/tests/test_config_generator.py b/tests/test_config_generator.py index ffba14ee..7e0e14ef 100644 --- a/tests/test_config_generator.py +++ b/tests/test_config_generator.py @@ -29,7 +29,7 @@ def mock_open(*args, **kwargs): @patch('patroni.psycopg.connect', psycopg_connect) @patch('builtins.open', MagicMock()) -@patch('subprocess.check_output', Mock(return_value=b"postgres (PostgreSQL) 16.2")) +@patch('subprocess.check_output', Mock(return_value=b"postgres (PostgreSQL) 17.0")) @patch('psutil.Process.exe', Mock(return_value='/bin/dir/from/running/postgres')) @patch('psutil.Process.__init__', Mock(return_value=None)) @patch('patroni.config_generator.get_address', Mock(return_value=(HOSTNAME, IP))) @@ -128,7 +128,8 @@ class TestGenerateConfig(unittest.TestCase): 'password': 'qwerty', 'channel_binding': 'prefer', 'gssencmode': 'prefer', - 'sslmode': 'prefer' + 'sslmode': 'prefer', + 'sslnegotiation': 'postgres' }, 'replication': { 'username': NO_VALUE_MSG, @@ -177,7 +178,7 @@ class TestGenerateConfig(unittest.TestCase): '--version']) @patch('os.makedirs', Mock()) - def test_generate_sample_config_16(self): + def test_generate_sample_config_17(self): conf = { 'bootstrap': { 'dcs': { @@ -213,7 +214,7 @@ class TestGenerateConfig(unittest.TestCase): @patch('os.makedirs', Mock()) @patch('sys.stdout') - def test_generate_config_running_instance_16(self, mock_sys_stdout): + def test_generate_config_running_instance_17(self, mock_sys_stdout): self._set_running_instance_config_vals() with patch('builtins.open', Mock(side_effect=self._get_running_instance_open_res())), \ @@ -226,11 +227,13 @@ class TestGenerateConfig(unittest.TestCase): @patch('os.makedirs', Mock()) @patch('sys.stdout') - def test_generate_config_running_instance_16_connect_from_env(self, mock_sys_stdout): + def test_generate_config_running_instance_17_connect_from_env(self, mock_sys_stdout): self._set_running_instance_config_vals() # su auth params and connect host from env os.environ['PGCHANNELBINDING'] = \ self.config['postgresql']['authentication']['superuser']['channel_binding'] = 'disable' + os.environ['PGSSLNEGOTIATION'] = \ + self.config['postgresql']['authentication']['superuser']['sslnegotiation'] = 'direct' conf = { 'scope': 'my_cluster', @@ -265,7 +268,7 @@ class TestGenerateConfig(unittest.TestCase): with patch('builtins.open', Mock(side_effect=self._get_running_instance_open_res())), \ patch('sys.argv', ['patroni.py', '--generate-config']), \ - patch.object(MockConnect, 'server_version', PropertyMock(return_value=160000)), \ + patch.object(MockConnect, 'server_version', PropertyMock(return_value=170000)), \ self.assertRaises(SystemExit) as e: _main() self.assertEqual(e.exception.code, 0) diff --git a/tests/test_postgresql.py b/tests/test_postgresql.py index dffc8ba7..69c46335 100644 --- a/tests/test_postgresql.py +++ b/tests/test_postgresql.py @@ -364,6 +364,7 @@ class TestPostgresql(BaseTestPostgresql): @patch.object(Postgresql, 'is_running', Mock(return_value=False)) @patch.object(Postgresql, 'start', Mock()) + @patch.object(Postgresql, 'major_version', PropertyMock(return_value=170000)) def test_follow(self): self.p.call_nowait(CallbackAction.ON_START) m = RemoteMember('1', {'restore_command': '2', 'primary_slot_name': 'foo', 'conn_kwargs': {'host': 'foo,bar'}}) @@ -1169,5 +1170,5 @@ class TestPostgresql2(BaseTestPostgresql): self.p.config.load_current_server_parameters() self.assertTrue(all(self.p.config._server_parameters[name] == value for name, value in keep_values.items())) self.assertEqual(dict(self.p.config._recovery_params), - {'primary_conninfo': {'host': 'a', 'port': '5433', 'passfile': '/blabla', - 'gssencmode': 'prefer', 'sslmode': 'prefer', 'channel_binding': 'prefer'}}) + {'primary_conninfo': {'host': 'a', 'port': '5433', 'passfile': '/blabla', 'sslmode': 'prefer', + 'gssencmode': 'prefer', 'channel_binding': 'prefer', 'sslnegotiation': 'postgres'}})