Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement grafana_datasource_exchange #476

Merged
merged 5 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
4 changes: 4 additions & 0 deletions metadata.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ provides:
interface: prometheus_scrape
grafana-dashboard:
interface: grafana_dashboard
send-datasource:
interface: grafana_datasource_exchange
description: |
Integration to share with other COS components this charm's grafana datasources, and receive theirs.

requires:
alertmanager:
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
cosl>=0.0.12
cosl>=0.0.47
# pinned to 2.16 as 2.17 breaks our unittests
ops
kubernetes
Expand Down
38 changes: 38 additions & 0 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
from charms.tempo_coordinator_k8s.v0.tracing import TracingEndpointRequirer, charm_tracing_config
from charms.traefik_k8s.v1.ingress_per_unit import IngressPerUnitRequirer
from cosl import JujuTopology
from cosl.interfaces.datasource_exchange import DatasourceDict, DatasourceExchange
from ops import CollectStatusEvent, StoredState
from ops.charm import CharmBase
from ops.main import main
Expand Down Expand Up @@ -231,6 +232,12 @@ def __init__(self, *args):
self.charm_tracing, self._ca_cert_path
)

self.datasource_exchange = DatasourceExchange(
self,
provider_endpoint="send-datasource",
requirer_endpoint=None,
)

self.framework.observe(
self.workload_tracing.on.endpoint_changed, # type: ignore
self._on_workload_tracing_endpoint_changed,
Expand All @@ -252,12 +259,29 @@ def __init__(self, *args):
self._loki_push_api_alert_rules_changed,
)
self.framework.observe(self.on.logging_relation_changed, self._on_logging_relation_changed)

self.framework.observe(
self.on.send_datasource_relation_changed, self._on_grafana_source_changed
michaeldmitry marked this conversation as resolved.
Show resolved Hide resolved
)
self.framework.observe(
self.on.send_datasource_relation_departed, self._on_grafana_source_changed
)
self.framework.observe(
self.on.grafana_source_relation_changed, self._on_grafana_source_changed
)
self.framework.observe(
self.on.grafana_source_relation_departed, self._on_grafana_source_changed
)

self.framework.observe(self.on.collect_unit_status, self._on_collect_unit_status)

##############################################
# CHARM HOOKS HANDLERS #
##############################################

def _on_grafana_source_changed(self, _):
self._configure()
michaeldmitry marked this conversation as resolved.
Show resolved Hide resolved

def _on_collect_unit_status(self, event: CollectStatusEvent):
# "Pull" statuses
# TODO refactor _configure to turn the "rules" status into a "pull" status.
Expand Down Expand Up @@ -578,6 +602,7 @@ def _configure(self): # noqa: C901
self.grafana_source_provider.update_source(source_url=self._external_url)
self.loki_provider.update_endpoint(url=self._external_url)
self.catalogue.update_item(item=self._catalogue_item)
self._update_datasource_exchange()

def _update_config(self, config: dict) -> bool:
if self._running_config() != config:
Expand Down Expand Up @@ -871,6 +896,19 @@ def _tsdb_versions_migration_dates(self) -> List[Dict[str, str]]:
ret.append({"version": "v13", "date": tomorrow.strftime(date_format)})
return ret

def _update_datasource_exchange(self) -> None:
"""Update the grafana-datasource-exchange relations."""
if not self.unit.is_leader():
return

grafana_uids_to_units_to_uids = self.grafana_source_provider.get_source_uids()
raw_datasources: List[DatasourceDict] = []

for grafana_uid, ds_uids in grafana_uids_to_units_to_uids.items():
for _unit_name, ds_uid in ds_uids.items():
michaeldmitry marked this conversation as resolved.
Show resolved Hide resolved
raw_datasources.append({"type": "loki", "uid": ds_uid, "grafana_uid": grafana_uid})
self.datasource_exchange.publish(datasources=raw_datasources)


