From a180611e3b60946017f2d44772444f573afdac15 Mon Sep 17 00:00:00 2001 From: Sina Date: Wed, 2 Sep 2026 15:29:56 -0400 Subject: [PATCH] experiment WIP: multitenancy --- .../charms/grafana_k8s/v1/grafana_source.py | 43 ++++++++++++++++++- .../v1/prometheus_remote_write.py | 33 +++++++++++++- coordinator/src/charm.py | 26 +++++++++-- worker/src/charm.py | 2 +- 4 files changed, 98 insertions(+), 6 deletions(-) diff --git a/coordinator/lib/charms/grafana_k8s/v1/grafana_source.py b/coordinator/lib/charms/grafana_k8s/v1/grafana_source.py index 7aeb9a0..b9656f1 100644 --- a/coordinator/lib/charms/grafana_k8s/v1/grafana_source.py +++ b/coordinator/lib/charms/grafana_k8s/v1/grafana_source.py @@ -239,7 +239,7 @@ def __init__(self, *args): # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 0 +LIBPATCH = 1 logger = logging.getLogger(__name__) @@ -420,6 +420,7 @@ def __init__( unit_datasources: bool = False, app_datasource_url: Optional[str] = None, unit_datasource_url: Optional[str] = None, + app_datasources: Optional[List[Dict]] = None, ) -> None: """Construct a Grafana charm client. @@ -499,6 +500,11 @@ def __init__( is used. This value is per-unit. Pass the base URL only: for ``source_type == "mimir"`` the library appends the ``/prometheus`` query path itself. + app_datasources: an optional list of additional application-level datasource + dicts to publish (under ``grafana_source_datasources``) alongside the + default datasource. Each entry supports the same keys as the consumer's + source dicts: ``source_name``, ``source_type``, ``url`` and, optionally, + ``extra_fields`` and ``secure_extra_fields``. """ _validate_relation_by_interface_and_direction( charm, relation_name, RELATION_INTERFACE_NAME, RelationRole.provides @@ -518,6 +524,7 @@ def __init__( self._extra_fields = extra_fields self._secure_extra_fields = secure_extra_fields + self._extra_app_datasources = app_datasources or [] if not refresh_event: if len(self._charm.meta.containers) == 1: @@ -571,6 +578,33 @@ def update_app_source(self, app_datasource_url: Optional[str] = ""): continue self._set_sources(rel) + def update_app_datasources( + self, datasources: Optional[List[Dict]], app_datasource_url: Optional[str] = None + ) -> None: + """Publish extra application-level datasources and re-publish relation data. + + The provided datasources are written to the app databag under + ``grafana_source_datasources``. Each entry must provide ``source_name``, + ``source_type``, ``url`` and, optionally, ``extra_fields`` and + ``secure_extra_fields`` (matching the shape of the consumer's source dicts). + + Passing an empty list removes the key and reverts to publishing only the + default application-level datasource. + + Args: + datasources: a list of datasource dicts to publish, or empty/None to clear. + app_datasource_url: an optional application-level URL to republish alongside + the extra datasources (e.g. to refresh after an ingress change). + """ + self._extra_app_datasources = list(datasources or []) + if app_datasource_url is not None: + self._app_datasource_url = self._sanitize_source_url(app_datasource_url) + + for rel in self._charm.model.relations.get(self._relation_name, []): + if not rel: + continue + self._set_sources(rel) + def update_unit_source(self, unit_datasource_url: Optional[str] = ""): """Update this unit's datasource URL and re-publish relation data. @@ -632,6 +666,13 @@ def _set_sources(self, rel: Relation): logger.debug("Setting Grafana data sources: %s", self._scrape_data) rel.data[self._charm.app]["grafana_source_data"] = json.dumps(self._scrape_data) + if self._extra_app_datasources: + rel.data[self._charm.app]["grafana_source_datasources"] = json.dumps( + self._extra_app_datasources + ) + elif "grafana_source_datasources" in rel.data[self._charm.app]: + del rel.data[self._charm.app]["grafana_source_datasources"] + @property def _scrape_data(self) -> Dict: """Generate source metadata. diff --git a/coordinator/lib/charms/prometheus_k8s/v1/prometheus_remote_write.py b/coordinator/lib/charms/prometheus_k8s/v1/prometheus_remote_write.py index b47a2bc..0d261da 100644 --- a/coordinator/lib/charms/prometheus_k8s/v1/prometheus_remote_write.py +++ b/coordinator/lib/charms/prometheus_k8s/v1/prometheus_remote_write.py @@ -44,7 +44,7 @@ # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 18 +LIBPATCH = 19 PYDEPS = ["cosl"] @@ -404,6 +404,7 @@ def __init__( peer_relation_name: str, forward_alert_rules: bool = True, extra_alert_labels: Dict = {}, + mimir_tenant_id: Optional[str] = None, ): """API to manage a required relation with the `prometheus_remote_write` interface. @@ -420,6 +421,9 @@ def __init__( peer_relation_name: Name of the peer relation containing units of this charm. forward_alert_rules: Flag to toggle forwarding of charmed alert rules. extra_alert_labels: Dict of extra labels to inject alert rules with. + mimir_tenant_id: The Mimir tenant ID to be announced to the remote-write + provider (e.g. Mimir) over the relation app data bag. If empty or None, + no tenant ID is sent. Raises: RelationNotFoundError: If there is no relation in the charm's metadata.yaml @@ -450,6 +454,7 @@ def __init__( self._alert_rules_path = alert_rules_path self._forward_alert_rules = forward_alert_rules self._extra_alert_labels = extra_alert_labels + self._mimir_tenant_id = mimir_tenant_id or "" self._peer_relation_name = peer_relation_name self.topology = JujuTopology.from_charm(charm) self._tool = CosTool("promql") @@ -528,6 +533,12 @@ def _push_alerts_to_relation_databag(self, relation: Relation) -> None: ) relation.data[self._charm.app]["alert_rules"] = json.dumps(alert_rules_as_dict) + # Announce the Mimir tenant ID (if any) to the provider side (e.g. Mimir). + if self._mimir_tenant_id: + relation.data[self._charm.app]["mimir_tenant_id"] = self._mimir_tenant_id + elif "mimir_tenant_id" in relation.data[self._charm.app]: + del relation.data[self._charm.app]["mimir_tenant_id"] + def reload_alerts(self) -> None: """Reload alert rules from disk and push to relation data.""" self._push_alerts_to_all_relation_databags(None) @@ -1019,3 +1030,23 @@ def _has_relation_error(self, error_key: str, error_label: str) -> bool: return False + @property + def mimir_tenant_ids(self) -> List[str]: + """Map relation IDs to the Mimir tenant IDs reported by consumers. + + Consumers announce the tenant ID they are configured with (e.g. via a + ``mimir_tenant_id`` config option) over the relation app data bag. This + helper exposes the ids announced by each related consumer. + + Returns: + A mapping of relation ID to tenant ID for each relation whose consumer + announced a non-empty tenant ID. + """ + tenants: List[str] = [] + for relation in self._charm.model.relations.get(self._relation_name, []): + if not relation.app: + continue + if tenant := relation.data[relation.app].get("mimir_tenant_id", ""): + tenants.append(tenant) + return tenants + diff --git a/coordinator/src/charm.py b/coordinator/src/charm.py index d1771df..3bb2682 100755 --- a/coordinator/src/charm.py +++ b/coordinator/src/charm.py @@ -442,6 +442,28 @@ def _update_prometheus_api(self) -> None: ingress_url=f"{self.external_url}/prometheus" if self.external_url else None, ) + def _update_grafana_sources(self) -> None: + """Publish the anonymous Mimir datasource plus one per announced tenant.""" + if not self.unit.is_leader(): + return + + base_url = f"{self.external_url or self._service_url}/prometheus" + extra_fields = self._build_grafana_source_extra_fields() + tenant_datasources = [ + { + "source_name": f"mimir-tenant-{tenant}", + "source_type": "prometheus", + "url": base_url, + "extra_fields": extra_fields, + "secure_extra_fields": {"httpHeaderValue1": tenant}, + } + for tenant in sorted(set(self.remote_write_provider.mimir_tenant_ids)) + ] + self.grafana_source.update_app_datasources( + tenant_datasources, + app_datasource_url=base_url, + ) + def _update_datasource_exchange(self) -> None: """Update the grafana-datasource-exchange relations.""" if not self.unit.is_leader(): @@ -481,9 +503,7 @@ def _reconcile(self): self._ensure_mimirtool() self._update_prometheus_api() self._update_datasource_exchange() - self.grafana_source.update_app_source( - app_datasource_url=f"{self.external_url or self._service_url}/prometheus" - ) + self._update_grafana_sources() self.remote_write_provider.update_endpoint() # Open necessary service ports. needed for telemetry proxying. diff --git a/worker/src/charm.py b/worker/src/charm.py index 47f0f24..4d9ec66 100755 --- a/worker/src/charm.py +++ b/worker/src/charm.py @@ -114,7 +114,7 @@ def pebble_layer(self, worker: Worker) -> Layer: "mimir": { "override": "replace", "summary": "mimir worker daemon", - "command": f"/bin/mimir --config.file={CONFIG_FILE} -target {targets} -auth.multitenancy-enabled=false", + "command": f"/bin/mimir --config.file={CONFIG_FILE} -target {targets}", "startup": "enabled", "environment": env, }