Skip to content

Commit 48e352b

Browse files
Rework secrets (canonical#27)
* Rework secrets management * Add unit tests * Add constants
1 parent a755b18 commit 48e352b

File tree

3 files changed

+97
-19
lines changed

3 files changed

+97
-19
lines changed

src/charm.py

Lines changed: 51 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 PostgreSQL
1111
from lightkube import ApiError, Client, codecs
@@ -30,7 +30,7 @@
3030
from requests import ConnectionError
3131
from tenacity import RetryError
3232

33-
from constants import PEER, USER
33+
from constants import PEER, REPLICATION_PASSWORD_KEY, USER, USER_PASSWORD_KEY
3434
from patroni import NotReadyError, Patroni
3535
from relations.db import DbProvides
3636
from relations.postgresql_provider import PostgreSQLProvider
@@ -69,6 +69,48 @@ def __init__(self, *args):
6969
self.legacy_db_relation = DbProvides(self, admin=False)
7070
self.legacy_db_admin_relation = DbProvides(self, admin=True)
7171

72+
@property
73+
def app_peer_data(self) -> Dict:
74+
"""Application peer relation data object."""
75+
relation = self.model.get_relation(PEER)
76+
if relation is None:
77+
return {}
78+
79+
return relation.data[self.app]
80+
81+
@property
82+
def unit_peer_data(self) -> Dict:
83+
"""Unit peer relation data object."""
84+
relation = self.model.get_relation(PEER)
85+
if relation is None:
86+
return {}
87+
88+
return relation.data[self.unit]
89+
90+
def _get_secret(self, scope: str, key: str) -> Optional[str]:
91+
"""Get secret from the secret storage."""
92+
if scope == "unit":
93+
return self.unit_peer_data.get(key, None)
94+
elif scope == "app":
95+
return self.app_peer_data.get(key, None)
96+
else:
97+
raise RuntimeError("Unknown secret scope.")
98+
99+
def _set_secret(self, scope: str, key: str, value: Optional[str]) -> None:
100+
"""Get secret from the secret storage."""
101+
if scope == "unit":
102+
if not value:
103+
del self.unit_peer_data[key]
104+
return
105+
self.unit_peer_data.update({key: value})
106+
elif scope == "app":
107+
if not value:
108+
del self.app_peer_data[key]
109+
return
110+
self.app_peer_data.update({key: value})
111+
else:
112+
raise RuntimeError("Unknown secret scope.")
113+
72114
@property
73115
def postgresql(self) -> PostgreSQL:
74116
"""Returns an instance of the object used to interact with the database."""
@@ -249,15 +291,11 @@ def _get_hostname_from_unit(self, member: str) -> str:
249291

250292
def _on_leader_elected(self, event: LeaderElectedEvent) -> None:
251293
"""Handle the leader-elected event."""
252-
data = self._peers.data[self.app]
253-
operator_password = data.get("operator-password", None)
254-
replication_password = data.get("replication-password", None)
255-
256-
if operator_password is None:
257-
self._peers.data[self.app]["operator-password"] = new_password()
294+
if self._get_secret("app", USER_PASSWORD_KEY) is None:
295+
self._set_secret("app", USER_PASSWORD_KEY, new_password())
258296

259-
if replication_password is None:
260-
self._peers.data[self.app]["replication-password"] = new_password()
297+
if self._get_secret("app", REPLICATION_PASSWORD_KEY) is None:
298+
self._set_secret("app", REPLICATION_PASSWORD_KEY, new_password())
261299

262300
# Create resources and add labels needed for replication.
263301
self._create_resources()
@@ -389,7 +427,7 @@ def _create_resources(self) -> None:
389427

390428
def _on_get_operator_password(self, event: ActionEvent) -> None:
391429
"""Returns the password for the operator user as an action response."""
392-
event.set_results({"operator-password": self._get_operator_password()})
430+
event.set_results({USER_PASSWORD_KEY: self._get_operator_password()})
393431

394432
def _on_get_primary(self, event: ActionEvent) -> None:
395433
"""Get primary instance."""
@@ -498,14 +536,12 @@ def _peers(self) -> Relation:
498536

499537
def _get_operator_password(self) -> str:
500538
"""Get operator user password."""
501-
data = self._peers.data[self.app]
502-
return data.get("operator-password", None)
539+
return self._get_secret("app", USER_PASSWORD_KEY)
503540

504541
@property
505542
def _replication_password(self) -> str:
506543
"""Get replication user password."""
507-
data = self._peers.data[self.app]
508-
return data.get("replication-password", None)
544+
return self._get_secret("app", REPLICATION_PASSWORD_KEY)
509545

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

src/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,6 @@
55

66
DATABASE_PORT = "5432"
77
PEER = "database-peers"
8+
REPLICATION_PASSWORD_KEY = "replication-password"
89
USER = "operator"
10+
USER_PASSWORD_KEY = "operator-password"

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)