From 98dcdbbfc9697e0f49777a5bd0f617f7f7d463a5 Mon Sep 17 00:00:00 2001 From: Dragomir Penev <6687393+dragomirp@users.noreply.github.com> Date: Mon, 5 Feb 2024 20:01:02 +0200 Subject: [PATCH] [MISC] Update rock due to outdated packages (#387) * Update rock * Update libs * Fix unit tests --- .../data_platform_libs/v0/data_interfaces.py | 62 +++++++++---------- lib/charms/loki_k8s/v0/loki_push_api.py | 18 +++++- lib/charms/rolling_ops/v0/rollingops.py | 39 +++++++++--- .../v2/tls_certificates.py | 21 ++++--- metadata.yaml | 2 +- tests/unit/test_charm.py | 4 +- 6 files changed, 92 insertions(+), 54 deletions(-) diff --git a/lib/charms/data_platform_libs/v0/data_interfaces.py b/lib/charms/data_platform_libs/v0/data_interfaces.py index fbe989726d..c940cc009a 100644 --- a/lib/charms/data_platform_libs/v0/data_interfaces.py +++ b/lib/charms/data_platform_libs/v0/data_interfaces.py @@ -320,7 +320,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 = 26 +LIBPATCH = 27 PYDEPS = ["ops>=2.0.0"] @@ -422,15 +422,15 @@ def diff(event: RelationChangedEvent, bucket: Union[Unit, Application]) -> Diff: ) # These are the keys that were added to the databag and triggered this event. - added = new_data.keys() - old_data.keys() # pyright: ignore [reportGeneralTypeIssues] + added = new_data.keys() - old_data.keys() # pyright: ignore [reportAssignmentType] # These are the keys that were removed from the databag and triggered this event. - deleted = old_data.keys() - new_data.keys() # pyright: ignore [reportGeneralTypeIssues] + deleted = old_data.keys() - new_data.keys() # pyright: ignore [reportAssignmentType] # These are the keys that already existed in the databag, # but had their values changed. changed = { key - for key in old_data.keys() & new_data.keys() # pyright: ignore [reportGeneralTypeIssues] - if old_data[key] != new_data[key] # pyright: ignore [reportGeneralTypeIssues] + for key in old_data.keys() & new_data.keys() # pyright: ignore [reportAssignmentType] + if old_data[key] != new_data[key] # pyright: ignore [reportAssignmentType] } # Convert the new_data to a serializable format and save it for a next diff check. set_encoded_field(event.relation, bucket, "data", new_data) @@ -1619,7 +1619,8 @@ def _delete_relation_data(self, relation: Relation, fields: List[str]) -> None: current_data.get(relation.id, []) ): logger.error( - "Non-existing secret %s was attempted to be removed.", non_existent + "Non-existing secret %s was attempted to be removed.", + ", ".join(non_existent), ) _, normal_fields = self._process_secret_fields( @@ -1686,12 +1687,8 @@ def extra_user_roles(self) -> Optional[str]: return self.relation.data[self.relation.app].get("extra-user-roles") -class AuthenticationEvent(RelationEvent): - """Base class for authentication fields for events. - - The amount of logic added here is not ideal -- but this was the only way to preserve - the interface when moving to Juju Secrets - """ +class RelationEventWithSecret(RelationEvent): + """Base class for Relation Events that need to handle secrets.""" @property def _secrets(self) -> dict: @@ -1703,18 +1700,6 @@ def _secrets(self) -> dict: self._cached_secrets = {} return self._cached_secrets - @property - def _jujuversion(self) -> JujuVersion: - """Caching jujuversion to avoid a Juju call on each field evaluation. - - DON'T USE the encapsulated helper variable outside of this function - """ - if not hasattr(self, "_cached_jujuversion"): - self._cached_jujuversion = None - if not self._cached_jujuversion: - self._cached_jujuversion = JujuVersion.from_environ() - return self._cached_jujuversion - def _get_secret(self, group) -> Optional[Dict[str, str]]: """Retrieveing secrets.""" if not self.app: @@ -1730,7 +1715,15 @@ def _get_secret(self, group) -> Optional[Dict[str, str]]: @property def secrets_enabled(self): """Is this Juju version allowing for Secrets usage?""" - return self._jujuversion.has_secrets + return JujuVersion.from_environ().has_secrets + + +class AuthenticationEvent(RelationEventWithSecret): + """Base class for authentication fields for events. + + The amount of logic added here is not ideal -- but this was the only way to preserve + the interface when moving to Juju Secrets + """ @property def username(self) -> Optional[str]: @@ -1813,7 +1806,7 @@ class DatabaseProvidesEvents(CharmEvents): database_requested = EventSource(DatabaseRequestedEvent) -class DatabaseRequiresEvent(RelationEvent): +class DatabaseRequiresEvent(RelationEventWithSecret): """Base class for database events.""" @property @@ -1868,6 +1861,11 @@ def uris(self) -> Optional[str]: if not self.relation.app: return None + if self.secrets_enabled: + secret = self._get_secret("user") + if secret: + return secret.get("uris") + return self.relation.data[self.relation.app].get("uris") @property @@ -1911,7 +1909,7 @@ class DatabaseRequiresEvents(CharmEvents): class DatabaseProvides(DataProvides): """Provider-side of the database relations.""" - on = DatabaseProvidesEvents() # pyright: ignore [reportGeneralTypeIssues] + on = DatabaseProvidesEvents() # pyright: ignore [reportAssignmentType] def __init__(self, charm: CharmBase, relation_name: str) -> None: super().__init__(charm, relation_name) @@ -2006,7 +2004,7 @@ def set_version(self, relation_id: int, version: str) -> None: class DatabaseRequires(DataRequires): """Requires-side of the database relation.""" - on = DatabaseRequiresEvents() # pyright: ignore [reportGeneralTypeIssues] + on = DatabaseRequiresEvents() # pyright: ignore [reportAssignmentType] def __init__( self, @@ -2335,7 +2333,7 @@ class KafkaRequiresEvents(CharmEvents): class KafkaProvides(DataProvides): """Provider-side of the Kafka relation.""" - on = KafkaProvidesEvents() # pyright: ignore [reportGeneralTypeIssues] + on = KafkaProvidesEvents() # pyright: ignore [reportAssignmentType] def __init__(self, charm: CharmBase, relation_name: str) -> None: super().__init__(charm, relation_name) @@ -2396,7 +2394,7 @@ def set_zookeeper_uris(self, relation_id: int, zookeeper_uris: str) -> None: class KafkaRequires(DataRequires): """Requires-side of the Kafka relation.""" - on = KafkaRequiresEvents() # pyright: ignore [reportGeneralTypeIssues] + on = KafkaRequiresEvents() # pyright: ignore [reportAssignmentType] def __init__( self, @@ -2533,7 +2531,7 @@ class OpenSearchRequiresEvents(CharmEvents): class OpenSearchProvides(DataProvides): """Provider-side of the OpenSearch relation.""" - on = OpenSearchProvidesEvents() # pyright: ignore[reportGeneralTypeIssues] + on = OpenSearchProvidesEvents() # pyright: ignore[reportAssignmentType] def __init__(self, charm: CharmBase, relation_name: str) -> None: super().__init__(charm, relation_name) @@ -2586,7 +2584,7 @@ def set_version(self, relation_id: int, version: str) -> None: class OpenSearchRequires(DataRequires): """Requires-side of the OpenSearch relation.""" - on = OpenSearchRequiresEvents() # pyright: ignore[reportGeneralTypeIssues] + on = OpenSearchRequiresEvents() # pyright: ignore[reportAssignmentType] def __init__( self, diff --git a/lib/charms/loki_k8s/v0/loki_push_api.py b/lib/charms/loki_k8s/v0/loki_push_api.py index 01d7dc161a..0e4a2667ac 100644 --- a/lib/charms/loki_k8s/v0/loki_push_api.py +++ b/lib/charms/loki_k8s/v0/loki_push_api.py @@ -480,7 +480,7 @@ def _alert_rules_error(self, event): # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 25 +LIBPATCH = 26 logger = logging.getLogger(__name__) @@ -2115,7 +2115,21 @@ def _download_and_push_promtail_to_workload(self, promtail_info: dict) -> None: - "zipsha": sha256 sum of zip file of promtail binary - "binsha": sha256 sum of unpacked promtail binary """ - with request.urlopen(promtail_info["url"]) as r: + # Check for Juju proxy variables and fall back to standard ones if not set + proxies: Optional[Dict[str, str]] = {} + if proxies and os.environ.get("JUJU_CHARM_HTTP_PROXY"): + proxies.update({"http": os.environ["JUJU_CHARM_HTTP_PROXY"]}) + if proxies and os.environ.get("JUJU_CHARM_HTTPS_PROXY"): + proxies.update({"https": os.environ["JUJU_CHARM_HTTPS_PROXY"]}) + if proxies and os.environ.get("JUJU_CHARM_NO_PROXY"): + proxies.update({"no_proxy": os.environ["JUJU_CHARM_NO_PROXY"]}) + else: + proxies = None + + proxy_handler = request.ProxyHandler(proxies) + opener = request.build_opener(proxy_handler) + + with opener.open(promtail_info["url"]) as r: file_bytes = r.read() file_path = os.path.join(BINARY_DIR, promtail_info["filename"] + ".gz") with open(file_path, "wb") as f: diff --git a/lib/charms/rolling_ops/v0/rollingops.py b/lib/charms/rolling_ops/v0/rollingops.py index 7f4bd1b89e..5a7d4ce306 100644 --- a/lib/charms/rolling_ops/v0/rollingops.py +++ b/lib/charms/rolling_ops/v0/rollingops.py @@ -72,7 +72,7 @@ def _on_trigger_restart(self, event): """ import logging from enum import Enum -from typing import AnyStr, Callable +from typing import AnyStr, Callable, Optional from ops.charm import ActionEvent, CharmBase, RelationChangedEvent from ops.framework import EventBase, Object @@ -88,7 +88,7 @@ def _on_trigger_restart(self, event): # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 2 +LIBPATCH = 5 class LockNoRelationError(Exception): @@ -261,7 +261,15 @@ class RunWithLock(EventBase): class AcquireLock(EventBase): """Signals that this unit wants to acquire a lock.""" - pass + def __init__(self, handle, callback_override: Optional[str] = None): + super().__init__(handle) + self.callback_override = callback_override or "" + + def snapshot(self): + return {"callback_override": self.callback_override} + + def restore(self, snapshot): + self.callback_override = snapshot["callback_override"] class ProcessLocks(EventBase): @@ -366,7 +374,8 @@ def _on_process_locks(self: CharmBase, event: ProcessLocks): self.charm.on[self.name].run_with_lock.emit() return - self.model.app.status = ActiveStatus() + if self.model.app.status.message == f"Beginning rolling {self.name}": + self.model.app.status = ActiveStatus() def _on_acquire_lock(self: CharmBase, event: ActionEvent): """Request a lock.""" @@ -374,7 +383,11 @@ def _on_acquire_lock(self: CharmBase, event: ActionEvent): Lock(self).acquire() # Updates relation data # emit relation changed event in the edge case where aquire does not relation = self.model.get_relation(self.name) - self.charm.on[self.name].relation_changed.emit(relation) + + # persist callback override for eventual run + relation.data[self.charm.unit].update({"callback_override": event.callback_override}) + self.charm.on[self.name].relation_changed.emit(relation, app=self.charm.app) + except LockNoRelationError: logger.debug("No {} peer relation yet. Delaying rolling op.".format(self.name)) event.defer() @@ -382,9 +395,21 @@ def _on_acquire_lock(self: CharmBase, event: ActionEvent): def _on_run_with_lock(self: CharmBase, event: RunWithLock): lock = Lock(self) self.model.unit.status = MaintenanceStatus("Executing {} operation".format(self.name)) - self._callback(event) + relation = self.model.get_relation(self.name) + + # default to instance callback if not set + callback_name = relation.data[self.charm.unit].get( + "callback_override", self._callback.__name__ + ) + callback = getattr(self.charm, callback_name) + callback(event) + lock.release() # Updates relation data if lock.unit == self.model.unit: self.charm.on[self.name].process_locks.emit() - self.model.unit.status = ActiveStatus() + # cleanup old callback overrides + relation.data[self.charm.unit].update({"callback_override": ""}) + + if self.model.unit.status.message == f"Executing {self.name} operation": + self.model.unit.status = ActiveStatus() diff --git a/lib/charms/tls_certificates_interface/v2/tls_certificates.py b/lib/charms/tls_certificates_interface/v2/tls_certificates.py index 08c5cb5004..5992faf431 100644 --- a/lib/charms/tls_certificates_interface/v2/tls_certificates.py +++ b/lib/charms/tls_certificates_interface/v2/tls_certificates.py @@ -286,7 +286,6 @@ def _on_all_certificates_invalidated(self, event: AllCertificatesInvalidatedEven from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives.serialization import pkcs12 -from cryptography.x509.extensions import Extension, ExtensionNotFound from jsonschema import exceptions, validate # type: ignore[import-untyped] from ops.charm import ( CharmBase, @@ -308,7 +307,7 @@ def _on_all_certificates_invalidated(self, event: AllCertificatesInvalidatedEven # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 22 +LIBPATCH = 24 PYDEPS = ["cryptography", "jsonschema"] @@ -939,9 +938,11 @@ def generate_private_key( key_bytes = private_key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.BestAvailableEncryption(password) - if password - else serialization.NoEncryption(), + encryption_algorithm=( + serialization.BestAvailableEncryption(password) + if password + else serialization.NoEncryption() + ), ) return key_bytes @@ -1676,7 +1677,7 @@ def get_assigned_certificates(self) -> List[Dict[str, str]]: """ final_list = [] for csr in self.get_certificate_signing_requests(fulfilled_only=True): - assert type(csr["certificate_signing_request"]) == str + assert isinstance(csr["certificate_signing_request"], str) if cert := self._find_certificate_in_relation_data(csr["certificate_signing_request"]): final_list.append(cert) return final_list @@ -1699,7 +1700,7 @@ def get_expiring_certificates(self) -> List[Dict[str, str]]: """ final_list = [] for csr in self.get_certificate_signing_requests(fulfilled_only=True): - assert type(csr["certificate_signing_request"]) == str + assert isinstance(csr["certificate_signing_request"], str) if cert := self._find_certificate_in_relation_data(csr["certificate_signing_request"]): expiry_time = _get_certificate_expiry_time(cert["certificate"]) if not expiry_time: @@ -1719,11 +1720,12 @@ def get_certificate_signing_requests( """Gets the list of CSR's that were sent to the provider. You can choose to get only the CSR's that have a certificate assigned or only the CSR's - that don't. + that don't. Args: fulfilled_only (bool): This option will discard CSRs that don't have certificates yet. unfulfilled_only (bool): This option will discard CSRs that have certificates signed. + Returns: List of CSR dictionaries. For example: [ @@ -1733,10 +1735,9 @@ def get_certificate_signing_requests( } ] """ - final_list = [] for csr in self._requirer_csrs: - assert type(csr["certificate_signing_request"]) == str + assert isinstance(csr["certificate_signing_request"], str) cert = self._find_certificate_in_relation_data(csr["certificate_signing_request"]) if (unfulfilled_only and cert) or (fulfilled_only and not cert): continue diff --git a/metadata.yaml b/metadata.yaml index 3c0ac30d0d..13dabfccee 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -28,7 +28,7 @@ resources: postgresql-image: type: oci-image description: OCI image for PostgreSQL - upstream-source: ghcr.io/canonical/charmed-postgresql@sha256:ff74e3e8fc0e08e7b952cb69477f1a39e94e7caa156f06bbb844e752bec42f0b + upstream-source: ghcr.io/canonical/charmed-postgresql@sha256:0cfc8cc4c096abdd713480d7cfa300e4edb15b59fc7db429a073a2c8179f5248 peers: database-peers: diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py index e642b8bfe8..68509516aa 100644 --- a/tests/unit/test_charm.py +++ b/tests/unit/test_charm.py @@ -909,13 +909,13 @@ def test_delete_existing_password_secrets(self, _, __): with self._caplog.at_level(logging.ERROR): self.harness.charm.remove_secret("app", "operator-password") assert ( - "Non-existing secret {'operator-password'} was attempted to be removed." + "Non-existing secret operator-password was attempted to be removed." in self._caplog.text ) self.harness.charm.remove_secret("unit", "operator-password") assert ( - "Non-existing secret {'operator-password'} was attempted to be removed." + "Non-existing secret operator-password was attempted to be removed." in self._caplog.text )