Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 75 additions & 20 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from datetime import datetime
from pathlib import Path
from typing import Literal, get_args
from urllib.parse import urlparse

import psycopg2
from charms.data_platform_libs.v0.data_interfaces import DataPeerData, DataPeerUnitData
Expand Down Expand Up @@ -73,6 +74,7 @@
APP_SCOPE,
BACKUP_USER,
DATABASE_DEFAULT_NAME,
DATABASE_PORT,
METRICS_PORT,
MONITORING_PASSWORD_KEY,
MONITORING_SNAP_SERVICE,
Expand Down Expand Up @@ -110,7 +112,7 @@
from relations.postgresql_provider import PostgreSQLProvider
from rotate_logs import RotateLogs
from upgrade import PostgreSQLUpgrade, get_postgresql_dependencies_model
from utils import new_password
from utils import new_password, snap_refreshed

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -1316,29 +1318,86 @@ def _restart_services_after_reboot(self):
self._patroni.start_patroni()
self.backup.start_stop_pgbackrest_service()

def _setup_exporter(self) -> None:
"""Set up postgresql_exporter options."""
cache = snap.SnapCache()
postgres_snap = cache[POSTGRESQL_SNAP_NAME]
def _restart_metrics_service(self, postgres_snap: snap.Snap) -> None:
"""Restart the monitoring service if the password was rotated."""
try:
snap_password = postgres_snap.get("exporter.password")
except snap.SnapError:
logger.warning("Early exit: Trying to reset metrics service with no configuration set")
return None

if postgres_snap.revision != next(
filter(lambda snap_package: snap_package[0] == POSTGRESQL_SNAP_NAME, SNAP_PACKAGES)
)[1]["revision"].get(platform.machine()):
logger.debug(
"Early exit _setup_exporter: snap was not refreshed to the right version yet"
)
if snap_password != self.get_secret(APP_SCOPE, MONITORING_PASSWORD_KEY):
self._setup_exporter(postgres_snap)

def _restart_ldap_sync_service(self, postgres_snap: snap.Snap) -> None:
"""Restart the LDAP sync service in case any configuration changed."""
if not self._patroni.member_started:
logger.debug("Restart LDAP sync early exit: Patroni has not started yet")
return

sync_service = postgres_snap.services["ldap-sync"]

if not self.is_primary and sync_service["active"]:
logger.debug("Stopping LDAP sync service. It must only run in the primary")
postgres_snap.stop(services=["ldap-sync"])

if self.is_primary and not self.is_ldap_enabled:
logger.debug("Stopping LDAP sync service")
postgres_snap.stop(services=["ldap-sync"])
return

if self.is_primary and self.is_ldap_enabled:
self._setup_ldap_sync(postgres_snap)

def _setup_exporter(self, postgres_snap: snap.Snap | None = None) -> None:
"""Set up postgresql_exporter options."""
if postgres_snap is None:
cache = snap.SnapCache()
postgres_snap = cache[POSTGRESQL_SNAP_NAME]

postgres_snap.set({
"exporter.user": MONITORING_USER,
"exporter.password": self.get_secret(APP_SCOPE, MONITORING_PASSWORD_KEY),
})

if postgres_snap.services[MONITORING_SNAP_SERVICE]["active"] is False:
postgres_snap.start(services=[MONITORING_SNAP_SERVICE], enable=True)
else:
postgres_snap.restart(services=[MONITORING_SNAP_SERVICE])

self.unit_peer_data.update({"exporter-started": "True"})

def _setup_ldap_sync(self, postgres_snap: snap.Snap | None = None) -> None:
"""Set up postgresql_ldap_sync options."""
if postgres_snap is None:
cache = snap.SnapCache()
postgres_snap = cache[POSTGRESQL_SNAP_NAME]

ldap_params = self.get_ldap_parameters()
ldap_url = urlparse(ldap_params["ldapurl"])
ldap_host = ldap_url.hostname
ldap_port = ldap_url.port

ldap_base_dn = ldap_params["ldapbasedn"]
ldap_bind_username = ldap_params["ldapbinddn"]
ldap_bind_password = ldap_params["ldapbindpasswd"]

postgres_snap.set({
"ldap-sync.ldap_host": ldap_host,
"ldap-sync.ldap_port": ldap_port,
"ldap-sync.ldap_base_dn": ldap_base_dn,
"ldap-sync.ldap_bind_username": ldap_bind_username,
"ldap-sync.ldap_bind_password": ldap_bind_password,
"ldap-sync.postgres_host": "127.0.0.1",
"ldap-sync.postgres_port": DATABASE_PORT,
"ldap-sync.postgres_database": DATABASE_DEFAULT_NAME,
"ldap-sync.postgres_username": USER,
"ldap-sync.postgres_password": self._get_password(),
})

logger.debug("Starting LDAP sync service")
postgres_snap.restart(services=["ldap-sync"])

