Skip to content

Commit e345432

Browse files
committed
Merge branch 'rework-secrets' into password-rotation
2 parents ca8f525 + 36640b2 commit e345432

File tree

3 files changed

+104
-19
lines changed

3 files changed

+104
-19
lines changed

src/charm.py

Lines changed: 58 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"""Charmed Kubernetes Operator for the PostgreSQL database."""
66
import json
77
import logging
8-
from typing import List
8+
from typing import Dict, List, Optional
99

1010
from charms.postgresql_k8s.v0.postgresql import (
1111
PostgreSQL,
@@ -33,7 +33,14 @@
3333
from requests import ConnectionError
3434
from tenacity import RetryError
3535

36-
from constants import PEER, REPLICATION_USER, SYSTEM_USERS, USER
36+
from constants import (
37+
PEER,
38+
REPLICATION_PASSWORD_KEY,
39+
REPLICATION_USER,
40+
SYSTEM_USERS,
41+
USER,
42+
USER_PASSWORD_KEY,
43+
)
3744
from patroni import NotReadyError, Patroni
3845
from relations.db import DbProvides
3946
from relations.postgresql_provider import PostgreSQLProvider
@@ -75,6 +82,48 @@ def __init__(self, *args):
7582
self.legacy_db_relation = DbProvides(self, admin=False)
7683
self.legacy_db_admin_relation = DbProvides(self, admin=True)
7784

85+
@property
86+
def app_peer_data(self) -> Dict:
87+
"""Application peer relation data object."""
88+
relation = self.model.get_relation(PEER)
89+
if relation is None:
90+
return {}
91+
92+
return relation.data[self.app]
93+
94+
@property
95+
def unit_peer_data(self) -> Dict:
96+
"""Unit peer relation data object."""
97+
relation = self.model.get_relation(PEER)
98+
if relation is None:
99+
return {}
100+
101+
return relation.data[self.unit]
102+
103+
def _get_secret(self, scope: str, key: str) -> Optional[str]:
104+
"""Get secret from the secret storage."""
105+
if scope == "unit":
106+
return self.unit_peer_data.get(key, None)
107+
elif scope == "app":
108+
return self.app_peer_data.get(key, None)
109+
else:
110+
raise RuntimeError("Unknown secret scope.")
111+
112+
def _set_secret(self, scope: str, key: str, value: Optional[str]) -> None:
113+
"""Get secret from the secret storage."""
114+
if scope == "unit":
115+
if not value:
116+
del self.unit_peer_data[key]
117+
return
118+
self.unit_peer_data.update({key: value})
119+
elif scope == "app":
120+
if not value:
121+
del self.app_peer_data[key]
122+
return
123+
self.app_peer_data.update({key: value})
124+
else:
125+
raise RuntimeError("Unknown secret scope.")
126+
78127
@property
79128
def postgresql(self) -> PostgreSQL:
80129
"""Returns an instance of the object used to interact with the database."""
@@ -255,15 +304,11 @@ def _get_hostname_from_unit(self, member: str) -> str:
255304

256305
def _on_leader_elected(self, event: LeaderElectedEvent) -> None:
257306
"""Handle the leader-elected event."""
258-
data = self._peers.data[self.app]
259-
operator_password = data.get("operator-password", None)
260-
replication_password = data.get("replication-password", None)
261-
262-
if operator_password is None:
263-
self._peers.data[self.app]["operator-password"] = new_password()
307+
if self._get_secret("app", USER_PASSWORD_KEY) is None:
308+
self._set_secret("app", USER_PASSWORD_KEY, new_password())
264309

265-
if replication_password is None:
266-
self._peers.data[self.app]["replication-password"] = new_password()
310+
if self._get_secret("app", REPLICATION_PASSWORD_KEY) is None:
311+
self._set_secret("app", REPLICATION_PASSWORD_KEY, new_password())
267312

268313
# Create resources and add labels needed for replication.
269314
self._create_resources()
@@ -395,7 +440,7 @@ def _create_resources(self) -> None:
395440

396441
def _on_get_operator_password(self, event: ActionEvent) -> None:
397442
"""Returns the password for the operator user as an action response."""
398-
event.set_results({"operator-password": self._get_operator_password()})
443+
event.set_results({USER_PASSWORD_KEY: self._get_operator_password()})
399444

400445
def _on_rotate_users_passwords(self, event: ActionEvent) -> None:
401446
"""Rotate the password for all system users or the specified user."""
@@ -551,14 +596,12 @@ def _peers(self) -> Relation:
551596

552597
def _get_operator_password(self) -> str:
553598
"""Get operator user password."""
554-
data = self._peers.data[self.app]
555-
return data.get("operator-password", None)
599+
return self._get_secret("app", USER_PASSWORD_KEY)
556600

557601
@property
558602
def _replication_password(self) -> str:
559603
"""Get replication user password."""
560-
data = self._peers.data[self.app]
561-
return data.get("replication-password", None)
604+
return self._get_secret("app", REPLICATION_PASSWORD_KEY)
562605

563606
def _unit_name_to_pod_name(self, unit_name: str) -> str:
564607
"""Converts unit name to pod name.

src/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,7 @@
66
DATABASE_PORT = "5432"
77
PEER = "database-peers"
88
REPLICATION_USER = "replication"
9+
REPLICATION_PASSWORD_KEY = "replication-password"
910
USER = "operator"
11+
USER_PASSWORD_KEY = "operator-password"
1012
SYSTEM_USERS = [REPLICATION_USER, USER]

tests/unit/test_charm.py

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ def setUp(self):
3131
"app_name": self.harness.model.app.name,
3232
}
3333

34+
self.rel_id = self.harness.add_relation(self._peer_relation, self.charm.app.name)
35+
3436
@patch_network_get(private_address="1.1.1.1")
3537
@patch("charm.Patroni.render_postgresql_conf_file")
3638
def test_on_install(
@@ -46,7 +48,6 @@ def test_on_install(
4648
@patch("charm.PostgresqlOperatorCharm._create_resources")
4749
def test_on_leader_elected(self, _, __, _render_postgresql_conf_file, ___):
4850
# Assert that there is no password in the peer relation.
49-
self.harness.add_relation(self._peer_relation, self.charm.app.name)
5051
self.assertIsNone(self.charm._peers.data[self.charm.app].get("postgres-password", None))
5152
self.assertIsNone(self.charm._peers.data[self.charm.app].get("replication-password", None))
5253

@@ -86,7 +87,6 @@ def test_on_postgresql_pebble_ready(
8687
# Check that the initial plan is empty.
8788
plan = self.harness.get_container_pebble_plan(self._postgresql_container)
8889
self.assertEqual(plan.to_dict(), {})
89-
self.harness.add_relation(self._peer_relation, self.charm.app.name)
9090

9191
# Get the current and the expected layer from the pebble plan and the _postgresql_layer
9292
# method, respectively.
@@ -202,7 +202,6 @@ def test_patch_pod_labels(self, _client):
202202
@patch("charm.PostgresqlOperatorCharm._create_resources")
203203
def test_postgresql_layer(self, _, __, ___, ____):
204204
# Test with the already generated password.
205-
self.harness.add_relation(self._peer_relation, self.charm.app.name)
206205
self.harness.set_leader()
207206
plan = self.charm._postgresql_layer().to_dict()
208207
expected = {
@@ -238,11 +237,52 @@ def test_postgresql_layer(self, _, __, ___, ____):
238237
@patch("charm.PostgresqlOperatorCharm._create_resources")
239238
def test_get_operator_password(self, _, __, ___, ____):
240239
# Test for a None password.
241-
self.harness.add_relation(self._peer_relation, self.charm.app.name)
242240
self.assertIsNone(self.charm._get_operator_password())
243241

244242
# Then test for a non empty password after leader election and peer data set.
245243
self.harness.set_leader()
246244
password = self.charm._get_operator_password()
247245
self.assertIsNotNone(password)
248246
self.assertNotEqual(password, "")
247+
248+
@patch("charm.Patroni.reload_patroni_configuration")
249+
@patch("charm.Patroni.render_postgresql_conf_file")
250+
@patch("charm.PostgresqlOperatorCharm._create_resources")
251+
def test_get_secret(self, _, __, ___):
252+
self.harness.set_leader()
253+
254+
# Test application scope.
255+
assert self.charm._get_secret("app", "password") is None
256+
self.harness.update_relation_data(
257+
self.rel_id, self.charm.app.name, {"password": "test-password"}
258+
)
259+
assert self.charm._get_secret("app", "password") == "test-password"
260+
261+
# Test unit scope.
262+
assert self.charm._get_secret("unit", "password") is None
263+
self.harness.update_relation_data(
264+
self.rel_id, self.charm.unit.name, {"password": "test-password"}
265+
)
266+
assert self.charm._get_secret("unit", "password") == "test-password"
267+
268+
@patch("charm.Patroni.reload_patroni_configuration")
269+
@patch("charm.Patroni.render_postgresql_conf_file")
270+
@patch("charm.PostgresqlOperatorCharm._create_resources")
271+
def test_set_secret(self, _, __, ___):
272+
self.harness.set_leader()
273+
274+
# Test application scope.
275+
assert "password" not in self.harness.get_relation_data(self.rel_id, self.charm.app.name)
276+
self.charm._set_secret("app", "password", "test-password")
277+
assert (
278+
self.harness.get_relation_data(self.rel_id, self.charm.app.name)["password"]
279+
== "test-password"
280+
)
281+
282+
# Test unit scope.
283+
assert "password" not in self.harness.get_relation_data(self.rel_id, self.charm.unit.name)
284+
self.charm._set_secret("unit", "password", "test-password")
285+
assert (
286+
self.harness.get_relation_data(self.rel_id, self.charm.unit.name)["password"]
287+
== "test-password"
288+
)

0 commit comments

Comments
 (0)