diff --git a/lib/charms/data_platform_libs/v0/data_interfaces.py b/lib/charms/data_platform_libs/v0/data_interfaces.py index 5d1691d9f1..714eace460 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 = 23 +LIBPATCH = 24 PYDEPS = ["ops>=2.0.0"] @@ -526,7 +526,16 @@ def get_content(self) -> Dict[str, str]: """Getting cached secret content.""" if not self._secret_content: if self.meta: - self._secret_content = self.meta.get_content() + try: + self._secret_content = self.meta.get_content(refresh=True) + except (ValueError, ModelError) as err: + # https://bugs.launchpad.net/juju/+bug/2042596 + # Only triggered when 'refresh' is set + msg = "ERROR either URI or label should be used for getting an owned secret but not both" + if isinstance(err, ModelError) and msg not in str(err): + raise + # Due to: ValueError: Secret owner cannot use refresh=True + self._secret_content = self.meta.get_content() return self._secret_content def set_content(self, content: Dict[str, str]) -> None: @@ -1085,7 +1094,7 @@ def _delete_relation_secret( secret = self._get_relation_secret(relation.id, group) if not secret: - logging.error("Can't update secret for relation %s", str(relation.id)) + logging.error("Can't delete secret for relation %s", str(relation.id)) return False old_content = secret.get_content() @@ -1827,7 +1836,8 @@ def _assign_relation_alias(self, relation_id: int) -> None: # We need to set relation alias also on the application level so, # it will be accessible in show-unit juju command, executed for a consumer application unit - self.update_relation_data(relation_id, {"alias": available_aliases[0]}) + if self.local_unit.is_leader(): + self.update_relation_data(relation_id, {"alias": available_aliases[0]}) def _emit_aliased_event(self, event: RelationChangedEvent, event_name: str) -> None: """Emit an aliased event to a particular relation if it has an alias. @@ -1914,6 +1924,9 @@ def _on_relation_created_event(self, event: RelationCreatedEvent) -> None: # Sets both database and extra user roles in the relation # if the roles are provided. Otherwise, sets only the database. + if not self.local_unit.is_leader(): + return + if self.extra_user_roles: self.update_relation_data( event.relation.id, @@ -2173,6 +2186,9 @@ def _on_relation_created_event(self, event: RelationCreatedEvent) -> None: """Event emitted when the Kafka relation is created.""" super()._on_relation_created_event(event) + if not self.local_unit.is_leader(): + return + # Sets topic, extra user roles, and "consumer-group-prefix" in the relation relation_data = { f: getattr(self, f.replace("-", "_"), "") @@ -2345,6 +2361,9 @@ def _on_relation_created_event(self, event: RelationCreatedEvent) -> None: """Event emitted when the OpenSearch relation is created.""" super()._on_relation_created_event(event) + if not self.local_unit.is_leader(): + return + # Sets both index and extra user roles in the relation if the roles are provided. # Otherwise, sets only the index. data = {"index": self.index} diff --git a/lib/charms/grafana_agent/v0/cos_agent.py b/lib/charms/grafana_agent/v0/cos_agent.py index d3130b2b5e..259a901700 100644 --- a/lib/charms/grafana_agent/v0/cos_agent.py +++ b/lib/charms/grafana_agent/v0/cos_agent.py @@ -206,17 +206,15 @@ def __init__(self, *args): ``` """ -import base64 import json import logging -import lzma from collections import namedtuple from itertools import chain from pathlib import Path from typing import TYPE_CHECKING, Any, Callable, ClassVar, Dict, List, Optional, Set, Union import pydantic -from cosl import JujuTopology +from cosl import GrafanaDashboard, JujuTopology from cosl.rules import AlertRules from ops.charm import RelationChangedEvent from ops.framework import EventBase, EventSource, Object, ObjectEvents @@ -236,7 +234,7 @@ class _MetricsEndpointDict(TypedDict): LIBID = "dc15fa84cef84ce58155fb84f6c6213a" LIBAPI = 0 -LIBPATCH = 6 +LIBPATCH = 7 PYDEPS = ["cosl", "pydantic < 2"] @@ -251,31 +249,6 @@ class _MetricsEndpointDict(TypedDict): SnapEndpoint = namedtuple("SnapEndpoint", "owner, name") -class GrafanaDashboard(str): - """Grafana Dashboard encoded json; lzma-compressed.""" - - # TODO Replace this with a custom type when pydantic v2 released (end of 2023 Q1?) - # https://github.com/pydantic/pydantic/issues/4887 - @staticmethod - def _serialize(raw_json: Union[str, bytes]) -> "GrafanaDashboard": - if not isinstance(raw_json, bytes): - raw_json = raw_json.encode("utf-8") - encoded = base64.b64encode(lzma.compress(raw_json)).decode("utf-8") - return GrafanaDashboard(encoded) - - def _deserialize(self) -> Dict: - try: - raw = lzma.decompress(base64.b64decode(self.encode("utf-8"))).decode() - return json.loads(raw) - except json.decoder.JSONDecodeError as e: - logger.error("Invalid Dashboard format: %s", e) - return {} - - def __repr__(self): - """Return string representation of self.""" - return "" - - class CosAgentProviderUnitData(pydantic.BaseModel): """Unit databag model for `cos-agent` relation.""" @@ -748,6 +721,10 @@ def metrics_jobs(self) -> List[Dict]: "job_name": job["job_name"], "metrics_path": job["path"], "static_configs": [{"targets": [f"localhost:{job['port']}"]}], + # We include insecure_skip_verify because we are always scraping localhost. + # Even if we have the certs for the scrape targets, we'd rather specify the scrape + # jobs with localhost rather than the SAN DNS the cert was issued for. + "tls_config": {"insecure_skip_verify": True}, } scrape_jobs.append(job) diff --git a/src/cluster.py b/src/cluster.py index 8b896da42f..6f5c748813 100644 --- a/src/cluster.py +++ b/src/cluster.py @@ -43,6 +43,8 @@ PG_BASE_CONF_PATH = f"{POSTGRESQL_CONF_PATH}/postgresql.conf" +RUNNING_STATES = ["running", "streaming"] + class NotReadyError(Exception): """Raised when not all cluster members healthy or finished initial sync.""" @@ -297,7 +299,7 @@ def are_all_members_ready(self) -> bool: # because sometimes there may exist (for some period of time) only # replicas after a failed switchover. return all( - member["state"] == "running" for member in cluster_status.json()["members"] + member["state"] in RUNNING_STATES for member in cluster_status.json()["members"] ) and any(member["role"] == "leader" for member in cluster_status.json()["members"]) def get_patroni_health(self) -> Dict[str, str]: @@ -366,7 +368,7 @@ def member_started(self) -> bool: except RetryError: return False - return response["state"] == "running" + return response["state"] in RUNNING_STATES @property def member_inactive(self) -> bool: @@ -381,7 +383,7 @@ def member_inactive(self) -> bool: except RetryError: return True - return response["state"] not in ["running", "starting", "restarting"] + return response["state"] not in [*RUNNING_STATES, "starting", "restarting"] @property def member_replication_lag(self) -> str: diff --git a/src/constants.py b/src/constants.py index 608e79ca0f..b370fe10fd 100644 --- a/src/constants.py +++ b/src/constants.py @@ -32,7 +32,7 @@ # Snap constants. PGBACKREST_EXECUTABLE = "charmed-postgresql.pgbackrest" POSTGRESQL_SNAP_NAME = "charmed-postgresql" -SNAP_PACKAGES = [(POSTGRESQL_SNAP_NAME, {"revision": "87"})] +SNAP_PACKAGES = [(POSTGRESQL_SNAP_NAME, {"revision": "89"})] SNAP_COMMON_PATH = "/var/snap/charmed-postgresql/common" SNAP_CURRENT_PATH = "/var/snap/charmed-postgresql/current"