def _start_primary(self, event: StartEvent) -> None:
"""Bootstrap the cluster."""
# Set some information needed by Patroni to bootstrap the cluster.
Expand Down Expand Up @@ -1986,19 +2045,15 @@ def update_config(self, is_creating_backup: bool = False, no_peers: bool = False

self._handle_postgresql_restart_need(enable_tls)

# Restart the monitoring service if the password was rotated
cache = snap.SnapCache()
postgres_snap = cache[POSTGRESQL_SNAP_NAME]

try:
snap_password = postgres_snap.get("exporter.password")
except snap.SnapError:
logger.warning(
"Early exit update_config: Trying to reset metrics service with no configuration set"
)
if not snap_refreshed(postgres_snap.revision):
logger.debug("Early exit: snap was not refreshed to the right version yet")
return True
if snap_password != self.get_secret(APP_SCOPE, MONITORING_PASSWORD_KEY):
self._setup_exporter()

self._restart_metrics_service(postgres_snap)
self._restart_ldap_sync_service(postgres_snap)

return True

Expand Down
19 changes: 19 additions & 0 deletions src/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,15 @@

"""A collection of utility functions that are used in the charm."""

import platform
import secrets
import string

from constants import (
POSTGRESQL_SNAP_NAME,
SNAP_PACKAGES,
)


def new_password() -> str:
"""Generate a random password string.
Expand All @@ -16,3 +22,16 @@ def new_password() -> str:
choices = string.ascii_letters + string.digits
password = "".join([secrets.choice(choices) for i in range(16)])
return password


def snap_refreshed(target_rev: str) -> bool:
"""Whether the snap was refreshed to the target version."""
arch = platform.machine()

for snap_package in SNAP_PACKAGES:
snap_name = snap_package[0]
snap_revs = snap_package[1]["revision"]
if snap_name == POSTGRESQL_SNAP_NAME and target_rev != snap_revs.get(arch):
return False

return True
17 changes: 17 additions & 0 deletions tests/unit/test_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -1270,10 +1270,17 @@ def test_restart(harness):
def test_update_config(harness):
with (
patch("subprocess.check_output", return_value=b"C"),
patch("charm.snap_refreshed", return_value=True),
patch("charm.snap.SnapCache"),
patch(
"charm.PostgresqlOperatorCharm._handle_postgresql_restart_need"
) as _handle_postgresql_restart_need,
patch(
"charm.PostgresqlOperatorCharm._restart_metrics_service"
) as _restart_metrics_service,
patch(
"charm.PostgresqlOperatorCharm._restart_ldap_sync_service"
) as _restart_ldap_sync_service,
patch("charm.Patroni.bulk_update_parameters_controller_by_patroni"),
patch("charm.Patroni.member_started", new_callable=PropertyMock) as _member_started,
patch(
Expand Down Expand Up @@ -1313,10 +1320,14 @@ def test_update_config(harness):
no_peers=False,
)
_handle_postgresql_restart_need.assert_called_once_with(False)
_restart_ldap_sync_service.assert_called_once()
_restart_metrics_service.assert_called_once()
assert "tls" not in harness.get_relation_data(rel_id, harness.charm.unit.name)

# Test with TLS files available.
_handle_postgresql_restart_need.reset_mock()
_restart_ldap_sync_service.reset_mock()
_restart_metrics_service.reset_mock()
harness.update_relation_data(
rel_id, harness.charm.unit.name, {"tls": ""}
) # Mock some data in the relation to test that it change.
Expand All @@ -1338,6 +1349,8 @@ def test_update_config(harness):
no_peers=False,
)
_handle_postgresql_restart_need.assert_called_once()
_restart_ldap_sync_service.assert_called_once()
_restart_metrics_service.assert_called_once()
assert "tls" not in harness.get_relation_data(
rel_id, harness.charm.unit.name
) # The "tls" flag is set in handle_postgresql_restart_need.
Expand All @@ -1347,6 +1360,8 @@ def test_update_config(harness):
rel_id, harness.charm.unit.name, {"tls": ""}
) # Mock some data in the relation to test that it change.
_handle_postgresql_restart_need.reset_mock()
_restart_ldap_sync_service.reset_mock()
_restart_metrics_service.reset_mock()
harness.charm.update_config()
_handle_postgresql_restart_need.assert_not_called()
assert harness.get_relation_data(rel_id, harness.charm.unit.name)["tls"] == "enabled"
Expand All @@ -1357,6 +1372,8 @@ def test_update_config(harness):
) # Mock some data in the relation to test that it doesn't change.
harness.charm.update_config()
_handle_postgresql_restart_need.assert_not_called()
_restart_ldap_sync_service.assert_not_called()
_restart_metrics_service.assert_not_called()
assert "tls" not in harness.get_relation_data(rel_id, harness.charm.unit.name)


Expand Down
20 changes: 19 additions & 1 deletion tests/unit/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
# See LICENSE file for licensing details.

import re
from unittest.mock import patch

from utils import new_password
from constants import POSTGRESQL_SNAP_NAME
from utils import new_password, snap_refreshed


def test_new_password():
Expand All @@ -16,3 +18,19 @@ def test_new_password():
second_password = new_password()
assert re.fullmatch("[a-zA-Z0-9\b]{16}$", second_password) is not None
assert second_password != first_password


def test_snap_refreshed():
with patch(
"utils.SNAP_PACKAGES",
[(POSTGRESQL_SNAP_NAME, {"revision": {"aarch64": "100", "x86_64": "100"}})],
):
assert snap_refreshed("100") is True
assert snap_refreshed("200") is False

with patch(
"utils.SNAP_PACKAGES",
[(POSTGRESQL_SNAP_NAME, {"revision": {}})],
):
assert snap_refreshed("100") is False
assert snap_refreshed("200") is False