Skip to content

Commit 315681f

Browse files
committed
refactor: remove COSL/Pydantic dependencies
Replaced metrics, tracing, and certificate charm-library usage with direct relation handling. This allows building on RISC-V. There no riscv64 wheel for the pydantic-core version, so uv tries to compile it from source and we get an error on the remote builders. Additional test coverage is added for the direct handling.
1 parent 08518b2 commit 315681f

5 files changed

Lines changed: 954 additions & 1117 deletions

File tree

charmcraft.yaml

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,3 @@ platforms:
5353
ubuntu@26.04:s390x:
5454
ubuntu@26.04:ppc64el:
5555
ubuntu@26.04:riscv64:
56-
57-
charm-libs:
58-
- lib: prometheus_k8s.prometheus_scrape
59-
version: "0"
60-
- lib: tempo_coordinator_k8s.tracing
61-
version: "0"
62-
- lib: certificate_transfer_interface.certificate_transfer
63-
version: "1"

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ requires-python = ">=3.8"
55
dependencies = [
66
"ops",
77
"charmhelpers",
8-
"cosl",
98
]
109

1110
[project.optional-dependencies]

src/charm.py

Lines changed: 159 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,20 @@
44

55
import controlsocket
66
import configchangesocket
7+
import ipaddress
78
import json
89
import logging
910
import secrets
11+
import socket
1012
import urllib.parse
1113
import yaml
1214

13-
from charms.prometheus_k8s.v0.prometheus_scrape import MetricsEndpointProvider
14-
from charms.tempo_coordinator_k8s.v0.tracing import TracingEndpointRequirer
15-
from charms.certificate_transfer_interface.v1.certificate_transfer import (
16-
CertificateTransferRequires,
17-
)
18-
from ops.charm import CharmBase, CollectStatusEvent
15+
from ops.charm import CharmBase, CollectStatusEvent, InstallEvent
1916
from ops.framework import StoredState
20-
from ops.charm import InstallEvent
2117
from ops.main import main
2218
from ops.model import ActiveStatus, BlockedStatus, Relation
2319
from pathlib import Path
24-
from typing import List
20+
from typing import List, Optional, Set
2521

2622
logger = logging.getLogger(__name__)
2723

@@ -40,15 +36,6 @@ class JujuControllerCharm(CharmBase):
4036
def __init__(self, *args):
4137
super().__init__(*args)
4238

43-
self.tracing_requirer = TracingEndpointRequirer(
44-
self,
45-
protocols=["otlp_http", "otlp_grpc"],
46-
relation_name='charm-tracing'
47-
)
48-
self._certificate_transfer = CertificateTransferRequires(
49-
self, relationship_name='charm-tracing-ca-cert'
50-
)
51-
5239
self._stored.set_default(
5340
last_bind_addresses=[],
5441
tracing_endpoints={},
@@ -86,16 +73,21 @@ def _observe(self):
8673
self.framework.observe(
8774
self.on.dbcluster_relation_departed, self._on_dbcluster_relation_departed)
8875
self.framework.observe(
89-
self.tracing_requirer.on.endpoint_changed, self._on_tracing_relation_changed)
76+
self.on.charm_tracing_relation_created, self._on_tracing_relation_created)
9077
self.framework.observe(
91-
self.tracing_requirer.on.endpoint_removed, self._on_tracing_relation_removed)
78+
self.on.charm_tracing_relation_changed, self._on_tracing_relation_changed)
9279
self.framework.observe(
93-
self._certificate_transfer.on.certificate_set_updated,
94-
self._on_receive_ca_cert_updated,
95-
)
80+
self.on.charm_tracing_relation_broken, self._on_tracing_relation_removed)
9681
self.framework.observe(
97-
self._certificate_transfer.on.certificates_removed, self._on_receive_ca_cert_removed)
98-
self._metrics_endpoint = None
82+
self.on.charm_tracing_ca_cert_relation_created,
83+
self._on_certificate_relation_created)
84+
self.framework.observe(
85+
self.on.charm_tracing_ca_cert_relation_changed,
86+
self._on_certificate_relation_changed)
87+
self.framework.observe(
88+
self.on.charm_tracing_ca_cert_relation_broken,
89+
self._on_receive_ca_cert_removed)
90+
self.framework.observe(self.on.update_status, self._on_metrics_refresh)
9991

