diff --git a/lib/charms/data_platform_libs/v0/data_interfaces.py b/lib/charms/data_platform_libs/v0/data_interfaces.py index 10bda6d..db65308 100644 --- a/lib/charms/data_platform_libs/v0/data_interfaces.py +++ b/lib/charms/data_platform_libs/v0/data_interfaces.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Library to manage the relation for the data-platform products. +r"""Library to manage the relation for the data-platform products. This library contains the Requires and Provides classes for handling the relation between an application and multiple managed application supported by the data-team: @@ -296,7 +296,7 @@ def _on_topic_requested(self, event: TopicRequestedEvent): from abc import ABC, abstractmethod from collections import namedtuple from datetime import datetime -from typing import List, Optional +from typing import List, Optional, Union from ops.charm import ( CharmBase, @@ -306,7 +306,7 @@ def _on_topic_requested(self, event: TopicRequestedEvent): RelationJoinedEvent, ) from ops.framework import EventSource, Object -from ops.model import Relation +from ops.model import Application, Relation, Unit # The unique Charmhub library identifier, never change it LIBID = "6c3e6b6680d64e9c89e611d1a15f65be" @@ -316,7 +316,7 @@ def _on_topic_requested(self, event: TopicRequestedEvent): # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 13 +LIBPATCH = 14 PYDEPS = ["ops>=2.0.0"] @@ -331,7 +331,7 @@ def _on_topic_requested(self, event: TopicRequestedEvent): deleted - key that were deleted""" -def diff(event: RelationChangedEvent, bucket: str) -> Diff: +def diff(event: RelationChangedEvent, bucket: Union[Unit, Application]) -> Diff: """Retrieves the diff of the data in the relation changed databag. Args: @@ -345,9 +345,11 @@ def diff(event: RelationChangedEvent, bucket: str) -> Diff: # Retrieve the old data from the data key in the application relation databag. old_data = json.loads(event.relation.data[bucket].get("data", "{}")) # Retrieve the new data from the event relation databag. - new_data = { - key: value for key, value in event.relation.data[event.app].items() if key != "data" - } + new_data = ( + {key: value for key, value in event.relation.data[event.app].items() if key != "data"} + if event.app + else {} + ) # These are the keys that were added to the databag and triggered this event. added = new_data.keys() - old_data.keys() @@ -409,9 +411,11 @@ def fetch_relation_data(self) -> dict: """ data = {} for relation in self.relations: - data[relation.id] = { - key: value for key, value in relation.data[relation.app].items() if key != "data" - } + data[relation.id] = ( + {key: value for key, value in relation.data[relation.app].items() if key != "data"} + if relation.app + else {} + ) return data def _update_relation_data(self, relation_id: int, data: dict) -> None: @@ -426,8 +430,8 @@ def _update_relation_data(self, relation_id: int, data: dict) -> None: that should be updated in the relation. """ if self.local_unit.is_leader(): - relation = self.charm.model.get_relation(self.relation_name, relation_id) - relation.data[self.local_app].update(data) + if relation := self.charm.model.get_relation(self.relation_name, relation_id): + relation.data[self.local_app].update(data) @property def relations(self) -> List[Relation]: @@ -479,7 +483,7 @@ def __init__( self, charm, relation_name: str, - extra_user_roles: str = None, + extra_user_roles: Optional[str] = None, ): """Manager of base client relations.""" super().__init__(charm, relation_name) @@ -517,9 +521,11 @@ def fetch_relation_data(self) -> dict: """ data = {} for relation in self.relations: - data[relation.id] = { - key: value for key, value in relation.data[relation.app].items() if key != "data" - } + data[relation.id] = ( + {key: value for key, value in relation.data[relation.app].items() if key != "data"} + if relation.app + else {} + ) return data def _update_relation_data(self, relation_id: int, data: dict) -> None: @@ -567,7 +573,10 @@ def _is_relation_active(relation: Relation): return False @staticmethod - def _is_resource_created_for_relation(relation: Relation): + def _is_resource_created_for_relation(relation: Relation) -> bool: + if not relation.app: + return False + return ( "username" in relation.data[relation.app] and "password" in relation.data[relation.app] ) @@ -599,10 +608,7 @@ def is_resource_created(self, relation_id: Optional[int] = None) -> bool: else: return ( all( - [ - self._is_resource_created_for_relation(relation) - for relation in self.relations - ] + self._is_resource_created_for_relation(relation) for relation in self.relations ) if self.relations else False @@ -618,6 +624,9 @@ class ExtraRoleEvent(RelationEvent): @property def extra_user_roles(self) -> Optional[str]: """Returns the extra user roles that were requested.""" + if not self.relation.app: + return None + return self.relation.data[self.relation.app].get("extra-user-roles") @@ -627,21 +636,33 @@ class AuthenticationEvent(RelationEvent): @property def username(self) -> Optional[str]: """Returns the created username.""" + if not self.relation.app: + return None + return self.relation.data[self.relation.app].get("username") @property def password(self) -> Optional[str]: """Returns the password for the created user.""" + if not self.relation.app: + return None + return self.relation.data[self.relation.app].get("password") @property def tls(self) -> Optional[str]: """Returns whether TLS is configured.""" + if not self.relation.app: + return None + return self.relation.data[self.relation.app].get("tls") @property def tls_ca(self) -> Optional[str]: """Returns TLS CA.""" + if not self.relation.app: + return None + return self.relation.data[self.relation.app].get("tls-ca") @@ -654,6 +675,9 @@ class DatabaseProvidesEvent(RelationEvent): @property def database(self) -> Optional[str]: """Returns the database that was requested.""" + if not self.relation.app: + return None + return self.relation.data[self.relation.app].get("database") @@ -676,6 +700,9 @@ class DatabaseRequiresEvent(RelationEvent): @property def database(self) -> Optional[str]: """Returns the database name.""" + if not self.relation.app: + return None + return self.relation.data[self.relation.app].get("database") @property @@ -685,6 +712,9 @@ def endpoints(self) -> Optional[str]: In VM charms, this is the primary's address. In kubernetes charms, this is the service to the primary pod. """ + if not self.relation.app: + return None + return self.relation.data[self.relation.app].get("endpoints") @property @@ -694,6 +724,9 @@ def read_only_endpoints(self) -> Optional[str]: In VM charms, this is the address of all the secondary instances. In kubernetes charms, this is the service to all replica pod instances. """ + if not self.relation.app: + return None + return self.relation.data[self.relation.app].get("read-only-endpoints") @property @@ -702,6 +735,9 @@ def replset(self) -> Optional[str]: MongoDB only. """ + if not self.relation.app: + return None + return self.relation.data[self.relation.app].get("replset") @property @@ -710,6 +746,9 @@ def uris(self) -> Optional[str]: MongoDB, Redis, OpenSearch. """ + if not self.relation.app: + return None + return self.relation.data[self.relation.app].get("uris") @property @@ -718,6 +757,9 @@ def version(self) -> Optional[str]: Version as informed by the database daemon. """ + if not self.relation.app: + return None + return self.relation.data[self.relation.app].get("version") @@ -750,7 +792,7 @@ class DatabaseRequiresEvents(CharmEvents): class DatabaseProvides(DataProvides): """Provider-side of the database relations.""" - on = DatabaseProvidesEvents() + on = DatabaseProvidesEvents() # pyright: ignore [reportGeneralTypeIssues] def __init__(self, charm: CharmBase, relation_name: str) -> None: super().__init__(charm, relation_name) @@ -767,7 +809,9 @@ def _on_relation_changed(self, event: RelationChangedEvent) -> None: # Emit a database requested event if the setup key (database name and optional # extra user roles) was added to the relation databag by the application. if "database" in diff.added: - self.on.database_requested.emit(event.relation, app=event.app, unit=event.unit) + getattr(self.on, "database_requested").emit( + event.relation, app=event.app, unit=event.unit + ) def set_database(self, relation_id: int, database_name: str) -> None: """Set database name. @@ -844,15 +888,15 @@ def set_version(self, relation_id: int, version: str) -> None: class DatabaseRequires(DataRequires): """Requires-side of the database relation.""" - on = DatabaseRequiresEvents() + on = DatabaseRequiresEvents() # pyright: ignore [reportGeneralTypeIssues] def __init__( self, charm, relation_name: str, database_name: str, - extra_user_roles: str = None, - relations_aliases: List[str] = None, + extra_user_roles: Optional[str] = None, + relations_aliases: Optional[List[str]] = None, ): """Manager of database client relations.""" super().__init__(charm, relation_name, extra_user_roles) @@ -974,7 +1018,9 @@ def is_postgresql_plugin_enabled(self, plugin: str, relation_index: int = 0) -> try: with psycopg.connect(connection_string) as connection: with connection.cursor() as cursor: - cursor.execute(f"SELECT TRUE FROM pg_extension WHERE extname='{plugin}';") + cursor.execute( + "SELECT TRUE FROM pg_extension WHERE extname=%s::text;", (plugin,) + ) return cursor.fetchone() is not None except psycopg.Error as e: logger.exception( @@ -1010,7 +1056,9 @@ def _on_relation_changed_event(self, event: RelationChangedEvent) -> None: if "username" in diff.added and "password" in diff.added: # Emit the default event (the one without an alias). logger.info("database created at %s", datetime.now()) - self.on.database_created.emit(event.relation, app=event.app, unit=event.unit) + getattr(self.on, "database_created").emit( + event.relation, app=event.app, unit=event.unit + ) # Emit the aliased event (if any). self._emit_aliased_event(event, "database_created") @@ -1024,7 +1072,9 @@ def _on_relation_changed_event(self, event: RelationChangedEvent) -> None: if "endpoints" in diff.added or "endpoints" in diff.changed: # Emit the default event (the one without an alias). logger.info("endpoints changed on %s", datetime.now()) - self.on.endpoints_changed.emit(event.relation, app=event.app, unit=event.unit) + getattr(self.on, "endpoints_changed").emit( + event.relation, app=event.app, unit=event.unit + ) # Emit the aliased event (if any). self._emit_aliased_event(event, "endpoints_changed") @@ -1038,7 +1088,7 @@ def _on_relation_changed_event(self, event: RelationChangedEvent) -> None: if "read-only-endpoints" in diff.added or "read-only-endpoints" in diff.changed: # Emit the default event (the one without an alias). logger.info("read-only-endpoints changed on %s", datetime.now()) - self.on.read_only_endpoints_changed.emit( + getattr(self.on, "read_only_endpoints_changed").emit( event.relation, app=event.app, unit=event.unit ) @@ -1055,11 +1105,17 @@ class KafkaProvidesEvent(RelationEvent): @property def topic(self) -> Optional[str]: """Returns the topic that was requested.""" + if not self.relation.app: + return None + return self.relation.data[self.relation.app].get("topic") @property def consumer_group_prefix(self) -> Optional[str]: """Returns the consumer-group-prefix that was requested.""" + if not self.relation.app: + return None + return self.relation.data[self.relation.app].get("consumer-group-prefix") @@ -1082,21 +1138,33 @@ class KafkaRequiresEvent(RelationEvent): @property def topic(self) -> Optional[str]: """Returns the topic.""" + if not self.relation.app: + return None + return self.relation.data[self.relation.app].get("topic") @property def bootstrap_server(self) -> Optional[str]: """Returns a comma-separated list of broker uris.""" + if not self.relation.app: + return None + return self.relation.data[self.relation.app].get("endpoints") @property def consumer_group_prefix(self) -> Optional[str]: """Returns the consumer-group-prefix.""" + if not self.relation.app: + return None + return self.relation.data[self.relation.app].get("consumer-group-prefix") @property def zookeeper_uris(self) -> Optional[str]: """Returns a comma separated list of Zookeeper uris.""" + if not self.relation.app: + return None + return self.relation.data[self.relation.app].get("zookeeper-uris") @@ -1124,7 +1192,7 @@ class KafkaRequiresEvents(CharmEvents): class KafkaProvides(DataProvides): """Provider-side of the Kafka relation.""" - on = KafkaProvidesEvents() + on = KafkaProvidesEvents() # pyright: ignore [reportGeneralTypeIssues] def __init__(self, charm: CharmBase, relation_name: str) -> None: super().__init__(charm, relation_name) @@ -1141,7 +1209,9 @@ def _on_relation_changed(self, event: RelationChangedEvent) -> None: # Emit a topic requested event if the setup key (topic name and optional # extra user roles) was added to the relation databag by the application. if "topic" in diff.added: - self.on.topic_requested.emit(event.relation, app=event.app, unit=event.unit) + getattr(self.on, "topic_requested").emit( + event.relation, app=event.app, unit=event.unit + ) def set_topic(self, relation_id: int, topic: str) -> None: """Set topic name in the application relation databag. @@ -1183,7 +1253,7 @@ def set_zookeeper_uris(self, relation_id: int, zookeeper_uris: str) -> None: class KafkaRequires(DataRequires): """Requires-side of the Kafka relation.""" - on = KafkaRequiresEvents() + on = KafkaRequiresEvents() # pyright: ignore [reportGeneralTypeIssues] def __init__( self, @@ -1232,7 +1302,7 @@ def _on_relation_changed_event(self, event: RelationChangedEvent) -> None: if "username" in diff.added and "password" in diff.added: # Emit the default event (the one without an alias). logger.info("topic created at %s", datetime.now()) - self.on.topic_created.emit(event.relation, app=event.app, unit=event.unit) + getattr(self.on, "topic_created").emit(event.relation, app=event.app, unit=event.unit) # To avoid unnecessary application restarts do not trigger # “endpoints_changed“ event if “topic_created“ is triggered. @@ -1243,7 +1313,7 @@ def _on_relation_changed_event(self, event: RelationChangedEvent) -> None: if "endpoints" in diff.added or "endpoints" in diff.changed: # Emit the default event (the one without an alias). logger.info("endpoints changed on %s", datetime.now()) - self.on.bootstrap_server_changed.emit( + getattr(self.on, "bootstrap_server_changed").emit( event.relation, app=event.app, unit=event.unit ) # here check if this is the right design return @@ -1258,6 +1328,9 @@ class OpenSearchProvidesEvent(RelationEvent): @property def index(self) -> Optional[str]: """Returns the index that was requested.""" + if not self.relation.app: + return None + return self.relation.data[self.relation.app].get("index") @@ -1299,7 +1372,7 @@ class OpenSearchRequiresEvents(CharmEvents): class OpenSearchProvides(DataProvides): """Provider-side of the OpenSearch relation.""" - on = OpenSearchProvidesEvents() + on = OpenSearchProvidesEvents() # pyright: ignore[reportGeneralTypeIssues] def __init__(self, charm: CharmBase, relation_name: str) -> None: super().__init__(charm, relation_name) @@ -1316,7 +1389,9 @@ def _on_relation_changed(self, event: RelationChangedEvent) -> None: # Emit an index requested event if the setup key (index name and optional extra user roles) # have been added to the relation databag by the application. if "index" in diff.added: - self.on.index_requested.emit(event.relation, app=event.app, unit=event.unit) + getattr(self.on, "index_requested").emit( + event.relation, app=event.app, unit=event.unit + ) def set_index(self, relation_id: int, index: str) -> None: """Set the index in the application relation databag. @@ -1351,7 +1426,7 @@ def set_version(self, relation_id: int, version: str) -> None: class OpenSearchRequires(DataRequires): """Requires-side of the OpenSearch relation.""" - on = OpenSearchRequiresEvents() + on = OpenSearchRequiresEvents() # pyright: ignore[reportGeneralTypeIssues] def __init__( self, charm, relation_name: str, index: str, extra_user_roles: Optional[str] = None @@ -1383,14 +1458,16 @@ def _on_relation_changed_event(self, event: RelationChangedEvent) -> None: updates = {"username", "password", "tls", "tls-ca"} if len(set(diff._asdict().keys()) - updates) < len(diff): logger.info("authentication updated at: %s", datetime.now()) - self.on.authentication_updated.emit(event.relation, app=event.app, unit=event.unit) + getattr(self.on, "authentication_updated").emit( + event.relation, app=event.app, unit=event.unit + ) # Check if the index is created # (the OpenSearch charm shares the credentials). if "username" in diff.added and "password" in diff.added: # Emit the default event (the one without an alias). logger.info("index created at: %s", datetime.now()) - self.on.index_created.emit(event.relation, app=event.app, unit=event.unit) + getattr(self.on, "index_created").emit(event.relation, app=event.app, unit=event.unit) # To avoid unnecessary application restarts do not trigger # “endpoints_changed“ event if “index_created“ is triggered. @@ -1401,7 +1478,7 @@ def _on_relation_changed_event(self, event: RelationChangedEvent) -> None: if "endpoints" in diff.added or "endpoints" in diff.changed: # Emit the default event (the one without an alias). logger.info("endpoints changed on %s", datetime.now()) - self.on.endpoints_changed.emit( + getattr(self.on, "endpoints_changed").emit( event.relation, app=event.app, unit=event.unit ) # here check if this is the right design return