diff --git a/lib/charms/grafana_agent/v0/cos_agent.py b/lib/charms/grafana_agent/v0/cos_agent.py index addd0d5..83911af 100644 --- a/lib/charms/grafana_agent/v0/cos_agent.py +++ b/lib/charms/grafana_agent/v0/cos_agent.py @@ -234,7 +234,7 @@ def __init__(self, *args): ) import pydantic -from cosl import GrafanaDashboard, JujuTopology +from cosl import DashboardPath40UID, JujuTopology, LZMABase64 from cosl.rules import AlertRules from ops.charm import RelationChangedEvent from ops.framework import EventBase, EventSource, Object, ObjectEvents @@ -254,9 +254,10 @@ class _MetricsEndpointDict(TypedDict): LIBID = "dc15fa84cef84ce58155fb84f6c6213a" LIBAPI = 0 -LIBPATCH = 16 +LIBPATCH = 17 -PYDEPS = ["cosl", "pydantic"] +# TODO revert to "cosl" after merged +PYDEPS = ["cosl >= 0.50.0", "pydantic"] DEFAULT_RELATION_NAME = "cos-agent" DEFAULT_PEER_RELATION_NAME = "peers" @@ -481,7 +482,7 @@ class CosAgentProviderUnitData(DatabagModel): # this needs to make its way to the gagent leader metrics_alert_rules: dict log_alert_rules: dict - dashboards: List[GrafanaDashboard] + dashboards: List[str] # subordinate is no longer used but we should keep it until we bump the library to ensure # we don't break compatibility. subordinate: Optional[bool] = None @@ -514,7 +515,7 @@ class CosAgentPeersUnitData(DatabagModel): # of the outgoing o11y relations. metrics_alert_rules: Optional[dict] log_alert_rules: Optional[dict] - dashboards: Optional[List[GrafanaDashboard]] + dashboards: Optional[List[str]] # when this whole datastructure is dumped into a databag, it will be nested under this key. # while not strictly necessary (we could have it 'flattened out' into the databag), @@ -742,12 +743,20 @@ def _log_alert_rules(self) -> Dict: return alert_rules.as_dict() @property - def _dashboards(self) -> List[GrafanaDashboard]: - dashboards: List[GrafanaDashboard] = [] + def _dashboards(self) -> List[str]: + dashboards: List[str] = [] for d in self._dashboard_dirs: for path in Path(d).glob("*"): - dashboard = GrafanaDashboard._serialize(path.read_bytes()) - dashboards.append(dashboard) + with open(path, "rt") as fp: + dashboard = json.load(fp) + rel_path = str( + path.relative_to(self._charm.charm_dir) if path.is_absolute() else path + ) + # COSAgentProvider is somewhat analogous to GrafanaDashboardProvider. We need to overwrite the uid here + # because there is currently no other way to communicate the dashboard path separately. + # https://github.com/canonical/grafana-k8s-operator/pull/363 + dashboard["uid"] = DashboardPath40UID.generate(self._charm.meta.name, rel_path) + dashboards.append(LZMABase64.compress(json.dumps(dashboard))) return dashboards @property @@ -1318,7 +1327,7 @@ def dashboards(self) -> List[Dict[str, str]]: seen_apps.append(app_name) for encoded_dashboard in data.dashboards or (): - content = GrafanaDashboard(encoded_dashboard)._deserialize() + content = json.loads(LZMABase64.decompress(encoded_dashboard)) title = content.get("title", "no_title") diff --git a/requirements.txt b/requirements.txt index 9114d86..b83b5b5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ # FIXME: Packing the charm with 2.2.0+139.gd011d92 will not include dependencies in PYDEPS key: # https://chat.charmhub.io/charmhub/pl/wngp665ycjnb78ar9ojrfhxjkr # That's why we are including cosl here until the bug in charmcraft is solved -cosl +cosl >= 0.50.0 ops > 2.5.0 pydantic < 2 requests diff --git a/tests/scenario/test_models.py b/tests/scenario/test_models.py index f90b6d8..ea79c4a 100644 --- a/tests/scenario/test_models.py +++ b/tests/scenario/test_models.py @@ -5,11 +5,11 @@ import pydantic import pytest -from charms.grafana_agent.v0.cos_agent import CosAgentProviderUnitData, GrafanaDashboard +from charms.grafana_agent.v0.cos_agent import CosAgentProviderUnitData, LZMABase64 class Foo(pydantic.BaseModel): - dash: List[GrafanaDashboard] + dash: List[str] def test_dashboard_validation(): @@ -20,7 +20,7 @@ def test_dashboard_validation(): def test_dashboard_serialization(): raw_dash = {"title": "foo", "bar": "baz"} - encoded_dashboard = GrafanaDashboard._serialize(json.dumps(raw_dash)) + encoded_dashboard = LZMABase64.compress(json.dumps(raw_dash)) data = Foo(dash=[encoded_dashboard]) assert data.json() == '{"dash": ["{encoded_dashboard}"]}'.replace( "{encoded_dashboard}", encoded_dashboard @@ -29,7 +29,7 @@ def test_dashboard_serialization(): def test_cos_agent_provider_unit_data_dashboard_serialization(): raw_dash = {"title": "title", "foo": "bar"} - encoded_dashboard = GrafanaDashboard()._serialize(json.dumps(raw_dash)) + encoded_dashboard = LZMABase64.compress(json.dumps(raw_dash)) data = CosAgentProviderUnitData( metrics_alert_rules={}, log_alert_rules={}, @@ -51,7 +51,7 @@ def test_cos_agent_provider_unit_data_dashboard_serialization(): def test_dashboard_deserialization_roundtrip(): raw_dash = {"title": "title", "foo": "bar"} - encoded_dashboard = GrafanaDashboard()._serialize(json.dumps(raw_dash)) + encoded_dashboard = LZMABase64.compress(json.dumps(raw_dash)) raw = { "metrics_alert_rules": {}, "log_alert_rules": {}, @@ -60,7 +60,7 @@ def test_dashboard_deserialization_roundtrip(): "dashboards": [encoded_dashboard], } data = CosAgentProviderUnitData(**raw) - assert GrafanaDashboard(data.dashboards[0])._deserialize() == raw_dash + assert json.loads(LZMABase64.decompress(data.dashboards[0])) == raw_dash def test_cos_agent_provider_tracing_protocols_are_passed(): diff --git a/tests/scenario/test_peer_relation.py b/tests/scenario/test_peer_relation.py index d26f234..777587f 100644 --- a/tests/scenario/test_peer_relation.py +++ b/tests/scenario/test_peer_relation.py @@ -12,14 +12,14 @@ from charms.prometheus_k8s.v1.prometheus_remote_write import ( PrometheusRemoteWriteConsumer, ) -from cosl import GrafanaDashboard +from cosl import LZMABase64 from ops.charm import CharmBase from ops.framework import Framework from ops.testing import Context, PeerRelation, State, SubordinateRelation def encode_as_dashboard(dct: dict): - return GrafanaDashboard._serialize(json.dumps(dct).encode("utf-8")) + return LZMABase64.compress(json.dumps(dct)) def test_fetch_data_from_relation(): @@ -48,7 +48,7 @@ def test_fetch_data_from_relation(): data_peer_1 = data[0] assert len(data_peer_1.dashboards) == 1 dash_out_raw = data_peer_1.dashboards[0] - assert GrafanaDashboard(dash_out_raw)._deserialize() == py_dash + assert json.loads(LZMABase64.decompress(dash_out_raw)) == py_dash class MyRequirerCharm(CharmBase): diff --git a/tests/scenario/test_relation_priority.py b/tests/scenario/test_relation_priority.py index 4051645..6d965a3 100644 --- a/tests/scenario/test_relation_priority.py +++ b/tests/scenario/test_relation_priority.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest -from cosl import GrafanaDashboard +from cosl import LZMABase64 from ops.testing import Context, PeerRelation, State, SubordinateRelation import charm @@ -90,7 +90,7 @@ def test_cos_machine_relation(mock_run, charm_config): "relation_name": "peers", "metrics_alert_rules": {}, "log_alert_rules": {}, - "dashboards": [GrafanaDashboard._serialize('{"very long": "dashboard"}')], + "dashboards": [LZMABase64.compress(json.dumps('{"very long": "dashboard"}'))], } ) } @@ -147,7 +147,7 @@ def test_both_relations(mock_run, charm_config): "relation_name": "peers", "metrics_alert_rules": {}, "log_alert_rules": {}, - "dashboards": [GrafanaDashboard._serialize('{"very long": "dashboard"}')], + "dashboards": [LZMABase64.compress(json.dumps('{"very long": "dashboard"}'))], } ) } diff --git a/tox.ini b/tox.ini index a30244b..7830b8e 100644 --- a/tox.ini +++ b/tox.ini @@ -58,6 +58,7 @@ deps = fs toml responses + cosl >= 0.50.0 commands = coverage run \ --source={[vars]src_path} \ @@ -69,7 +70,7 @@ description = Run scenario tests on LXD deps = -r{toxinidir}/requirements.txt pytest - cosl + cosl >= 0.50.0 ops[testing] commands = pytest -vv --tb native --log-cli-level=INFO -s {posargs} {[vars]tst_path}/scenario