diff --git a/charms/planner-operator/src/cos/grafana_dashboards/go.json b/charms/planner-operator/cos_custom/grafana_dashboards/go.json similarity index 100% rename from charms/planner-operator/src/cos/grafana_dashboards/go.json rename to charms/planner-operator/cos_custom/grafana_dashboards/go.json diff --git a/charms/planner-operator/requirements.txt b/charms/planner-operator/requirements.txt index cb3c9a38..510023a7 100644 --- a/charms/planner-operator/requirements.txt +++ b/charms/planner-operator/requirements.txt @@ -1,3 +1,4 @@ +--only-binary=pluggy ops==3.6.0 paas-charm==1.11.1 requests==2.33.0 diff --git a/charms/planner-operator/src/charm.py b/charms/planner-operator/src/charm.py index 56e8e012..99a69ae1 100755 --- a/charms/planner-operator/src/charm.py +++ b/charms/planner-operator/src/charm.py @@ -7,7 +7,6 @@ import dataclasses import json import logging -import pathlib import typing import ops @@ -62,14 +61,6 @@ def __init__(self, *args: typing.Any) -> None: self._on_planner_relation_broken, ) - def get_cos_dir(self) -> str: - """Get the COS directory for this charm. - - Returns: - The COS directory. - """ - return str((pathlib.Path(__file__).parent / "cos").absolute()) - def _create_app(self): """Patch _create_app to add OpenTelemetry environment variables.""" original_app = super()._create_app() diff --git a/charms/planner-operator/src/cos/loki_alert_rules/.gitkeep b/charms/planner-operator/src/cos/loki_alert_rules/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/charms/planner-operator/src/cos/prometheus_alert_rules/.gitkeep b/charms/planner-operator/src/cos/prometheus_alert_rules/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/charms/tests/integration/conftest.py b/charms/tests/integration/conftest.py index 1d98d5cb..2ee5fb38 100644 --- a/charms/tests/integration/conftest.py +++ b/charms/tests/integration/conftest.py @@ -248,6 +248,24 @@ def integrate_planner_rabbitmq_postgresql_fixture( return planner_app +@pytest.fixture(scope="module", name="any_charm_grafana_consumer_app") +def deploy_any_charm_grafana_consumer_app_fixture(juju: jubilant.Juju) -> str: + """Deploy any charm to act as a grafana-dashboard consumer.""" + app_name = "grafana-consumer" + + juju.deploy( + "any-charm", + app=app_name, + channel="latest/beta", + ) + juju.wait( + lambda status: jubilant.all_active(status, app_name), + timeout=10 * 60, + delay=10, + ) + return app_name + + @pytest.fixture(scope="module", name="any_charm_github_runner_app") def deploy_any_charm_github_runner_app_fixture(juju: jubilant.Juju) -> str: """Deploy any charm to act as a GitHub runner application.""" diff --git a/charms/tests/integration/helpers.py b/charms/tests/integration/helpers.py new file mode 100644 index 00000000..118bfc74 --- /dev/null +++ b/charms/tests/integration/helpers.py @@ -0,0 +1,31 @@ +# Copyright 2026 Canonical Ltd. +# See LICENSE file for licensing details. +import json +import time +from typing import Any + +import jubilant + + +def poll_grafana_dashboard_templates( + juju: jubilant.Juju, consumer_unit: str, attempts: int = 24, interval: int = 5 +) -> dict[str, Any]: + """Poll for dashboard templates via the grafana-dashboard consumer's relation data. + + Checks show-unit on the consumer side, where application-data contains + the provider's data (including the dashboards key). + Returns the templates dict if found, or an empty dict after all attempts are exhausted. + """ + for _ in range(attempts): + stdout = juju.cli("show-unit", consumer_unit, "--format=json") + result = json.loads(stdout) + for relation in result[consumer_unit]["relation-info"]: + if relation["endpoint"] == "require-grafana-dashboard": + dashboards_raw = relation["application-data"].get("dashboards") + if dashboards_raw: + dashboards = json.loads(dashboards_raw) + templates = dashboards.get("templates", {}) + if templates: + return templates + time.sleep(interval) + return {} diff --git a/charms/tests/integration/test_planner.py b/charms/tests/integration/test_planner.py index adde1b73..217ee7b3 100644 --- a/charms/tests/integration/test_planner.py +++ b/charms/tests/integration/test_planner.py @@ -1,10 +1,14 @@ # Copyright 2025 Canonical Ltd. # See LICENSE file for licensing details. +import base64 import json +import lzma import time + import jubilant import pytest import requests +from tests.integration.helpers import poll_grafana_dashboard_templates APP_PORT = 8080 METRICS_PORT = 9464 @@ -204,6 +208,35 @@ def test_planner_enable_disable_flavor_actions( assert flavor_data["is_disabled"] is False, "Flavor should be enabled after action" +@pytest.mark.usefixtures("planner_with_integrations") +def test_planner_grafana_dashboard( + juju: jubilant.Juju, + planner_app: str, + any_charm_grafana_consumer_app: str, +): + """ + arrange: The planner app is deployed with required integrations. + act: Integrate with a grafana-dashboard consumer and inspect relation data. + assert: The grafana-dashboard relation contains dashboard templates including + the custom planner dashboard with planner-specific metrics. + """ + juju.integrate( + f"{planner_app}:grafana-dashboard", + f"{any_charm_grafana_consumer_app}:require-grafana-dashboard", + ) + + templates = poll_grafana_dashboard_templates(juju, f"{any_charm_grafana_consumer_app}/0") + assert templates, "expected non-empty dashboard templates in grafana-dashboard relation" + + all_content = "" + for template in templates.values(): + raw = base64.b64decode(template["content"].encode("utf-8")) + all_content += lzma.decompress(raw).decode("utf-8") + assert "github_runner_planner_webhook_consumed_total" in all_content, ( + "expected custom planner dashboard with planner-specific metrics" + ) + + def poll_flavor_status(unit_ip, flavor_name, token, expected_status, attempts=24, interval=5): """Poll the flavor API until the expected HTTP status is returned.""" for _ in range(attempts): diff --git a/charms/tests/integration/test_webhook_gateway.py b/charms/tests/integration/test_webhook_gateway.py index b213c226..80d0a6f3 100644 --- a/charms/tests/integration/test_webhook_gateway.py +++ b/charms/tests/integration/test_webhook_gateway.py @@ -3,6 +3,7 @@ import jubilant import pytest import requests +from tests.integration.helpers import poll_grafana_dashboard_templates APP_PORT = 8080 METRICS_PORT = 9464 @@ -51,3 +52,23 @@ def test_webhook_gateway_prometheus_metrics( response = requests.get(f"http://{unit_ip}:{METRICS_PORT}/metrics") assert response.status_code == requests.status_codes.codes.OK + + +@pytest.mark.usefixtures("webhook_gateway_with_rabbitmq") +def test_webhook_gateway_grafana_dashboard( + juju: jubilant.Juju, + webhook_gateway_app: str, + any_charm_grafana_consumer_app: str, +): + """ + arrange: The webhook gateway app is deployed with required integrations. + act: Integrate with a grafana-dashboard consumer and inspect relation data. + assert: The grafana-dashboard relation contains non-empty dashboard templates. + """ + juju.integrate( + f"{webhook_gateway_app}:grafana-dashboard", + f"{any_charm_grafana_consumer_app}:require-grafana-dashboard", + ) + + templates = poll_grafana_dashboard_templates(juju, f"{any_charm_grafana_consumer_app}/0") + assert templates, "expected non-empty dashboard templates in grafana-dashboard relation" diff --git a/charms/webhook-gateway-operator/requirements.txt b/charms/webhook-gateway-operator/requirements.txt index a4f538d4..5c92d4cb 100644 --- a/charms/webhook-gateway-operator/requirements.txt +++ b/charms/webhook-gateway-operator/requirements.txt @@ -1,2 +1,3 @@ +--only-binary=pluggy ops==3.6.0 paas-charm==1.11.1