Skip to content

Commit 1dd02b9

Browse files
committed
fix: update charm dependent libs
1 parent 94f43ea commit 1dd02b9

8 files changed

Lines changed: 2769 additions & 1433 deletions

File tree

lib/charms/data_platform_libs/v0/data_interfaces.py

Lines changed: 1238 additions & 76 deletions
Large diffs are not rendered by default.

lib/charms/grafana_k8s/v0/grafana_dashboard.py

Lines changed: 76 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -187,8 +187,9 @@ def __init__(self, *args):
187187
import uuid
188188
from pathlib import Path
189189
from typing import Any, Callable, Dict, List, Optional, Tuple
190-
191190
import yaml
191+
from cosl import DashboardPath40UID, LZMABase64
192+
from cosl.types import type_convert_stored
192193
from ops.charm import (
193194
CharmBase,
194195
HookEvent,
@@ -203,12 +204,9 @@ def __init__(self, *args):
203204
EventSource,
204205
Object,
205206
ObjectEvents,
206-
StoredDict,
207-
StoredList,
208207
StoredState,
209208
)
210209
from ops.model import Relation
211-
from cosl import LZMABase64, DashboardPath40UID
212210

213211
# The unique Charmhub library identifier, never change it
214212
LIBID = "c49eb9c7dfef40c7b6235ebd67010a3f"
@@ -219,7 +217,7 @@ def __init__(self, *args):
219217
# Increment this PATCH version before using `charmcraft publish-lib` or reset
220218
# to 0 if you are raising the major API version
221219

222-
LIBPATCH = 44
220+
LIBPATCH = 49
223221

224222
PYDEPS = ["cosl >= 0.0.50"]
225223

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

589587
# Put our own variables in the template
588+
# We only want to inject our own dropdowns IFF they are NOT
589+
# already in the template coming over relation data.
590+
# We'll store all dropdowns in the template from the provider
591+
# in a set. We'll add our own if they are not in this set.
592+
existing_names = {
593+
item.get("name")
594+
for item in dict_content["templating"]["list"]
595+
}
596+
590597
for d in template_dropdowns: # type: ignore
591-
if d not in dict_content["templating"]["list"]:
598+
if d.get("name") not in existing_names:
592599
dict_content["templating"]["list"].insert(0, d)
600+
existing_names.add(d.get("name"))
593601

594602
dict_content = cls._replace_template_fields(dict_content, datasources, existing_templates)
595603
return json.dumps(dict_content)
@@ -946,23 +954,33 @@ def _replace_uid(
946954
# If we're running this from within an aggregator (such as grafana agent), then the uid was
947955
# already rendered there, so we do not want to overwrite it with a uid generated from aggregator's info.
948956
# We overwrite the uid only if it's not a valid "Path40" uid.
949-
if not DashboardPath40UID.is_valid(original_uid := dashboard_dict.get("uid", "")):
957+
original_uid = dashboard_dict.get("uid", "")
958+
959+
if DashboardPath40UID.is_valid(original_uid):
960+
logger.debug(
961+
"Processed dashboard '%s': kept original uid '%s'", dashboard_path, original_uid
962+
)
963+
return
964+
965+
try:
950966
rel_path = str(
951967
dashboard_path.relative_to(charm_dir)
952968
if dashboard_path.is_absolute()
953969
else dashboard_path
954970
)
955-
dashboard_dict["uid"] = DashboardPath40UID.generate(charm_name, rel_path)
956-
logger.debug(
957-
"Processed dashboard '%s': replaced original uid '%s' with '%s'",
958-
dashboard_path,
959-
original_uid,
960-
dashboard_dict["uid"],
961-
)
971+
except ValueError:
972+
uid = DashboardPath40UID.generate(charm_name, str(dashboard_path))
962973
else:
963-
logger.debug(
964-
"Processed dashboard '%s': kept original uid '%s'", dashboard_path, original_uid
965-
)
974+
uid = DashboardPath40UID.generate(charm_name, rel_path)
975+
976+
977+
logger.debug(
978+
"Processed dashboard '%s': replaced original uid '%s' with '%s'",
979+
dashboard_path,
980+
original_uid,
981+
uid,
982+
)
983+
dashboard_dict["uid"] = uid
966984

967985
@classmethod
968986
def _add_tags(cls, dashboard_dict: dict, charm_name: str):
@@ -995,7 +1013,7 @@ def _is_dashboard(p: Path) -> bool:
9951013

9961014
dashboard_templates = {}
9971015

998-
for path in filter(_is_dashboard, Path(dashboards_path).glob("*")):
1016+
for path in filter(_is_dashboard, Path(dashboards_path).glob("**/*")):
9991017
try:
10001018
dashboard_dict = json.loads(path.read_bytes())
10011019
except json.JSONDecodeError as e:
@@ -1027,18 +1045,6 @@ def _is_dashboard(p: Path) -> bool:
10271045
return dashboard_templates
10281046

10291047

1030-
def _type_convert_stored(obj):
1031-
"""Convert Stored* to their appropriate types, recursively."""
1032-
if isinstance(obj, StoredList):
1033-
return list(map(_type_convert_stored, obj))
1034-
if isinstance(obj, StoredDict):
1035-
rdict = {} # type: Dict[Any, Any]
1036-
for k in obj.keys():
1037-
rdict[k] = _type_convert_stored(obj[k])
1038-
return rdict
1039-
return obj
1040-
1041-
10421048
class GrafanaDashboardsChanged(EventBase):
10431049
"""Event emitted when Grafana dashboards change."""
10441050

@@ -1346,7 +1352,7 @@ def _upset_dashboards_on_relation(self, relation: Relation) -> None:
13461352
# It's completely ridiculous to add a UUID, but if we don't have some
13471353
# pseudo-random value, this never makes it across 'juju set-state'
13481354
stored_data = {
1349-
"templates": _type_convert_stored(self._stored.dashboard_templates), # pyright: ignore
1355+
"templates": type_convert_stored(self._stored.dashboard_templates), # pyright: ignore
13501356
"uuid": str(uuid.uuid4()),
13511357
}
13521358

@@ -1612,7 +1618,7 @@ def _render_dashboards_and_signal_changed(self, relation: Relation) -> bool: #
16121618
stored_data = rendered_dashboards
16131619
currently_stored_data = self._get_stored_dashboards(relation.id)
16141620

1615-
coerced_data = _type_convert_stored(currently_stored_data) if currently_stored_data else {}
1621+
coerced_data = type_convert_stored(currently_stored_data) if currently_stored_data else {}
16161622

16171623
if not coerced_data == stored_data:
16181624
stored_dashboards = self.get_peer_data("dashboards")
@@ -1639,29 +1645,60 @@ def _remove_all_dashboards_for_relation(self, relation: Relation) -> None:
16391645
self.on.dashboards_changed.emit() # pyright: ignore
16401646

16411647
def _to_external_object(self, relation_id, dashboard):
1648+
decompressed = LZMABase64.decompress(dashboard["content"])
1649+
as_dict = json.loads(decompressed)
1650+
1651+
dashboard_title = as_dict.get("title", "")
1652+
dashboard_uid = as_dict.get("uid", "")
1653+
1654+
try:
1655+
dashboard_version = int(as_dict["version"])
1656+
except (KeyError, ValueError):
1657+
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)
1658+
dashboard_version = 0
1659+
16421660
return {
16431661
"id": dashboard["original_id"],
16441662
"relation_id": relation_id,
16451663
"charm": dashboard["template"]["charm"],
1646-
"content": LZMABase64.decompress(dashboard["content"]),
1664+
"content": decompressed,
1665+
"dashboard_uid": dashboard_uid,
1666+
"dashboard_version": dashboard_version,
1667+
"dashboard_title": dashboard_title,
16471668
}
16481669