10092
def _on_install(self, event: InstallEvent):
10193
"""Ensure that the controller configuration file exists."""
@@ -200,16 +192,61 @@ def _configure_metrics_endpoint(self, username, password):
200192
jobs = self._metrics_jobs(username, password)
201193
if jobs is None:
202194
return
203-
if self._metrics_endpoint is None:
204-
self._metrics_endpoint = MetricsEndpointProvider(self, jobs=jobs)
205-
self._metrics_endpoint.set_scrape_job_spec()
206-
else:
207-
self._metrics_endpoint.update_scrape_job_spec(jobs)
195+
self._set_metrics_scrape_data(
196+
self.model.relations["metrics-endpoint"], jobs=jobs)
208197

209198
def _configure_metrics_as_unit(self):
210-
if self._metrics_endpoint is None:
211-
self._metrics_endpoint = MetricsEndpointProvider(self, jobs=[])
212-
self._metrics_endpoint.set_scrape_job_spec()
199+
self._set_metrics_scrape_data(
200+
self.model.relations["metrics-endpoint"], jobs=None)
201+
202+
def _set_metrics_scrape_data(self, relations, jobs: Optional[List[dict]]):
203+
"""Publish the prometheus_scrape relation data without the COSL-backed library."""
204+
for relation in relations:
205+
address, fqdn = self._metrics_unit_address(relation)
206+
relation.data[self.unit].update({
207+
"prometheus_scrape_unit_address": address,
208+
"prometheus_scrape_unit_path": "",
209+
"prometheus_scrape_unit_name": self.unit.name,
210+
"prometheus_scrape_unit_fqdn": fqdn,
211+
})
212+
213+
if not self.unit.is_leader() or jobs is None:
214+
return
215+
216+
metadata = json.dumps(self._prometheus_scrape_metadata(), sort_keys=True)
217+
scrape_jobs = json.dumps(jobs, sort_keys=True)
218+
for relation in relations:
219+
relation.data[self.app].update({
220+
"scrape_metadata": metadata,
221+
"scrape_jobs": scrape_jobs,
222+
"alert_rules": json.dumps({}),
223+
})
224+
225+
def _metrics_unit_address(self, relation: Relation):
226+
binding = self.model.get_binding(relation)
227+
network = getattr(binding, "network", None)
228+
address = getattr(network, "bind_address", None)
229+
if address and self._is_valid_address(str(address)):
230+
return str(address), socket.getfqdn()
231+
232+
fqdn = socket.getfqdn()
233+
return fqdn, fqdn
234+
235+
def _is_valid_address(self, address: str) -> bool:
236+
try:
237+
ipaddress.ip_address(address)
238+
except ValueError:
239+
return False
240+
return True
241+
242+
def _prometheus_scrape_metadata(self) -> dict:
243+
return {
244+
"model": self.model.name,
245+
"model_uuid": str(self.model.uuid),
246+
"application": self.app.name,
247+
"unit": self.unit.name,
248+
"charm_name": self.meta.name,
249+
}
213250

214251
def _remove_metrics_user(self, username):
215252
try:
@@ -230,9 +267,9 @@ def _ensure_metrics_user(self, username, password):
230267
def _reconcile_metrics_as_leader(self, relations):
231268
credentials = self._metrics_credentials(relations)
232269
if credentials is None:
233-
# MetricsEndpointProvider publishes one scrape job to every
234-
# metrics-endpoint relation, so all Prometheus applications share
235-
# one controller user. The oldest relation only seeds its name.
270+
# One scrape job is published to every metrics-endpoint relation,
271+
# so all Prometheus applications share one controller user. The
272+
# oldest relation only seeds its name.
236273
username = metrics_username(min(relations, key=lambda r: r.id))
237274
password = generate_password()
238275
else:
@@ -267,6 +304,18 @@ def _on_metrics_endpoint_relation_created(self, event):
267304
def _on_metrics_reconcile(self, _event):
268305
self._reconcile_metrics(self.model.relations["metrics-endpoint"])
269306

