diff --git a/lib/charms/grafana_k8s/v0/grafana_dashboard.py b/lib/charms/grafana_k8s/v0/grafana_dashboard.py index b3945d7a..5aaf0f37 100644 --- a/lib/charms/grafana_k8s/v0/grafana_dashboard.py +++ b/lib/charms/grafana_k8s/v0/grafana_dashboard.py @@ -392,54 +392,60 @@ def __init__(self, *args): } -# def load_from_dir(dashboards_path: Path): -# # Path.glob uses fnmatch on the backend, which is pretty limited, so use a -# # custom function for the filter -# def _is_dashboard(p: Path) -> bool: -# return p.is_file() and p.name.endswith((".json", ".json.tmpl", ".tmpl")) -# -# for path in filter(_is_dashboard, Path(dashboards_path).glob("*")): -# id = "file:{}".format(path.stem) -# -# # If we're running this class from within an aggregator (such as grafana agent), then the uid was -# # already rendered there, so we do not want to overwrite it with a uid generated from aggregator's info. -# # We overwrite the uid only if it's not a valid "Path40" uid. -# try: -# dashboard_dict = json.loads(path.read_bytes()) -# except json.JSONDecodeError as e: -# logger.error("Failed to load dashboard '%s': %s", path, e) -# continue -# if type(dashboard_dict) is not dict: -# logger.error( -# "Invalid dashboard '%s': expected dict, got %s", path, type(dashboard_dict) -# ) -# -# if not DashboardPath40UID.is_valid(original_uid := dashboard_dict.get("uid", "")): -# rel_path = str( -# path.relative_to(self._charm.charm_dir) if path.is_absolute() else path -# ) -# dashboard_dict["uid"] = DashboardPath40UID.generate( -# self._charm.meta.name, rel_path -# ) -# logger.debug( -# "Processed dashboard '%s': replaced original uid '%s' with '%s'", -# path, -# original_uid, -# dashboard_dict["uid"], -# ) -# else: -# logger.debug( -# "Processed dashboard '%s': kept original uid '%s'", path, original_uid -# ) -# -# stored_dashboard_templates[id] = CharmedDashboard._content_to_dashboard_object( -# charm_name=self._charm.meta.name, -# content=LZMABase64.compress(json.dumps(dashboard_dict)), -# inject_dropdowns=inject_dropdowns, -# juju_topology=self._juju_topology, -# ) -# -# stored_dashboard_templates[id]["dashboard_alt_uid"] = self._generate_alt_uid(id) +def load_from_dir( + *, + dashboards_path: Path, + charm_dir: Path, + charm_name: str, + inject_dropdowns: bool, + juju_topology: dict, +) -> dict: + """Load dashboards files from directory into a mapping from "dashboard id" to a so-called "dashboard object".""" + + # Path.glob uses fnmatch on the backend, which is pretty limited, so use a + # custom function for the filter + def _is_dashboard(p: Path) -> bool: + return p.is_file() and p.name.endswith((".json", ".json.tmpl", ".tmpl")) + + dashboard_templates = {} + + for path in filter(_is_dashboard, Path(dashboards_path).glob("*")): + id = "file:{}".format(path.stem) + + # If we're running this class from within an aggregator (such as grafana agent), then the uid was + # already rendered there, so we do not want to overwrite it with a uid generated from aggregator's info. + # We overwrite the uid only if it's not a valid "Path40" uid. + try: + dashboard_dict = json.loads(path.read_bytes()) + except json.JSONDecodeError as e: + logger.error("Failed to load dashboard '%s': %s", path, e) + continue + if type(dashboard_dict) is not dict: + logger.error( + "Invalid dashboard '%s': expected dict, got %s", path, type(dashboard_dict) + ) + + if not DashboardPath40UID.is_valid(original_uid := dashboard_dict.get("uid", "")): + rel_path = str(path.relative_to(charm_dir) if path.is_absolute() else path) + dashboard_dict["uid"] = DashboardPath40UID.generate(charm_name, rel_path) + logger.debug( + "Processed dashboard '%s': replaced original uid '%s' with '%s'", + path, + original_uid, + dashboard_dict["uid"], + ) + else: + logger.debug("Processed dashboard '%s': kept original uid '%s'", path, original_uid) + + dashboard_templates[id] = CharmedDashboard._content_to_dashboard_object( + charm_name=charm_name, + content=LZMABase64.compress(json.dumps(dashboard_dict)), + dashboard_alt_uid=CharmedDashboard._generate_alt_uid(charm_name, id), + inject_dropdowns=inject_dropdowns, + juju_topology=juju_topology, + ) + + return dashboard_templates class RelationNotFoundError(Exception): @@ -965,23 +971,42 @@ def _modify_panel(cls, panel: dict, topology: dict, transformer: "CosTool") -> d @classmethod def _content_to_dashboard_object( - cls, - *, - charm_name, - content: str, - inject_dropdowns: bool = True, - juju_topology: Optional[dict] = None, + cls, + *, + charm_name, + content: str, + dashboard_alt_uid: Optional[str] = None, + inject_dropdowns: bool = True, + juju_topology: Optional[dict] = None, ) -> Dict: if not juju_topology: juju_topology = {} - return { + ret = { "charm": charm_name, "content": content, "juju_topology": juju_topology if inject_dropdowns else {}, "inject_dropdowns": inject_dropdowns, } + if dashboard_alt_uid is not None: + ret["dashboard_alt_uid"] = dashboard_alt_uid + + return ret + + @classmethod + def _generate_alt_uid(cls, charm_name: str, key: str) -> str: + """Generate alternative uid for dashboards. + + Args: + charm_name: The name of the charm (not app; from metadata). + key: A string used (along with charm.meta.name) to build the hash uid. + + Returns: A hash string. + """ + raw_dashboard_alt_uid = "{}-{}".format(charm_name, key) + return hashlib.shake_256(raw_dashboard_alt_uid.encode("utf-8")).hexdigest(8) + def _type_convert_stored(obj): """Convert Stored* to their appropriate types, recursively.""" @@ -1170,12 +1195,11 @@ def add_dashboard(self, content: str, inject_dropdowns: bool = True) -> None: stored_dashboard_templates[id] = CharmedDashboard._content_to_dashboard_object( charm_name=self._charm.meta.name, content=encoded_dashboard, + dashboard_alt_uid=CharmedDashboard._generate_alt_uid(self._charm.meta.name, id), inject_dropdowns=inject_dropdowns, juju_topology=self._juju_topology, ) - stored_dashboard_templates[id]["dashboard_alt_uid"] = self._generate_alt_uid(id) - if self._charm.unit.is_leader(): for dashboard_relation in self._charm.model.relations[self._relation_name]: self._upset_dashboards_on_relation(dashboard_relation) @@ -1217,72 +1241,20 @@ def _update_all_dashboards_from_dir( if dashboard_id.startswith("file:"): del stored_dashboard_templates[dashboard_id] - # Path.glob uses fnmatch on the backend, which is pretty limited, so use a - # custom function for the filter - def _is_dashboard(p: Path) -> bool: - return p.is_file() and p.name.endswith((".json", ".json.tmpl", ".tmpl")) - - for path in filter(_is_dashboard, Path(self._dashboards_path).glob("*")): - # path = Path(path) - id = "file:{}".format(path.stem) - - # If we're running this class from within an aggregator (such as grafana agent), then the uid was - # already rendered there, so we do not want to overwrite it with a uid generated from aggregator's info. - # We overwrite the uid only if it's not a valid "Path40" uid. - try: - dashboard_dict = json.loads(path.read_bytes()) - except json.JSONDecodeError as e: - logger.error("Failed to load dashboard '%s': %s", path, e) - continue - if type(dashboard_dict) is not dict: - logger.error( - "Invalid dashboard '%s': expected dict, got %s", path, type(dashboard_dict) - ) - - if not DashboardPath40UID.is_valid(original_uid := dashboard_dict.get("uid", "")): - rel_path = str( - path.relative_to(self._charm.charm_dir) if path.is_absolute() else path - ) - dashboard_dict["uid"] = DashboardPath40UID.generate( - self._charm.meta.name, rel_path - ) - logger.debug( - "Processed dashboard '%s': replaced original uid '%s' with '%s'", - path, - original_uid, - dashboard_dict["uid"], - ) - else: - logger.debug( - "Processed dashboard '%s': kept original uid '%s'", path, original_uid - ) - - stored_dashboard_templates[id] = CharmedDashboard._content_to_dashboard_object( + stored_dashboard_templates.update( + load_from_dir( + dashboards_path=Path(self._dashboards_path), + charm_dir=self._charm.charm_dir, charm_name=self._charm.meta.name, - content=LZMABase64.compress(json.dumps(dashboard_dict)), inject_dropdowns=inject_dropdowns, juju_topology=self._juju_topology, ) - - stored_dashboard_templates[id]["dashboard_alt_uid"] = self._generate_alt_uid(id) - - self._stored.dashboard_templates = stored_dashboard_templates + ) if self._charm.unit.is_leader(): for dashboard_relation in self._charm.model.relations[self._relation_name]: self._upset_dashboards_on_relation(dashboard_relation) - def _generate_alt_uid(self, key: str) -> str: - """Generate alternative uid for dashboards. - - Args: - key: A string used (along with charm.meta.name) to build the hash uid. - - Returns: A hash string. - """ - raw_dashboard_alt_uid = "{}-{}".format(self._charm.meta.name, key) - return hashlib.shake_256(raw_dashboard_alt_uid.encode("utf-8")).hexdigest(8) - def _reinitialize_dashboard_data(self, inject_dropdowns: bool = True) -> None: """Triggers a reload of dashboard outside of an eventing workflow.