Skip to content

Commit

Permalink
Add revision to the dashboard title of the Grafana
Browse files Browse the repository at this point in the history
With this change, two different OpenSearch applications, in
different revisions, will have separate dashboards when related
to COS.

- data-platform-helpers was added to use the get_charm_revision
- titles of dashboards are changed during the charm upgrade hook
- config_changed was added on refresh_events of COSAgentProvider
  to re-render the dashboards after a charm upgrade
  • Loading branch information
gabrielcocenza committed Dec 2, 2024
1 parent e225961 commit 0dfe057
Show file tree
Hide file tree
Showing 6 changed files with 194 additions and 3 deletions.
44 changes: 44 additions & 0 deletions lib/charms/opensearch/v0/helper_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
# See LICENSE file for licensing details.

"""Utility functions for charms related operations."""
import json
import logging
import os
import re
import subprocess
from pathlib import Path
from time import time_ns
from types import SimpleNamespace
from typing import TYPE_CHECKING, List, Union
Expand All @@ -15,6 +17,7 @@
from charms.opensearch.v0.models import App, PeerClusterApp
from charms.opensearch.v0.opensearch_exceptions import OpenSearchCmdError
from charms.opensearch.v0.opensearch_internal_data import Scope
from data_platform_helpers.version_check import get_charm_revision
from ops import CharmBase
from ops.model import ActiveStatus, StatusBase, Unit

Expand Down Expand Up @@ -228,3 +231,44 @@ def mask_sensitive_information(cmd: str) -> str:
pattern = re.compile(r"(-tspass\s+|-kspass\s+|-storepass\s+|-new\s+|pass:)(\S+)")

return re.sub(pattern, r"\1" + "xxx", cmd)


def update_grafana_dashboards_titles(charm: "OpenSearchBaseCharm") -> None:
"""Update the titles in the specified directory to include the charm revision."""
revision = get_charm_revision(charm.model.unit)
path = charm.charm_dir / "src/grafana_dashboards"

for dashboard_path in path.iterdir():
if dashboard_path.is_file() and dashboard_path.suffix == ".json":
try:
_update_dashboard_title(revision, dashboard_path)
except (json.JSONDecodeError, IOError) as e:
logger.error("Failed to process %s: %s", dashboard_path.name, str(e))
else:
logger.warning("%s is not a valid JSON file", dashboard_path.name)


def _update_dashboard_title(revision: str, dashboard_path: Path) -> None:
"""Update the title of a Grafana dashboard file to include the charm revision."""
with open(dashboard_path, "r") as file:
dashboard = json.load(file)

old_title = dashboard.get("title")
if old_title:
title_prefix = old_title.split(" - Rev")[0]
new_title = f"{old_title} - Rev {revision}"
dashboard["title"] = f"{title_prefix} - Rev {revision}"

logger.info(
"Changing the title of dashboard %s from %s to %s",
dashboard_path.name,
old_title,
new_title,
)