307+
def _on_metrics_refresh(self, _event):
308+
relations = self.model.relations["metrics-endpoint"]
309+
if not relations:
310+
return
311+
312+
jobs = None
313+
if self.unit.is_leader():
314+
credentials = self._metrics_credentials(relations)
315+
if credentials:
316+
jobs = self._metrics_jobs(*credentials)
317+
self._set_metrics_scrape_data(relations, jobs)
318+
270319
def _on_metrics_endpoint_relation_broken(self, event):
271320
relations = [
272321
relation for relation in self.model.relations["metrics-endpoint"]
@@ -299,14 +348,22 @@ def _on_dbcluster_relation_departed(self, event):
299348
relation = event.relation
300349
self._update_bind_addresses(relation)
301350

351+
def _on_tracing_relation_created(self, event):
352+
self._request_tracing_protocols(event.relation)
353+
354+
def _request_tracing_protocols(self, relation: Relation):
355+
if self.unit.is_leader():
356+
relation.data[self.app]["receivers"] = json.dumps(["otlp_http", "otlp_grpc"])
357+
302358
def _on_tracing_relation_changed(self, event):
303-
if not self.tracing_requirer.is_ready(event.relation):
359+
self._request_tracing_protocols(event.relation)
360+
endpoints = self._tracing_endpoints(event.relation)
361+
if not endpoints:
362+
if self._stored.tracing_endpoints:
363+
self._on_tracing_relation_removed(event)
304364
return
305365

306-
self._stored.tracing_endpoints = {
307-
"otlp_grpc": self.tracing_requirer.get_endpoint("otlp_grpc", event.relation),
308-
"otlp_http": self.tracing_requirer.get_endpoint("otlp_http", event.relation),
309-
}
366+
self._stored.tracing_endpoints = endpoints
310367
logger.info("tracing endpoints updated: %s", self._stored.tracing_endpoints)
311368
self._update_charm_tracing_config()
312369

@@ -315,18 +372,76 @@ def _on_tracing_relation_removed(self, event):
315372
logger.info("tracing endpoints cleared")
316373
self._update_charm_tracing_config()
317374

318-
def _on_receive_ca_cert_updated(self, event):
319-
ca_list = event.certificates
375+
def _tracing_endpoints(self, relation: Relation) -> Optional[dict]:
376+
if not relation.app:
377+
return None
378+
379+
try:
380+
receivers = json.loads(relation.data[relation.app].get("receivers", "[]"))
381+
except (json.JSONDecodeError, TypeError):
382+
logger.info("failed parsing tracing receivers for relation %s", relation.id)
383+
return None
384+
385+
if not isinstance(receivers, list):
386+
return None
387+
388+
endpoints = {}
389+
for receiver in receivers:
390+
if not isinstance(receiver, dict):
391+
continue
392+
393+
protocol = receiver.get("protocol")
394+
name = protocol.get("name") if isinstance(protocol, dict) else protocol
395+
url = receiver.get("url")
396+
if name in ("otlp_grpc", "otlp_http") and isinstance(url, str):
397+
endpoints[name] = url
398+
399+
return endpoints or None
400+
401+
def _on_certificate_relation_created(self, event):
402+
if self.unit.is_leader():
403+
event.relation.data[self.app]["version"] = json.dumps(1)
404+
405+
def _on_certificate_relation_changed(self, event):
406+
ca_list = self._certificates_from_relation(event.relation)
320407
if not ca_list:
321408
return
322409

323410
self._stored.ca_cert = '\n'.join(sorted(ca_list))
324-
logger.info("CA certificate updated from relation id %s", event.relation_id)
411+
logger.info("CA certificate updated from relation id %s", event.relation.id)
325412
self._update_charm_tracing_config()
326413

414+
def _certificates_from_relation(self, relation: Relation) -> Set[str]:
415+
if relation.app:
416+
certificates = self._json_string_set(
417+
relation.data[relation.app].get("certificates"))
418+
if certificates:
419+
return certificates
420+
421+
for unit in relation.units:
422+
certificates = self._json_string_set(relation.data[unit].get("chain"))
423+
if certificates:
424+
return certificates
425+
426+
return set()
427+
428+
def _json_string_set(self, value: Optional[str]) -> Set[str]:
429+
if not value:
430+
return set()
431+
432+
try:
433+
data = json.loads(value)
434+
except (json.JSONDecodeError, TypeError):
435+
return set()
436+
437+
if not isinstance(data, list):
438+
return set()
439+
440+
return {item for item in data if isinstance(item, str)}
441+
327442
def _on_receive_ca_cert_removed(self, event):
328443
self._stored.ca_cert = None
329-
logger.info("CA certificate removed from relation id %s", event.relation_id)
444+
logger.info("CA certificate removed from relation id %s", event.relation.id)
330445
self._update_charm_tracing_config()
331446

332447
def _update_bind_addresses(self, relation):

0 commit comments

Comments
 (0)