diff --git a/src/charm.py b/src/charm.py index 3bc82ce0d4..b09a6f87b1 100755 --- a/src/charm.py +++ b/src/charm.py @@ -7,7 +7,7 @@ import logging import os import subprocess -from typing import List, Optional, Set +from typing import Dict, List, Optional, Set from charms.operator_libs_linux.v0 import apt from charms.postgresql_k8s.v0.postgresql import PostgreSQL, PostgreSQLCreateUserError @@ -37,7 +37,7 @@ RemoveRaftMemberFailedError, SwitchoverFailedError, ) -from constants import PEER, USER +from constants import PEER, REPLICATION_PASSWORD_KEY, USER, USER_PASSWORD_KEY from relations.db import DbProvides from relations.postgresql_provider import PostgreSQLProvider from utils import new_password @@ -73,6 +73,48 @@ def __init__(self, *args): self.legacy_db_relation = DbProvides(self, admin=False) self.legacy_db_admin_relation = DbProvides(self, admin=True) + @property + def app_peer_data(self) -> Dict: + """Application peer relation data object.""" + relation = self.model.get_relation(PEER) + if relation is None: + return {} + + return relation.data[self.app] + + @property + def unit_peer_data(self) -> Dict: + """Unit peer relation data object.""" + relation = self.model.get_relation(PEER) + if relation is None: + return {} + + return relation.data[self.unit] + + def _get_secret(self, scope: str, key: str) -> Optional[str]: + """Get secret from the secret storage.""" + if scope == "unit": + return self.unit_peer_data.get(key, None) + elif scope == "app": + return self.app_peer_data.get(key, None) + else: + raise RuntimeError("Unknown secret scope.") + + def _set_secret(self, scope: str, key: str, value: Optional[str]) -> None: + """Get secret from the secret storage.""" + if scope == "unit": + if not value: + del self.unit_peer_data[key] + return + self.unit_peer_data.update({key: value}) + elif scope == "app": + if not value: + del self.app_peer_data[key] + return + self.app_peer_data.update({key: value}) + else: + raise RuntimeError("Unknown secret scope.") + @property def postgresql(self) -> PostgreSQL: """Returns an instance of the object used to interact with the database.""" @@ -468,10 +510,11 @@ def _inhibit_default_cluster_creation(self) -> None: def _on_leader_elected(self, event: LeaderElectedEvent) -> None: """Handle the leader-elected event.""" - data = self._peers.data[self.app] - # The leader sets the needed password on peer relation databag if they weren't set before. - data.setdefault("operator-password", new_password()) - data.setdefault("replication-password", new_password()) + # The leader sets the needed passwords if they weren't set before. + if self._get_secret("app", USER_PASSWORD_KEY) is None: + self._set_secret("app", USER_PASSWORD_KEY, new_password()) + if self._get_secret("app", REPLICATION_PASSWORD_KEY) is None: + self._set_secret("app", REPLICATION_PASSWORD_KEY, new_password()) # Update the list of the current PostgreSQL hosts when a new leader is elected. # Add this unit to the list of cluster members @@ -570,7 +613,7 @@ def _on_start(self, event) -> None: def _on_get_password(self, event: ActionEvent) -> None: """Returns the password for the operator user as an action response.""" - event.set_results({"operator-password": self._get_password()}) + event.set_results({USER_PASSWORD_KEY: self._get_password()}) def _on_update_status(self, _) -> None: """Update endpoints of the postgres client relation and update users list.""" @@ -591,8 +634,7 @@ def _get_password(self) -> str: The password from the peer relation or None if the password has not yet been set by the leader. """ - data = self._peers.data[self.app] - return data.get("operator-password") + return self._get_secret("app", USER_PASSWORD_KEY) @property def _replication_password(self) -> str: @@ -602,8 +644,7 @@ def _replication_password(self) -> str: The password from the peer relation or None if the password has not yet been set by the leader. """ - data = self._peers.data[self.app] - return data.get("replication-password") + return self._get_secret("app", REPLICATION_PASSWORD_KEY) def _install_apt_packages(self, _, packages: List[str]) -> None: """Simple wrapper around 'apt-get install -y. diff --git a/src/constants.py b/src/constants.py index 1dea2ef46c..8fcc127f29 100644 --- a/src/constants.py +++ b/src/constants.py @@ -9,4 +9,6 @@ LEGACY_DB_ADMIN = "db-admin" PEER = "database-peers" ALL_CLIENT_RELATIONS = [DATABASE, LEGACY_DB, LEGACY_DB_ADMIN] +REPLICATION_PASSWORD_KEY = "replication-password" USER = "operator" +USER_PASSWORD_KEY = "operator-password" diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py index 5e69bf0a8e..41fd7ac032 100644 --- a/tests/unit/test_charm.py +++ b/tests/unit/test_charm.py @@ -27,7 +27,7 @@ def setUp(self): self.addCleanup(self.harness.cleanup) self.harness.begin() self.charm = self.harness.charm - self.harness.add_relation(self._peer_relation, self.charm.app.name) + self.rel_id = self.harness.add_relation(self._peer_relation, self.charm.app.name) @patch_network_get(private_address="1.1.1.1") @patch("charm.PostgresqlOperatorCharm._install_pip_packages") @@ -305,3 +305,43 @@ def test_install_pip_packages(self, _call): # Then, test for an error. with self.assertRaises(subprocess.SubprocessError): self.charm._install_pip_packages(packages) + + @patch_network_get(private_address="1.1.1.1") + @patch("charm.PostgresqlOperatorCharm._on_leader_elected") + def test_get_secret(self, _): + self.harness.set_leader() + + # Test application scope. + assert self.charm._get_secret("app", "password") is None + self.harness.update_relation_data( + self.rel_id, self.charm.app.name, {"password": "test-password"} + ) + assert self.charm._get_secret("app", "password") == "test-password" + + # Test unit scope. + assert self.charm._get_secret("unit", "password") is None + self.harness.update_relation_data( + self.rel_id, self.charm.unit.name, {"password": "test-password"} + ) + assert self.charm._get_secret("unit", "password") == "test-password" + + @patch_network_get(private_address="1.1.1.1") + @patch("charm.PostgresqlOperatorCharm._on_leader_elected") + def test_set_secret(self, _): + self.harness.set_leader() + + # Test application scope. + assert "password" not in self.harness.get_relation_data(self.rel_id, self.charm.app.name) + self.charm._set_secret("app", "password", "test-password") + assert ( + self.harness.get_relation_data(self.rel_id, self.charm.app.name)["password"] + == "test-password" + ) + + # Test unit scope. + assert "password" not in self.harness.get_relation_data(self.rel_id, self.charm.unit.name) + self.charm._set_secret("unit", "password", "test-password") + assert ( + self.harness.get_relation_data(self.rel_id, self.charm.unit.name)["password"] + == "test-password" + )