Skip to content
Open
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
130 changes: 103 additions & 27 deletions lib/charms/data_platform_libs/v0/data_interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,7 @@ def _on_subject_requested(self, event: SubjectRequestedEvent):

# Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version
LIBPATCH = 55
LIBPATCH = 58

PYDEPS = ["ops>=2.0.0"]

Expand Down Expand Up @@ -842,6 +842,11 @@ def _legacy_compat_find_secret_by_old_label(self) -> None:
self._secret_meta = self._model.get_secret(label=label)
except SecretNotFoundError:
pass
except ModelError as e:
# Permission denied can be raised if the secret exists but is not yet granted to us.
if "permission denied" in str(e):
return
raise
else:
if label != self.label:
self.current_label = label
Expand Down Expand Up @@ -876,6 +881,8 @@ def _legacy_migration_to_new_label_if_needed(self) -> None:
except ModelError as err:
if MODEL_ERRORS["not_leader"] not in str(err):
raise
if "permission denied" not in str(err):
raise
self.current_label = None

##########################################################################
Expand Down Expand Up @@ -2071,6 +2078,7 @@ def __init__(
requested_entity_secret: Optional[str] = None,
requested_entity_name: Optional[str] = None,
requested_entity_password: Optional[str] = None,
prefix_matching: Optional[str] = None,
):
"""Manager of base client relations."""
super().__init__(model, relation_name)
Expand All @@ -2081,6 +2089,7 @@ def __init__(
self.requested_entity_secret = requested_entity_secret
self.requested_entity_name = requested_entity_name
self.requested_entity_password = requested_entity_password
self.prefix_matching = prefix_matching

if (
self.requested_entity_secret or self.requested_entity_name
Expand Down Expand Up @@ -3258,6 +3267,14 @@ def requested_entity_secret_content(self) -> Optional[Dict[str, Optional[str]]]:
logger.warning("Invalid requested-entity-secret: no entity name")
return names

@property
def prefix_matching(self) -> Optional[str]:
"""Returns the prefix matching strategy that were requested."""
if not self.relation.app:
return None

return self.relation.data[self.relation.app].get("prefix-matching")


class DatabaseEntityRequestedEvent(DatabaseProvidesEvent, EntityProvidesEvent):
"""Event emitted when a new entity is requested for use on this relation."""
Expand Down Expand Up @@ -3364,6 +3381,16 @@ def version(self) -> Optional[str]:

return self.relation.data[self.relation.app].get("version")

@property
def prefix_databases(self) -> Optional[List[str]]:
"""Returns a list of databases matching a prefix."""
if not self.relation.app:
return None

if prefixed_databases := self.relation.data[self.relation.app].get("prefix-databases"):
return prefixed_databases.split(",")
return []


class DatabaseCreatedEvent(AuthenticationEvent, DatabaseRequiresEvent):
"""Event emitted when a new database is created for use on this relation."""
Expand All @@ -3381,6 +3408,10 @@ class DatabaseReadOnlyEndpointsChangedEvent(AuthenticationEvent, DatabaseRequire
"""Event emitted when the read only endpoints are changed."""


class DatabasePrefixDatabasesChangedEvent(AuthenticationEvent, DatabaseRequiresEvent):
"""Event emitted when the prefix databases are changed."""


class DatabaseRequiresEvents(RequirerCharmEvents):
"""Database events.

Expand All @@ -3391,6 +3422,7 @@ class DatabaseRequiresEvents(RequirerCharmEvents):
database_entity_created = EventSource(DatabaseEntityCreatedEvent)
endpoints_changed = EventSource(DatabaseEndpointsChangedEvent)
read_only_endpoints_changed = EventSource(DatabaseReadOnlyEndpointsChangedEvent)
prefix_databases_changed = EventSource(DatabasePrefixDatabasesChangedEvent)


# Database Provider and Requires
Expand All @@ -3416,6 +3448,18 @@ def set_database(self, relation_id: int, database_name: str) -> None:
"""
self.update_relation_data(relation_id, {"database": database_name})

def set_prefix_databases(self, relation_id: int, databases: List[str]) -> None:
"""Set a coma separated list of databases matching a prefix.

This function writes in the application data bag, therefore,
only the leader unit can call it.

Args:
relation_id: the identifier for a particular relation.
databases: list of database names matching the requested prefix.
"""
self.update_relation_data(relation_id, {"prefix-databases": ",".join(sorted(databases))})

def set_endpoints(self, relation_id: int, connection_strings: str) -> None:
"""Set database primary connections.

Expand Down Expand Up @@ -3588,6 +3632,7 @@ def __init__(
requested_entity_secret: Optional[str] = None,
requested_entity_name: Optional[str] = None,
requested_entity_password: Optional[str] = None,
prefix_matching: Optional[str] = None,
):
"""Manager of database client relations."""
super().__init__(
Expand All @@ -3601,6 +3646,7 @@ def __init__(
requested_entity_secret,
requested_entity_name,
requested_entity_password,
prefix_matching,
)
self.database = database_name
self.relations_aliases = relations_aliases
Expand Down Expand Up @@ -3700,6 +3746,10 @@ def __init__(
f"{relation_alias}_read_only_endpoints_changed",
DatabaseReadOnlyEndpointsChangedEvent,
)
self.on.define_event(
f"{relation_alias}_prefix_databases_changed",
DatabasePrefixDatabasesChangedEvent,
)

def _on_secret_changed_event(self, event: SecretChangedEvent):
"""Event notifying about a new value of a secret."""
Expand Down Expand Up @@ -3792,6 +3842,8 @@ def _on_relation_created_event(self, event: RelationCreatedEvent) -> None:
event_data["entity-permissions"] = self.relation_data.entity_permissions
if self.relation_data.requested_entity_secret:
event_data["requested-entity-secret"] = self.relation_data.requested_entity_secret
if self.relation_data.prefix_matching:
event_data["prefix-matching"] = self.relation_data.prefix_matching

# Create helper secret if needed
if (
Expand Down Expand Up @@ -3884,32 +3936,22 @@ def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
# To avoid unnecessary application restarts do not trigger other events.
return

# Emit an endpoints changed event if the database
# added or changed this info in the relation databag.
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())
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")

# To avoid unnecessary application restarts do not trigger other events.
return

# Emit a read only endpoints changed event if the database
# added or changed this info in the relation databag.
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())
getattr(self.on, "read_only_endpoints_changed").emit(
event.relation, app=event.app, unit=event.unit
)

# Emit the aliased event (if any).
self._emit_aliased_event(event, "read_only_endpoints_changed")
for key, event_name in [
("endpoints", "endpoints_changed"),
("read-only-endpoints", "read_only_endpoints_changed"),
("prefix-databases", "prefix_databases_changed"),
]:
# Emit a change event if the key changed.
if key in diff.added or key in diff.changed:
# Emit the default event (the one without an alias).
logger.info("%s changed on %s", key, datetime.now())
getattr(self.on, event_name).emit(event.relation, app=event.app, unit=event.unit)

# Emit the aliased event (if any).
self._emit_aliased_event(event, event_name)

# To avoid unnecessary application restarts do not trigger other events.
return


class DatabaseRequires(DatabaseRequirerData, DatabaseRequirerEventHandlers):
Expand All @@ -3930,6 +3972,7 @@ def __init__(
requested_entity_secret: Optional[str] = None,
requested_entity_name: Optional[str] = None,
requested_entity_password: Optional[str] = None,
prefix_matching: Optional[str] = None,
):
DatabaseRequirerData.__init__(
self,
Expand All @@ -3946,6 +3989,7 @@ def __init__(
requested_entity_secret,
requested_entity_name,
requested_entity_password,
prefix_matching,
)
DatabaseRequirerEventHandlers.__init__(self, charm, self)

Expand Down Expand Up @@ -4231,6 +4275,14 @@ def _on_secret_changed_event(self, event: SecretChangedEvent):
if relation.app == self.charm.app:
logging.info("Secret changed event ignored for Secret Owner")

if relation.name != self.relation_data.relation_name:
logger.debug(
"Ignoring secret-changed from endpoint %s (expected %s)",
relation.name,
self.relation_data.relation_name,
)
return

remote_unit = None
for unit in relation.units:
if unit.app != self.charm.app:
Expand Down Expand Up @@ -5257,6 +5309,14 @@ def _on_secret_changed_event(self, event: SecretChangedEvent):
)
return

if relation.name != self.relation_data.relation_name:
logger.debug(
"Ignoring secret-changed from endpoint %s (expected %s)",
relation.name,
self.relation_data.relation_name,
)
return

if relation.app == self.charm.app:
logging.info("Secret changed event ignored for Secret Owner")

Expand Down Expand Up @@ -5519,6 +5579,14 @@ def _on_secret_changed_event(self, event: SecretChangedEvent):
)
return

if relation.name != self.relation_data.relation_name:
logger.debug(
"Ignoring secret-changed from endpoint %s (expected %s)",
relation.name,
self.relation_data.relation_name,
)
return

if relation.app == self.charm.app:
logging.info("Secret changed event ignored for Secret Owner")

Expand Down Expand Up @@ -5664,6 +5732,14 @@ def _on_secret_changed_event(self, event: SecretChangedEvent):
if relation.app == self.charm.app:
logging.info("Secret changed event ignored for Secret Owner")

if relation.name != self.relation_data.relation_name:
logger.debug(
"Ignoring secret-changed from endpoint %s (expected %s)",
relation.name,
self.relation_data.relation_name,
)
return

remote_unit = None
for unit in relation.units:
if unit.app != self.charm.app:
Expand Down
54 changes: 47 additions & 7 deletions lib/charms/grafana_k8s/v0/grafana_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,6 @@ def __init__(self, *args):
import uuid
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Tuple

import yaml
from cosl import DashboardPath40UID, LZMABase64
from cosl.types import type_convert_stored
Expand Down Expand Up @@ -218,7 +217,7 @@ def __init__(self, *args):
# Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version

LIBPATCH = 46
LIBPATCH = 48

PYDEPS = ["cosl >= 0.0.50"]

Expand Down Expand Up @@ -586,9 +585,19 @@ def _convert_dashboard_fields(cls, content: str, inject_dropdowns: bool = True)
datasources[template_value["name"]] = template_value["query"].lower()

# Put our own variables in the template
# We only want to inject our own dropdowns IFF they are NOT
# already in the template coming over relation data.
# We'll store all dropdowns in the template from the provider
# in a set. We'll add our own if they are not in this set.
existing_names = {
item.get("name")
for item in dict_content["templating"]["list"]
}

for d in template_dropdowns: # type: ignore
if d not in dict_content["templating"]["list"]:
if d.get("name") not in existing_names:
dict_content["templating"]["list"].insert(0, d)
existing_names.add(d.get("name"))

dict_content = cls._replace_template_fields(dict_content, datasources, existing_templates)
return json.dumps(dict_content)
Expand Down Expand Up @@ -1636,29 +1645,60 @@ def _remove_all_dashboards_for_relation(self, relation: Relation) -> None:
self.on.dashboards_changed.emit() # pyright: ignore

def _to_external_object(self, relation_id, dashboard):
decompressed = LZMABase64.decompress(dashboard["content"])
as_dict = json.loads(decompressed)

dashboard_title = as_dict.get("title", "")
dashboard_uid = as_dict.get("uid", "")

try:
dashboard_version = int(as_dict["version"])
except (KeyError, ValueError):
logger.warning("Dashboard '%s' (uid '%s') is missing a '.version' field or is invalid (must be integer); using '0' as fallback", dashboard_title, dashboard_uid)
dashboard_version = 0

return {
"id": dashboard["original_id"],
"relation_id": relation_id,
"charm": dashboard["template"]["charm"],
"content": LZMABase64.decompress(dashboard["content"]),
"content": decompressed,
"dashboard_uid": dashboard_uid,
"dashboard_version": dashboard_version,
"dashboard_title": dashboard_title,
}

@property
def dashboards(self) -> List[Dict]:
"""Get a list of known dashboards across all instances of the monitored relation.

Filters out dashboards with the same uid, keeping only the one with the highest version.
When more than one dashboard have the same uid and version, keep the first one when
sorted by (relation_id, content) in reverse lexicographic order (highest relid first).

Returns: a list of known dashboards. The JSON of each of the dashboards is available
in the `content` field of the corresponding `dict`.
"""
dashboards = []
d: Dict[str, dict] = {}

for _, (relation_id, dashboards_for_relation) in enumerate(
self.get_peer_data("dashboards").items()
):
for dashboard in dashboards_for_relation:
dashboards.append(self._to_external_object(relation_id, dashboard))
obj = self._to_external_object(relation_id, dashboard)

key = obj.get("dashboard_uid")
if key is None or str(key).strip() == "":
# At this point, we assume that a `.uid` is present so we do not render a fallback identifier here. Instead, we omit it.
logger.error("dashboard '%s' from relation id '%s' is missing a '.uid' field; omitted", obj["dashboard_title"], obj["relation_id"])
continue

if key in d:
d[key] = max(d[key], obj, key=lambda o: (o["dashboard_version"], o["relation_id"], o["content"]))
logger.warning("deduplicate dashboard '%s' (uid '%s') - kept version '%s' from relation id '%s'", d[key]["dashboard_title"], d[key]["dashboard_uid"], d[key]["dashboard_version"], d[key]["relation_id"])
else:
d[key] = obj

return dashboards
return list(d.values())

def _get_stored_dashboards(self, relation_id: int) -> list:
"""Pull stored dashboards out of the peer data bucket."""
Expand Down
Loading