if __name__ == "__main__":
main(LokiOperatorCharm)
83 changes: 83 additions & 0 deletions tests/interface/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Copyright 2024 Canonical Ltd.
# See LICENSE file for licensing details.

import json
from contextlib import ExitStack
from unittest.mock import MagicMock, patch

import ops
import pytest
from charms.tempo_coordinator_k8s.v0.charm_tracing import charm_tracing_disabled
from interface_tester import InterfaceTester
from ops import ActiveStatus
from scenario.state import Container, Exec, Relation, State

from charm import LokiOperatorCharm


@pytest.fixture(autouse=True, scope="module")
def patch_all():
with ExitStack() as stack:
stack.enter_context(patch("lightkube.core.client.GenericSyncClient"))
stack.enter_context(
patch.multiple(
"charms.observability_libs.v0.kubernetes_compute_resources_patch.KubernetesComputeResourcesPatch",
_namespace="test-namespace",
_patch=lambda _: None,
is_ready=MagicMock(return_value=True),
get_status=lambda _: ActiveStatus(""),
)
)
stack.enter_context(charm_tracing_disabled())

yield


loki_container = Container(
name="loki",
can_connect=True,
execs={Exec(["update-ca-certificates", "--fresh"], return_code=0)},
layers={"loki": ops.pebble.Layer({"services": {"loki": {}}})},
service_statuses={"loki": ops.pebble.ServiceStatus.ACTIVE},
)

grafana_source_relation = Relation(
"grafana-source",
remote_app_data={
"datasource_uids": json.dumps({"loki/0": "01234"}),
"grafana_uid": "5678",
},
)

grafana_datasource_exchange_relation = Relation(
"send-datasource",
remote_app_data={
"datasources": json.dumps(
{"loki/0": {"type": "loki", "uid": "01234", "grafana_uid": "5678"}}
michaeldmitry marked this conversation as resolved.
Show resolved Hide resolved
)
},
)


@pytest.fixture
def grafana_datasource_tester(interface_tester: InterfaceTester):
interface_tester.configure(
charm_type=LokiOperatorCharm,
state_template=State(
leader=True, containers=[loki_container], relations=[grafana_source_relation]
),
)
yield interface_tester


@pytest.fixture
def grafana_datasource_exchange_tester(interface_tester: InterfaceTester):
interface_tester.configure(
charm_type=LokiOperatorCharm,
state_template=State(
leader=True,
containers=[loki_container],
relations=[grafana_source_relation, grafana_datasource_exchange_relation],
),
)
yield interface_tester
13 changes: 13 additions & 0 deletions tests/interface/test_grafana_datasource_exchange.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright 2024 Canonical Ltd.
# See LICENSE file for licensing details.
from interface_tester import InterfaceTester


def test_grafana_datasource_exchange_v0_interface(
grafana_datasource_exchange_tester: InterfaceTester,
):
grafana_datasource_exchange_tester.configure(
interface_name="grafana_datasource_exchange",
interface_version=0,
)
grafana_datasource_exchange_tester.run()
11 changes: 11 additions & 0 deletions tests/interface/test_grafana_source.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Copyright 2024 Canonical Ltd.
# See LICENSE file for licensing details.
from interface_tester import InterfaceTester


def test_grafana_datasource_v0_interface(grafana_datasource_tester: InterfaceTester):
grafana_datasource_tester.configure(
interface_name="grafana_datasource",
interface_version=0,
)
grafana_datasource_tester.run()
20 changes: 18 additions & 2 deletions tests/scenario/conftest.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from unittest.mock import PropertyMock, patch

import ops
import pytest
from charms.tempo_coordinator_k8s.v0.charm_tracing import charm_tracing_disabled
from ops.testing import Context
from scenario import Container, Exec

from charm import LokiOperatorCharm

Expand All @@ -11,7 +14,7 @@ def tautology(*_, **__) -> bool:


