Skip to content

Commit e7c1809

Browse files
[DPE-3705, DPE-3542] Reintroduce fallback keys and fix TLS secrets initialization (#427)
* Reintroduce fallback keys and fix TLS secrets initialization * Update mysql libpatch * Fix bug in implementation to avoid overwriting secret value in set_secret * Update data_interfaces charm lib to v0.31 * Remove secret with fallback key in set_secret * Fix typo
1 parent bf48660 commit e7c1809

File tree

3 files changed

+168
-22
lines changed

3 files changed

+168
-22
lines changed

lib/charms/data_platform_libs/v0/data_interfaces.py

+125-6
Original file line numberDiff line numberDiff line change
@@ -295,10 +295,21 @@ def _on_topic_requested(self, event: TopicRequestedEvent):
295295
import json
296296
import logging
297297
from abc import ABC, abstractmethod
298-
from collections import namedtuple
298+
from collections import UserDict, namedtuple
299299
from datetime import datetime
300300
from enum import Enum
301-
from typing import Callable, Dict, List, Optional, Set, Tuple, Union
301+
from typing import (
302+
Callable,
303+
Dict,
304+
ItemsView,
305+
KeysView,
306+
List,
307+
Optional,
308+
Set,
309+
Tuple,
310+
Union,
311+
ValuesView,
312+
)
302313

303314
from ops import JujuVersion, Model, Secret, SecretInfo, SecretNotFoundError
304315
from ops.charm import (
@@ -320,7 +331,7 @@ def _on_topic_requested(self, event: TopicRequestedEvent):
320331

321332
# Increment this PATCH version before using `charmcraft publish-lib` or reset
322333
# to 0 if you are raising the major API version
323-
LIBPATCH = 29
334+
LIBPATCH = 31
324335

325336
PYDEPS = ["ops>=2.0.0"]
326337

@@ -612,6 +623,102 @@ def remove(self, label: str) -> None:
612623
# Base Data
613624

614625

626+
class DataDict(UserDict[str, str]):
627+
"""Python Standard Library 'dict' - like representation of Relation Data."""
628+
629+
def __init__(self, relation_data: "Data", relation_id: int):
630+
self.relation_data = relation_data
631+
self.relation_id = relation_id
632+
633+
@property
634+
def data(self) -> Dict[str, str]:
635+
"""Return the full content of the Abstract Relation Data dictionary."""
636+
result = self.relation_data.fetch_my_relation_data([self.relation_id])
637+
try:
638+
result_remote = self.relation_data.fetch_relation_data([self.relation_id])
639+
except NotImplementedError:
640+
result_remote = {self.relation_id: {}}
641+
if result:
642+
result_remote[self.relation_id].update(result[self.relation_id])
643+
return result_remote.get(self.relation_id, {})
644+
645+
def __setitem__(self, key: str, item: str) -> None:
646+
"""Set an item of the Abstract Relation Data dictionary."""
647+
self.relation_data.update_relation_data(self.relation_id, {key: item})
648+
649+
def __getitem__(self, key: str) -> str:
650+
"""Get an item of the Abstract Relation Data dictionary."""
651+
result = None
652+
if not (result := self.relation_data.fetch_my_relation_field(self.relation_id, key)):
653+
try:
654+
result = self.relation_data.fetch_relation_field(self.relation_id, key)
655+
except NotImplementedError:
656+
pass
657+
if not result:
658+
raise KeyError
659+
return result
660+
661+
def __eq__(self, d: dict) -> bool:
662+
"""Equality."""
663+
return self.data == d
664+
665+
def __repr__(self) -> str:
666+
"""String representation Abstract Relation Data dictionary."""
667+
return repr(self.data)
668+
669+
def __len__(self) -> int:
670+
"""Length of the Abstract Relation Data dictionary."""
671+
return len(self.data)
672+
673+
def __delitem__(self, key: str) -> None:
674+
"""Delete an item of the Abstract Relation Data dictionary."""
675+
self.relation_data.delete_relation_data(self.relation_id, [key])
676+
677+
def has_key(self, key: str) -> bool:
678+
"""Does the key exist in the Abstract Relation Data dictionary?"""
679+
return key in self.data
680+
681+
def update(self, items: Dict[str, str]):
682+
"""Update the Abstract Relation Data dictionary."""
683+
self.relation_data.update_relation_data(self.relation_id, items)
684+
685+
def keys(self) -> KeysView[str]:
686+
"""Keys of the Abstract Relation Data dictionary."""
687+
return self.data.keys()
688+
689+
def values(self) -> ValuesView[str]:
690+
"""Values of the Abstract Relation Data dictionary."""
691+
return self.data.values()
692+
693+
def items(self) -> ItemsView[str, str]:
694+
"""Items of the Abstract Relation Data dictionary."""
695+
return self.data.items()
696+
697+
def pop(self, item: str) -> str:
698+
"""Pop an item of the Abstract Relation Data dictionary."""
699+
result = self.relation_data.fetch_my_relation_field(self.relation_id, item)
700+
if not result:
701+
raise KeyError(f"Item {item} doesn't exist.")
702+
self.relation_data.delete_relation_data(self.relation_id, [item])
703+
return result
704+
705+
def __contains__(self, item: str) -> bool:
706+
"""Does the Abstract Relation Data dictionary contain item?"""
707+
return item in self.data.values()
708+
709+
def __iter__(self):
710+
"""Iterate through the Abstract Relation Data dictionary."""
711+
return iter(self.data)
712+
713+
def get(self, key: str, default: Optional[str] = None) -> Optional[str]:
714+
"""Safely get an item of the Abstract Relation Data dictionary."""
715+
try:
716+
if result := self[key]:
717+
return result
718+
except KeyError:
719+
return default
720+
721+
615722
class Data(ABC):
616723
"""Base relation data mainpulation (abstract) class."""
617724

@@ -929,6 +1036,10 @@ def _delete_relation_data_without_secrets(
9291036
# Public interface methods
9301037
# Handling Relation Fields seamlessly, regardless if in databag or a Juju Secret
9311038

1039+
def as_dict(self, relation_id: int) -> UserDict[str, str]:
1040+
"""Dict behavior representation of the Abstract Data."""
1041+
return DataDict(self, relation_id)
1042+
9321043
def get_relation(self, relation_name, relation_id) -> Relation:
9331044
"""Safe way of retrieving a relation."""
9341045
relation = self._model.get_relation(relation_name, relation_id)
@@ -1787,6 +1898,14 @@ def __init__(self, unit: Unit, *args, **kwargs):
17871898
self.local_unit = unit
17881899
self.component = unit
17891900

1901+
def update_relation_data(self, relation_id: int, data: dict) -> None:
1902+
"""This method makes no sense for a Other Peer Relation."""
1903+
raise NotImplementedError("It's not possible to update data of another unit.")
1904+
1905+
def delete_relation_data(self, relation_id: int, fields: List[str]) -> None:
1906+
"""This method makes no sense for a Other Peer Relation."""
1907+
raise NotImplementedError("It's not possible to delete data of another unit.")
1908+
17901909

17911910
class DataPeerOtherUnitEventHandlers(DataPeerEventHandlers):
17921911
"""Requires-side of the relation."""
@@ -1809,18 +1928,18 @@ def __init__(
18091928
additional_secret_fields: Optional[List[str]] = [],
18101929
secret_field_name: Optional[str] = None,
18111930
deleted_label: Optional[str] = None,
1812-
unique_key: str = "",
18131931
):
1814-
DataPeerData.__init__(
1932+
DataPeerOtherUnitData.__init__(
18151933
self,
1934+
unit,
18161935
charm.model,
18171936
relation_name,
18181937
extra_user_roles,
18191938
additional_secret_fields,
18201939
secret_field_name,
18211940
deleted_label,
18221941
)
1823-
DataPeerEventHandlers.__init__(self, charm, self, unique_key)
1942+
DataPeerOtherUnitEventHandlers.__init__(self, charm, self)
18241943

18251944

18261945
# General events

lib/charms/mysql/v0/mysql.py

+34-16
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ def wait_until_mysql_connection(self) -> None:
9999
PEER,
100100
ROOT_PASSWORD_KEY,
101101
ROOT_USERNAME,
102+
SECRET_KEY_FALLBACKS,
102103
SERVER_CONFIG_PASSWORD_KEY,
103104
SERVER_CONFIG_USERNAME,
104105
)
@@ -114,7 +115,7 @@ def wait_until_mysql_connection(self) -> None:
114115

115116
# Increment this PATCH version before using `charmcraft publish-lib` or reset
116117
# to 0 if you are raising the major API version
117-
LIBPATCH = 56
118+
LIBPATCH = 57
118119

119120
UNIT_TEARDOWN_LOCKNAME = "unit-teardown"
120121
UNIT_ADD_LOCKNAME = "unit-add"
@@ -411,8 +412,8 @@ def __init__(self, *args):
411412
additional_secret_fields=[
412413
"key",
413414
"csr",
414-
"cert",
415-
"cauth",
415+
"certificate",
416+
"certificate-authority",
416417
"chain",
417418
],
418419
secret_field_name=SECRET_INTERNAL_LABEL,
@@ -580,6 +581,13 @@ def has_cos_relation(self) -> bool:
580581

581582
return len(active_cos_relations) > 0
582583

584+
def peer_relation_data(self, scope: Scopes) -> DataPeer:
585+
"""Returns the peer relation data per scope."""
586+
if scope == APP_SCOPE:
587+
return self.peer_relation_app
588+
elif scope == UNIT_SCOPE:
589+
return self.peer_relation_unit
590+
583591
def get_secret(
584592
self,
585593
scope: Scopes,
@@ -591,11 +599,15 @@ def get_secret(
591599
Else retrieve from peer databag. This is to account for cases where secrets are stored in
592600
peer databag but the charm is then refreshed to a newer revision.
593601
"""
602+
if scope not in get_args(Scopes):
603+
raise ValueError("Unknown secret scope")
604+
594605
peers = self.model.get_relation(PEER)
595-
if scope == APP_SCOPE:
596-
value = self.peer_relation_app.fetch_my_relation_field(peers.id, key)
597-
else:
598-
value = self.peer_relation_unit.fetch_my_relation_field(peers.id, key)
606+
if not (value := self.peer_relation_data(scope).fetch_my_relation_field(peers.id, key)):
607+
if key in SECRET_KEY_FALLBACKS:
608+
value = self.peer_relation_data(scope).fetch_my_relation_field(
609+
peers.id, SECRET_KEY_FALLBACKS[key]
610+
)
599611
return value
600612

601613
def set_secret(self, scope: Scopes, key: str, value: Optional[str]) -> None:
@@ -607,24 +619,30 @@ def set_secret(self, scope: Scopes, key: str, value: Optional[str]) -> None:
607619
raise MySQLSecretError("Can only set app secrets on the leader unit")
608620

609621
if not value:
610-
return self.remove_secret(scope, key)
622+
if key in SECRET_KEY_FALLBACKS:
623+
self.remove_secret(scope, SECRET_KEY_FALLBACKS[key])
624+
self.remove_secret(scope, key)
625+
return
611626

612627
peers = self.model.get_relation(PEER)
613-
if scope == APP_SCOPE:
614-
self.peer_relation_app.update_relation_data(peers.id, {key: value})
615-
elif scope == UNIT_SCOPE:
616-
self.peer_relation_unit.update_relation_data(peers.id, {key: value})
628+
629+
fallback_key_to_secret_key = {v: k for k, v in SECRET_KEY_FALLBACKS.items()}
630+
if key in fallback_key_to_secret_key:
631+
if self.peer_relation_data(scope).fetch_my_relation_field(peers.id, key):
632+
self.remove_secret(scope, key)
633+
self.peer_relation_data(scope).update_relation_data(
634+
peers.id, {fallback_key_to_secret_key[key]: value}
635+
)
636+
else:
637+
self.peer_relation_data(scope).update_relation_data(peers.id, {key: value})
617638

618639
def remove_secret(self, scope: Scopes, key: str) -> None:
619640
"""Removing a secret."""
620641
if scope not in get_args(Scopes):
621642
raise RuntimeError("Unknown secret scope.")
622643

623644
peers = self.model.get_relation(PEER)
624-
if scope == APP_SCOPE:
625-
self.peer_relation_app.delete_relation_data(peers.id, [key])
626-
else:
627-
self.peer_relation_unit.delete_relation_data(peers.id, [key])
645+
self.peer_relation_data(scope).delete_relation_data(peers.id, [key])
628646

629647

630648
class MySQLMemberState(str, enum.Enum):

src/constants.py

+9
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,12 @@
4747
GR_MAX_MEMBERS = 9
4848
HOSTNAME_DETAILS = "hostname-details"
4949
COS_AGENT_RELATION_NAME = "cos-agent"
50+
SECRET_KEY_FALLBACKS = {
51+
"root-password": "root_password",
52+
"server-config-password": "server_config_password",
53+
"cluster-admin-password": "cluster_admin_password",
54+
"monitoring-password": "monitoring_password",
55+
"backups-password": "backups_password",
56+
"certificate": "cert",
57+
"certificate-authority": "ca",
58+
}

0 commit comments

Comments
 (0)