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
63 changes: 52 additions & 11 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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."""
Expand All @@ -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:
Expand All @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions src/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
42 changes: 41 additions & 1 deletion tests/unit/test_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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"
)