@pytest.fixture
def loki_charm():
def loki_charm(tmp_path):
with patch.multiple(
"charm.KubernetesComputeResourcesPatch",
_namespace=PropertyMock("test-namespace"),
Expand All @@ -20,9 +23,22 @@ def loki_charm():
):
with patch("socket.getfqdn", new=lambda *args: "fqdn"):
with patch("lightkube.core.client.GenericSyncClient"):
yield LokiOperatorCharm
with charm_tracing_disabled():
with patch("subprocess.run"):
michaeldmitry marked this conversation as resolved.
Show resolved Hide resolved
yield LokiOperatorCharm


@pytest.fixture
def context(loki_charm):
return Context(loki_charm)


@pytest.fixture(scope="function")
def loki_container():
return Container(
"loki",
can_connect=True,
execs={Exec(["update-ca-certificates", "--fresh"], return_code=0)},
layers={"loki": ops.pebble.Layer({"services": {"loki": {}}})},
service_statuses={"loki": ops.pebble.ServiceStatus.INACTIVE},
)
78 changes: 78 additions & 0 deletions tests/scenario/test_datasource_exchange.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Copyright 2024 Canonical Ltd.
# See LICENSE file for licensing details.

import json

import pytest
from cosl.interfaces.datasource_exchange import (
DatasourceExchange,
DSExchangeAppData,
GrafanaDatasource,
)
from scenario import Relation, State

from charm import LokiOperatorCharm

ds_tempo = [
{"type": "tempo", "uid": "3", "grafana_uid": "4"},
]

ds_mimir = [
{"type": "prometheus", "uid": "8", "grafana_uid": "9"},
]

mimir_dsx = Relation(
"send-datasource",
remote_app_data=DSExchangeAppData(datasources=json.dumps(ds_mimir)).dump(),
)
tempo_dsx = Relation(
"send-datasource",
remote_app_data=DSExchangeAppData(datasources=json.dumps(ds_tempo)).dump(),
)

ds = Relation(
"grafana-source",
remote_app_data={
"grafana_uid": "9",
"datasource_uids": json.dumps({"loki/0": "1234"}),
},
)


@pytest.mark.parametrize("event_type", ("changed", "created", "joined"))
@pytest.mark.parametrize("relation_to_observe", (ds, mimir_dsx, tempo_dsx))
def test_datasource_send(context, loki_container, relation_to_observe, event_type):

state_in = State(
relations=[
ds,
mimir_dsx,
tempo_dsx,
],
containers=[loki_container],
leader=True,
)

# WHEN we receive a datasource-related event
with context(
getattr(context.on, f"relation_{event_type}")(relation_to_observe), state_in
) as mgr:
charm: LokiOperatorCharm = mgr.charm
# THEN we can find all received datasource uids
dsx: DatasourceExchange = charm.datasource_exchange
received = dsx.received_datasources
assert received == (
GrafanaDatasource(type="tempo", uid="3", grafana_uid="4"),
GrafanaDatasource(type="prometheus", uid="8", grafana_uid="9"),
)
state_out = mgr.run()

# AND THEN we publish our own datasource information to mimir and tempo
published_dsx_mimir = state_out.get_relation(mimir_dsx.id).local_app_data
published_dsx_tempo = state_out.get_relation(tempo_dsx.id).local_app_data
assert published_dsx_tempo == published_dsx_mimir
assert json.loads(published_dsx_tempo["datasources"])[0] == {
"type": "loki",
"uid": "1234",
"grafana_uid": "9",
}
9 changes: 9 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,12 @@ deps =
minio
commands =
pytest -v --tb native --log-cli-level=INFO --color=yes -s {posargs} {toxinidir}/tests/integration

[testenv:interface]
description = Run interface tests
deps =
pytest
-r{toxinidir}/requirements.txt
pytest-interface-tester
commands =
pytest -v --tb native --log-cli-level=INFO -s {posargs} {[vars]tst_path}/interface
Loading