with open(dashboard_path, "w") as file:
json.dump(dashboard, file, indent=4)
else:
logger.warning(
"Dashboard %s does not have title and cannot be updated", dashboard_path.name
)
1 change: 1 addition & 0 deletions lib/charms/opensearch/v0/opensearch_base_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ def __init__(self, *args, distro: Type[OpenSearchDistribution] = None):
metrics_endpoints=[],
scrape_configs=self._scrape_config,
refresh_events=[
self.on.config_changed,
self.on.set_password_action,
self.on.secret_changed,
self.on[PeerRelationName].relation_changed,
Expand Down
22 changes: 20 additions & 2 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pydantic = "^1.10.17, <2"
cryptography = "^43.0.0"
jsonschema = "^4.23.0"
poetry-core = "^1.9.0"
data-platform-helpers = "^0.1.4"


[tool.poetry.group.charm-libs.dependencies]
Expand Down
2 changes: 2 additions & 0 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import ops
from charms.opensearch.v0.constants_charm import InstallError, InstallProgress
from charms.opensearch.v0.helper_charm import update_grafana_dashboards_titles
from charms.opensearch.v0.models import PerformanceType
from charms.opensearch.v0.opensearch_base_charm import OpenSearchBaseCharm
from charms.opensearch.v0.opensearch_exceptions import OpenSearchInstallError
Expand Down Expand Up @@ -137,6 +138,7 @@ def _set_upgrade_status(self):
logger.debug(f"Set app status to {self.app.status}")

def _on_upgrade_charm(self, _):
update_grafana_dashboards_titles(self)
if not self.performance_profile.current:
# We are running (1) install or (2) an upgrade on instance that pre-dates profile
# First, we set this unit's effective profile -> 1G heap and no index templates.
Expand Down
127 changes: 126 additions & 1 deletion tests/unit/lib/test_helper_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,18 @@

"""Unit test for the helper_cluster library."""

import json
import unittest
from pathlib import Path
from unittest.mock import MagicMock, PropertyMock, call, mock_open, patch

from charms.opensearch.v0.constants_charm import PeerRelationName
from charms.opensearch.v0.helper_charm import Status, mask_sensitive_information
from charms.opensearch.v0.helper_charm import (
Status,
_update_dashboard_title,
mask_sensitive_information,
update_grafana_dashboards_titles,
)
from ops.model import BlockedStatus, MaintenanceStatus, WaitingStatus
from ops.testing import Harness

Expand Down Expand Up @@ -64,3 +72,120 @@ def test_mask_sensitive_information(self):

actual_result = mask_sensitive_information(command_to_test)
assert actual_result == expected_result


class TestCOSGrafanaDashboard(unittest.TestCase):

@patch("charms.opensearch.v0.helper_charm.get_charm_revision", return_value=167)
@patch("charms.opensearch.v0.helper_charm.Path.iterdir")
@patch("charms.opensearch.v0.helper_charm._update_dashboard_title")
def test_update_grafana_dashboards_titles(self, mock_update_dashboard, mock_iterdir, _):
mock_charm = MagicMock()
mock_charm.model.unit = MagicMock()
type(mock_charm).charm_dir = PropertyMock(return_value=Path("/fake/charm/dir"))

mock_json_file_1 = MagicMock(spec=Path)
mock_json_file_1.is_file.return_value = True
mock_json_file_1.suffix = ".json"
mock_json_file_1.name = "dashboard1.json"

mock_non_json_file = MagicMock(spec=Path)
mock_non_json_file.is_file.return_value = True
mock_non_json_file.suffix = ".txt"
mock_non_json_file.name = "not_a_dashboard.txt"

mock_json_file_2 = MagicMock(spec=Path)
mock_json_file_2.is_file.return_value = True
mock_json_file_2.suffix = ".json"
mock_json_file_2.name = "dashboard2.json"

mock_iterdir.return_value = [mock_json_file_1, mock_non_json_file, mock_json_file_2]

update_grafana_dashboards_titles(mock_charm)

# non-json files are not called
mock_update_dashboard.assert_has_calls(
[
call(167, mock_json_file_1),
call(167, mock_json_file_2),
],
any_order=True,
)

self.assertEqual(mock_update_dashboard.call_count, 2)

@patch("charms.opensearch.v0.helper_charm.get_charm_revision", return_value=167)
@patch("charms.opensearch.v0.helper_charm.logger")
@patch("charms.opensearch.v0.helper_charm.Path.iterdir")
@patch("charms.opensearch.v0.helper_charm._update_dashboard_title")
def test_update_grafana_dashboards_titles_json_exception(
self, mock_update_dashboard, mock_iterdir, mock_logger, _
):
mock_charm = MagicMock()
mock_charm.model.unit = MagicMock()
type(mock_charm).charm_dir = PropertyMock(return_value=Path("/fake/charm/dir"))

mock_json_file_1 = MagicMock(spec=Path)
mock_json_file_1.is_file.return_value = True
mock_json_file_1.suffix = ".json"
mock_json_file_1.name = "dashboard1.json"

mock_iterdir.return_value = [mock_json_file_1]

mock_update_dashboard.side_effect = json.JSONDecodeError("Error", "Error", 0)

update_grafana_dashboards_titles(mock_charm)

mock_logger.error.assert_called_once()

@patch(
"builtins.open",
new_callable=mock_open,
read_data=json.dumps({"title": "Charmed OpenSearch"}),
)
@patch("json.dump")
def test_update_dashboard_title_no_prior_revision(self, mock_json_dump, mock_open_func):

_update_dashboard_title(167, MagicMock())

expected_updated_dashboard = {"title": "Charmed OpenSearch - Rev 167"}
mock_json_dump.assert_called_once_with(
expected_updated_dashboard, mock_open_func(), indent=4
)

@patch(
"builtins.open",
new_callable=mock_open,
read_data=json.dumps({"title": "Charmed OpenSearch - Rev 166"}),
)
@patch("json.dump")
def test_update_dashboard_title_prior_revision(
self,
mock_json_dump,
mock_open_func,
):

_update_dashboard_title("167", MagicMock())

expected_updated_dashboard = {"title": "Charmed OpenSearch - Rev 167"}
mock_json_dump.assert_called_once_with(
expected_updated_dashboard, mock_open_func(), indent=4
)

@patch("charms.opensearch.v0.helper_charm.logger")
@patch(
"builtins.open",
new_callable=mock_open,
read_data=json.dumps({"my-content": "content"}),
)
@patch("json.dump")
def test_update_dashboard_title_json_no_title(
self,
_,
__,
mock_logger,
):

_update_dashboard_title("167", MagicMock())

mock_logger.warning.assert_called_once()

0 comments on commit 0dfe057

Please sign in to comment.