diff --git a/src/charm.py b/src/charm.py index 7cf803742c..b5a4bec8bb 100755 --- a/src/charm.py +++ b/src/charm.py @@ -19,6 +19,7 @@ PostgreSQL, PostgreSQLCreateUserError, PostgreSQLEnableDisableExtensionError, + PostgreSQLListUsersError, PostgreSQLUpdateUserPasswordError, ) from charms.postgresql_k8s.v0.postgresql_tls import PostgreSQLTLS @@ -299,6 +300,9 @@ def postgresql(self) -> PostgreSQL: @property def primary_endpoint(self) -> Optional[str]: """Returns the endpoint of the primary instance or None when no primary available.""" + if not self._peers: + logger.debug("primary endpoint early exit: Peer relation not joined yet.") + return None try: for attempt in Retrying(stop=stop_after_delay(60), wait=wait_fixed(3)): with attempt: @@ -308,7 +312,20 @@ def primary_endpoint(self) -> Optional[str]: # returned is not in the list of the current cluster members # (like when the cluster was not updated yet after a failed switchover). if not primary_endpoint or primary_endpoint not in self._units_ips: - raise ValueError() + # TODO figure out why peer data is not available + if ( + primary_endpoint + and len(self._units_ips) == 1 + and len(self._peers.units) > 1 + ): + logger.warning( + "Possibly incoplete peer data: Will not map primary IP to unit IP" + ) + return primary_endpoint + logger.debug( + "primary endpoint early exit: Primary IP not in cached peer list." + ) + primary_endpoint = None except RetryError: return None else: @@ -1036,6 +1053,10 @@ def _start_primary(self, event: StartEvent) -> None: logger.exception(e) self.unit.status = BlockedStatus("Failed to create postgres user") return + except PostgreSQLListUsersError: + logger.warning("Deferriing on_start: Unable to list users") + event.defer() + return self.postgresql.set_up_database() @@ -1381,6 +1402,17 @@ def _is_workload_running(self) -> bool: return charmed_postgresql_snap.services["patroni"]["active"] + @property + def _can_connect_to_postgresql(self) -> bool: + try: + for attempt in Retrying(stop=stop_after_delay(30), wait=wait_fixed(3)): + with attempt: + assert self.postgresql.get_postgresql_timezones() + except RetryError: + logger.debug("Cannot connect to database") + return False + return True + def update_config(self, is_creating_backup: bool = False) -> bool: """Updates Patroni config file based on the existence of the TLS files.""" if ( @@ -1424,6 +1456,10 @@ def update_config(self, is_creating_backup: bool = False) -> bool: logger.debug("Early exit update_config: Patroni not started yet") return False + # Try to connect + if not self._can_connect_to_postgresql: + logger.warning("Early exit update_config: Cannot connect to Postgresql") + return False self._validate_config_options() self._patroni.bulk_update_parameters_controller_by_patroni( diff --git a/tests/integration/test_db_admin.py b/tests/integration/test_db_admin.py index eb0d2291db..4c7868035b 100644 --- a/tests/integration/test_db_admin.py +++ b/tests/integration/test_db_admin.py @@ -34,7 +34,6 @@ @pytest.mark.group(1) -@pytest.mark.skip(reason="DB Admin tests are currently broken") async def test_landscape_scalable_bundle_db(ops_test: OpsTest, charm: str) -> None: """Deploy Landscape Scalable Bundle to test the 'db-admin' relation.""" await ops_test.model.deploy( diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py index a02e4cb1e6..c8e2ba912d 100644 --- a/tests/unit/test_charm.py +++ b/tests/unit/test_charm.py @@ -5,7 +5,7 @@ import platform import subprocess import unittest -from unittest.mock import MagicMock, Mock, PropertyMock, call, mock_open, patch +from unittest.mock import MagicMock, Mock, PropertyMock, call, mock_open, patch, sentinel import pytest from charms.operator_libs_linux.v2 import snap @@ -179,6 +179,33 @@ def test_patroni_scrape_config_tls(self, _): }, ] + @patch( + "charm.PostgresqlOperatorCharm._units_ips", + new_callable=PropertyMock, + return_value={"1.1.1.1", "1.1.1.2"}, + ) + @patch("charm.PostgresqlOperatorCharm._patroni", new_callable=PropertyMock) + def test_primary_endpoint(self, _patroni, _): + _patroni.return_value.get_member_ip.return_value = "1.1.1.1" + _patroni.return_value.get_primary.return_value = sentinel.primary + assert self.charm.primary_endpoint == "1.1.1.1" + + _patroni.return_value.get_member_ip.assert_called_once_with(sentinel.primary) + _patroni.return_value.get_primary.assert_called_once_with() + + @patch("charm.PostgresqlOperatorCharm._peers", new_callable=PropertyMock, return_value=None) + @patch( + "charm.PostgresqlOperatorCharm._units_ips", + new_callable=PropertyMock, + return_value={"1.1.1.1", "1.1.1.2"}, + ) + @patch("charm.PostgresqlOperatorCharm._patroni", new_callable=PropertyMock) + def test_primary_endpoint_no_peers(self, _patroni, _, __): + assert self.charm.primary_endpoint is None + + assert not _patroni.return_value.get_member_ip.called + assert not _patroni.return_value.get_primary.called + @patch("charm.PostgresqlOperatorCharm._update_relation_endpoints", new_callable=PropertyMock) @patch( "charm.PostgresqlOperatorCharm.primary_endpoint",