16491670
@property
16501671
def dashboards(self) -> List[Dict]:
16511672
"""Get a list of known dashboards across all instances of the monitored relation.
16521673
1674+
Filters out dashboards with the same uid, keeping only the one with the highest version.
1675+
When more than one dashboard have the same uid and version, keep the first one when
1676+
sorted by (relation_id, content) in reverse lexicographic order (highest relid first).
1677+
16531678
Returns: a list of known dashboards. The JSON of each of the dashboards is available
16541679
in the `content` field of the corresponding `dict`.
16551680
"""
1656-
dashboards = []
1681+
d: Dict[str, dict] = {}
16571682

16581683
for _, (relation_id, dashboards_for_relation) in enumerate(
16591684
self.get_peer_data("dashboards").items()
16601685
):
16611686
for dashboard in dashboards_for_relation:
1662-
dashboards.append(self._to_external_object(relation_id, dashboard))
1687+
obj = self._to_external_object(relation_id, dashboard)
1688+
1689+
key = obj.get("dashboard_uid")
1690+
if key is None or str(key).strip() == "":
1691+
# At this point, we assume that a `.uid` is present so we do not render a fallback identifier here. Instead, we omit it.
1692+
logger.error("dashboard '%s' from relation id '%s' is missing a '.uid' field; omitted", obj["dashboard_title"], obj["relation_id"])
1693+
continue
1694+
1695+
if key in d:
1696+
d[key] = max(d[key], obj, key=lambda o: (o["dashboard_version"], o["relation_id"], o["content"]))
1697+
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"])
1698+
else:
1699+
d[key] = obj
16631700

1664-
return dashboards
1701+
return list(d.values())
16651702

16661703
def _get_stored_dashboards(self, relation_id: int) -> list:
16671704
"""Pull stored dashboards out of the peer data bucket."""
@@ -1796,7 +1833,7 @@ def _update_remote_grafana(self, _: Optional[RelationEvent] = None) -> None:
17961833
"""Push dashboards to the downstream Grafana relation."""
17971834
# It's still ridiculous to add a UUID here, but needed
17981835
stored_data = {
1799-
"templates": _type_convert_stored(self._stored.dashboard_templates), # pyright: ignore
1836+
"templates": type_convert_stored(self._stored.dashboard_templates), # pyright: ignore
18001837
"uuid": str(uuid.uuid4()),
18011838
}
18021839

@@ -1806,7 +1843,7 @@ def _update_remote_grafana(self, _: Optional[RelationEvent] = None) -> None:
18061843

18071844
def remove_dashboards(self, event: RelationBrokenEvent) -> None:
18081845
"""Remove a dashboard if the relation is broken."""
1809-
app_ids = _type_convert_stored(self._stored.id_mappings.get(event.app.name, "")) # type: ignore
1846+
app_ids = type_convert_stored(self._stored.id_mappings.get(event.app.name, "")) # type: ignore
18101847

18111848
if not app_ids:
18121849
logger.info("Could not look up stored dashboards for %s", event.app.name) # type: ignore
@@ -1817,7 +1854,7 @@ def remove_dashboards(self, event: RelationBrokenEvent) -> None:
18171854
del self._stored.dashboard_templates[id] # type: ignore
18181855

18191856
stored_data = {
1820-
"templates": _type_convert_stored(self._stored.dashboard_templates), # pyright: ignore
1857+
"templates": type_convert_stored(self._stored.dashboard_templates), # pyright: ignore
18211858
"uuid": str(uuid.uuid4()),
18221859
}
18231860

0 commit comments

Comments
 (0)