From 0e8b93e1d05f60a13599e747c06f8007fa9e985f Mon Sep 17 00:00:00 2001 From: Florent 'Skia' Jacquet Date: Thu, 7 May 2026 10:04:18 +0200 Subject: [PATCH 1/3] charm: add hostnames for 'daisy' and 'errors' --- charm/charm.py | 30 ++++++++++++++++----------- charm/tests/integration/test_daisy.py | 6 ++++-- charm/tests/integration/test_web.py | 6 ++++-- charmcraft.yaml | 10 +++++++++ 4 files changed, 36 insertions(+), 16 deletions(-) diff --git a/charm/charm.py b/charm/charm.py index ee05f55..026155e 100755 --- a/charm/charm.py +++ b/charm/charm.py @@ -20,18 +20,6 @@ class ErrorTrackerCharm(ops.CharmBase): def __init__(self, *args): super().__init__(*args) self._error_tracker = ErrorTracker() - self.route_daisy = HaproxyRouteRequirer( - self, - service="daisy", - ports=[self._error_tracker.daisy_port], - relation_name="route_daisy", - ) - self.route_web = HaproxyRouteRequirer( - self, - service="web", - ports=[self._error_tracker.web_port], - relation_name="route_web", - ) self.framework.observe(self.on.start, self._on_start) self.framework.observe(self.on.install, self._on_install) @@ -82,6 +70,24 @@ def _on_config_changed(self, event: ops.ConfigChangedEvent): self.unit.set_ports(*ports) self.unit.set_workload_version(self._error_tracker.get_version()) + + daisy_hostname = self.config.get("daisy_hostname") + errors_hostname = self.config.get("errors_hostname") + self.route_daisy = HaproxyRouteRequirer( + self, + service="daisy", + ports=[self._error_tracker.daisy_port], + relation_name="route_daisy", + hostname=daisy_hostname, + ) + self.route_web = HaproxyRouteRequirer( + self, + service="web", + ports=[self._error_tracker.web_port], + relation_name="route_web", + hostname=errors_hostname, + ) + self.unit.status = ops.ActiveStatus("Ready") diff --git a/charm/tests/integration/test_daisy.py b/charm/tests/integration/test_daisy.py index be09856..4ae928c 100644 --- a/charm/tests/integration/test_daisy.py +++ b/charm/tests/integration/test_daisy.py @@ -10,6 +10,8 @@ HAPROXY = "haproxy" SSC = "self-signed-certificates" +external_hostname = "daisy.internal" + def test_deploy( juju: jubilant.Juju, @@ -24,6 +26,7 @@ def test_deploy( app="daisy", config={ "configuration": error_tracker_config, + "daisy_hostname": external_hostname, "enable_daisy": True, "enable_retracer": False, "enable_timers": False, @@ -37,8 +40,7 @@ def test_deploy( def test_http(juju: jubilant.Juju): - external_hostname = "daisy.internal" - juju.deploy(HAPROXY, channel="2.8/edge", config={"external-hostname": external_hostname}) + juju.deploy(HAPROXY, channel="2.8/edge", config={"external-hostname": "haproxy.internal"}) juju.deploy(SSC, channel="1/edge") juju.integrate(HAPROXY + ":certificates", SSC + ":certificates") diff --git a/charm/tests/integration/test_web.py b/charm/tests/integration/test_web.py index 7d8a747..d98ebd4 100644 --- a/charm/tests/integration/test_web.py +++ b/charm/tests/integration/test_web.py @@ -10,6 +10,8 @@ HAPROXY = "haproxy" SSC = "self-signed-certificates" +external_hostname = "errors.internal" + def test_deploy( juju: jubilant.Juju, @@ -24,6 +26,7 @@ def test_deploy( app="web", config={ "configuration": error_tracker_config, + "errors_hostname": external_hostname, "enable_daisy": False, "enable_retracer": False, "enable_timers": False, @@ -37,8 +40,7 @@ def test_deploy( def test_http(juju: jubilant.Juju): - external_hostname = "errors.internal" - juju.deploy(HAPROXY, channel="2.8/edge", config={"external-hostname": external_hostname}) + juju.deploy(HAPROXY, channel="2.8/edge", config={"external-hostname": "haproxy.internal"}) juju.deploy(SSC, channel="1/edge") juju.integrate(HAPROXY + ":certificates", SSC + ":certificates") diff --git a/charmcraft.yaml b/charmcraft.yaml index 2e9e557..ae1399d 100644 --- a/charmcraft.yaml +++ b/charmcraft.yaml @@ -51,6 +51,16 @@ charm-libs: config: options: + daisy_hostname: + description: | + The hostname to use for daisy + default: "daisy.local" + type: string + errors_hostname: + description: | + The hostname to use for errors + default: "errors.local" + type: string enable_daisy: description: | Whether to enable the 'daisy' component of the Error Tracker From eab678826ffbcc70fed35ed2418c4818cf36ebe8 Mon Sep 17 00:00:00 2001 From: Florent 'Skia' Jacquet Date: Thu, 7 May 2026 10:07:45 +0200 Subject: [PATCH 2/3] charm: rename 'web' to 'errors' --- .github/copilot-instructions.md | 2 +- charm/charm.py | 16 +++++++-------- charm/errortracker.py | 20 +++++++++---------- charm/tests/integration/test_daisy.py | 2 +- .../{test_web.py => test_errors.py} | 10 +++++----- charm/tests/integration/test_retracer.py | 2 +- charm/tests/integration/test_timers.py | 2 +- charmcraft.yaml | 8 ++++---- 8 files changed, 31 insertions(+), 31 deletions(-) rename charm/tests/integration/{test_web.py => test_errors.py} (88%) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 2431d19..ca9f7bd 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -145,7 +145,7 @@ The charm is a **machine charm** targeting `ubuntu@24.04`. It deploys the Error | `enable_daisy` | `true` | Enable the daisy crash receiver service | | `enable_retracer` | `true` | Enable the retracer service | | `enable_timers` | `true` | Enable the housekeeping timer units | -| `enable_web` | `true` | Enable the errors web frontend service | +| `enable_errors` | `true` | Enable the errors web frontend service | | `configuration` | `""` | Full Python configuration file content for the app | | `retracer_failed_queue` | `false` | Whether the retracer listens on the failed retracing queue| diff --git a/charm/charm.py b/charm/charm.py index 026155e..87232dd 100755 --- a/charm/charm.py +++ b/charm/charm.py @@ -46,7 +46,7 @@ def _on_config_changed(self, event: ops.ConfigChangedEvent): enable_daisy = self.config.get("enable_daisy") enable_retracer = self.config.get("enable_retracer") enable_timers = self.config.get("enable_timers") - enable_web = self.config.get("enable_web") + enable_errors = self.config.get("enable_errors") config = self.config.get("configuration") @@ -64,9 +64,9 @@ def _on_config_changed(self, event: ops.ConfigChangedEvent): self._error_tracker.configure_retracer(self.config.get("retracer_failed_queue")) if enable_timers: self._error_tracker.configure_timers() - if enable_web: - self._error_tracker.configure_web() - ports.append(self._error_tracker.web_port) + if enable_errors: + self._error_tracker.configure_errors() + ports.append(self._error_tracker.errors_port) self.unit.set_ports(*ports) self.unit.set_workload_version(self._error_tracker.get_version()) @@ -80,11 +80,11 @@ def _on_config_changed(self, event: ops.ConfigChangedEvent): relation_name="route_daisy", hostname=daisy_hostname, ) - self.route_web = HaproxyRouteRequirer( + self.route_errors = HaproxyRouteRequirer( self, - service="web", - ports=[self._error_tracker.web_port], - relation_name="route_web", + service="errors", + ports=[self._error_tracker.errors_port], + relation_name="route_errors", hostname=errors_hostname, ) diff --git a/charm/errortracker.py b/charm/errortracker.py index c740f5c..2833afc 100644 --- a/charm/errortracker.py +++ b/charm/errortracker.py @@ -48,9 +48,9 @@ def __init__(self): self.enable_retracer = True self.enable_timers = True self.enable_daisy = True - self.enable_web = True + self.enable_errors = True self.daisy_port = 8000 - self.web_port = 9000 + self.errors_port = 9000 def install(self): self._install_deps() @@ -237,9 +237,9 @@ def configure_timers(self): "*-*-* 06:45:00", # every day at 06:45 ) - def configure_web(self): - logger.info("Configuring web") - logger.info("Installing additional web dependencies") + def configure_errors(self): + logger.info("Configuring errors") + logger.info("Installing additional errors dependencies") check_call( [ "apt-get", @@ -256,10 +256,10 @@ def configure_web(self): ) systemd_unit_location = Path("/") / "etc" / "systemd" / "system" systemd_unit_location.mkdir(parents=True, exist_ok=True) - (systemd_unit_location / "et-web.service").write_text( + (systemd_unit_location / "errors.service").write_text( f""" [Unit] -Description=Error Tracker web +Description=Error Tracker errors After=network.target [Service] @@ -268,7 +268,7 @@ def configure_web(self): WorkingDirectory={REPO_LOCATION}/src ExecStartPre={REPO_LOCATION}/src/errors/manage.py migrate --no-input ExecStartPre={REPO_LOCATION}/src/errors/manage.py collectstatic --no-input --clear -ExecStart=bash -c 'exec uwsgi --plugins python3 --http-socket 0.0.0.0:{self.web_port} --wsgi-file {REPO_LOCATION}/src/errors/wsgi.py --static-map "/static={REPO_LOCATION}/src/static/" --chdir {REPO_LOCATION}/src/ --die-on-term --master --env PYTHONPATH={REPO_LOCATION}/src/ --max-requests 4000 --max-worker-lifetime 21600 --processes "$(($(nproc) * 2))"' +ExecStart=bash -c 'exec uwsgi --plugins python3 --http-socket 0.0.0.0:{self.errors_port} --wsgi-file {REPO_LOCATION}/src/errors/wsgi.py --static-map "/static={REPO_LOCATION}/src/static/" --chdir {REPO_LOCATION}/src/ --die-on-term --master --env PYTHONPATH={REPO_LOCATION}/src/ --max-requests 4000 --max-worker-lifetime 21600 --processes "$(($(nproc) * 2))"' Restart=always [Install] @@ -279,7 +279,7 @@ def configure_web(self): check_call(["systemctl", "daemon-reload"]) logger.info("enabling systemd units") - check_call(["systemctl", "enable", "et-web"]) + check_call(["systemctl", "enable", "errors"]) logger.info("restarting systemd units") - check_call(["systemctl", "restart", "et-web"]) + check_call(["systemctl", "restart", "errors"]) diff --git a/charm/tests/integration/test_daisy.py b/charm/tests/integration/test_daisy.py index 4ae928c..dffc1e7 100644 --- a/charm/tests/integration/test_daisy.py +++ b/charm/tests/integration/test_daisy.py @@ -30,7 +30,7 @@ def test_deploy( "enable_daisy": True, "enable_retracer": False, "enable_timers": False, - "enable_web": False, + "enable_errors": False, }, ) diff --git a/charm/tests/integration/test_web.py b/charm/tests/integration/test_errors.py similarity index 88% rename from charm/tests/integration/test_web.py rename to charm/tests/integration/test_errors.py index d98ebd4..ec97c56 100644 --- a/charm/tests/integration/test_web.py +++ b/charm/tests/integration/test_errors.py @@ -23,20 +23,20 @@ def test_deploy( ): juju.deploy( charm=charm_path, - app="web", + app="errors", config={ "configuration": error_tracker_config, "errors_hostname": external_hostname, "enable_daisy": False, "enable_retracer": False, "enable_timers": False, - "enable_web": True, + "enable_errors": True, }, ) - juju.wait(lambda status: jubilant.all_active(status, "web"), timeout=600) + juju.wait(lambda status: jubilant.all_active(status, "errors"), timeout=600) - check_config(juju, amqp, cassandra, swift, "web/0") + check_config(juju, amqp, cassandra, swift, "errors/0") def test_http(juju: jubilant.Juju): @@ -44,7 +44,7 @@ def test_http(juju: jubilant.Juju): juju.deploy(SSC, channel="1/edge") juju.integrate(HAPROXY + ":certificates", SSC + ":certificates") - juju.integrate("web:route_web", HAPROXY) + juju.integrate("errors:route_errors", HAPROXY) juju.wait(lambda status: jubilant.all_active(status, HAPROXY, SSC), timeout=1800) haproxy_ip = juju.status().apps[HAPROXY].units[f"{HAPROXY}/0"].public_address diff --git a/charm/tests/integration/test_retracer.py b/charm/tests/integration/test_retracer.py index 15acdd8..b064f2f 100644 --- a/charm/tests/integration/test_retracer.py +++ b/charm/tests/integration/test_retracer.py @@ -23,7 +23,7 @@ def test_deploy( "enable_daisy": False, "enable_retracer": True, "enable_timers": False, - "enable_web": False, + "enable_errors": False, }, ) diff --git a/charm/tests/integration/test_timers.py b/charm/tests/integration/test_timers.py index bdaa58d..b9574e5 100644 --- a/charm/tests/integration/test_timers.py +++ b/charm/tests/integration/test_timers.py @@ -23,7 +23,7 @@ def test_deploy( "enable_daisy": False, "enable_retracer": False, "enable_timers": True, - "enable_web": False, + "enable_errors": False, }, ) diff --git a/charmcraft.yaml b/charmcraft.yaml index ae1399d..fb86987 100644 --- a/charmcraft.yaml +++ b/charmcraft.yaml @@ -5,7 +5,7 @@ summary: The whole infrastructure collecting and processing crashes for Ubuntu description: | The Error Tracker is made of the following components: * daisy - receiving crashes - * web UI - displaying and visualizing data for developers + * errors - displaying and visualizing data for developers * timers - for processing and housekeeping tasks * retracer - to get symbolic stacktraces out of coredumps @@ -76,9 +76,9 @@ config: Whether to enable the 'timers' component of the Error Tracker default: true type: boolean - enable_web: + enable_errors: description: | - Whether to enable the 'web' component of the Error Tracker + Whether to enable the 'errors' component of the Error Tracker default: true type: boolean configuration: @@ -97,7 +97,7 @@ requires: interface: haproxy-route limit: 1 optional: true - route_web: + route_errors: interface: haproxy-route limit: 1 optional: true From 2d5329e5a472690a365fa6e550443b9c5682dadf Mon Sep 17 00:00:00 2001 From: Florent 'Skia' Jacquet Date: Thu, 7 May 2026 10:52:03 +0200 Subject: [PATCH 3/3] charm: fixup haproxy route and implement ingress as well --- charm/charm.py | 86 ++++++++++++++++++++++++++++++++++++++++--------- charmcraft.yaml | 10 ++++++ 2 files changed, 81 insertions(+), 15 deletions(-) diff --git a/charm/charm.py b/charm/charm.py index 87232dd..5da87a8 100755 --- a/charm/charm.py +++ b/charm/charm.py @@ -8,6 +8,12 @@ import ops from charms.haproxy.v1.haproxy_route import HaproxyRouteRequirer +from charms.traefik_k8s.v2.ingress import ( + IngressPerAppReadyEvent, +) +from charms.traefik_k8s.v2.ingress import ( + IngressPerAppRequirer as IngressRequirer, +) from errortracker import ErrorTracker @@ -21,11 +27,43 @@ def __init__(self, *args): super().__init__(*args) self._error_tracker = ErrorTracker() + self.ingress_daisy = IngressRequirer( + self, + port=self._error_tracker.daisy_port, + strip_prefix=True, + relation_name="ingress_daisy", + ) + self.ingress_errors = IngressRequirer( + self, + port=self._error_tracker.errors_port, + strip_prefix=True, + relation_name="ingress_errors", + ) + + self.route_daisy = HaproxyRouteRequirer( + self, + service="daisy", + ports=[self._error_tracker.daisy_port], + relation_name="route_daisy", + ) + self.route_errors = HaproxyRouteRequirer( + self, + service="errors", + ports=[self._error_tracker.errors_port], + relation_name="route_errors", + ) + self.framework.observe(self.on.start, self._on_start) self.framework.observe(self.on.install, self._on_install) self.framework.observe(self.on.upgrade_charm, self._on_install) self.framework.observe(self.on.config_changed, self._on_config_changed) + self.framework.observe(self.route_daisy.on.ready, self._on_endpoints_ready) + self.framework.observe(self.route_errors.on.ready, self._on_endpoints_ready) + + self.framework.observe(self.ingress_daisy.on.ready, self._on_ingress_ready) + self.framework.observe(self.ingress_errors.on.ready, self._on_ingress_ready) + def _on_start(self, event: ops.StartEvent): """Handle start event.""" self.unit.status = ops.ActiveStatus() @@ -71,24 +109,42 @@ def _on_config_changed(self, event: ops.ConfigChangedEvent): self.unit.set_workload_version(self._error_tracker.get_version()) + self.unit.status = ops.ActiveStatus("Ready") + + def _on_ingress_ready(self, event: IngressPerAppReadyEvent): + logger.info("ingress URL: %s", event.url) daisy_hostname = self.config.get("daisy_hostname") errors_hostname = self.config.get("errors_hostname") - self.route_daisy = HaproxyRouteRequirer( - self, - service="daisy", - ports=[self._error_tracker.daisy_port], - relation_name="route_daisy", - hostname=daisy_hostname, - ) - self.route_errors = HaproxyRouteRequirer( - self, - service="errors", - ports=[self._error_tracker.errors_port], - relation_name="route_errors", - hostname=errors_hostname, - ) + enable_daisy = self.config.get("enable_daisy") + enable_errors = self.config.get("enable_errors") + if enable_daisy: + self.ingress_daisy.provide_ingress_requirements( + port=self._error_tracker.daisy_port, + host=daisy_hostname, + ) + if enable_errors: + self.ingress_errors.provide_ingress_requirements( + port=self._error_tracker.errors_port, + host=errors_hostname, + ) - self.unit.status = ops.ActiveStatus("Ready") + def _on_endpoints_ready(self, event): + daisy_hostname = self.config.get("daisy_hostname") + errors_hostname = self.config.get("errors_hostname") + enable_daisy = self.config.get("enable_daisy") + enable_errors = self.config.get("enable_errors") + if enable_daisy: + self.route_daisy.provide_haproxy_route_requirements( + service="daisy", + ports=[self._error_tracker.daisy_port], + hostname=daisy_hostname, + ) + if enable_errors: + self.route_errors.provide_haproxy_route_requirements( + service="errors", + ports=[self._error_tracker.errors_port], + hostname=errors_hostname, + ) if __name__ == "__main__": # pragma: nocover diff --git a/charmcraft.yaml b/charmcraft.yaml index fb86987..c3e85a3 100644 --- a/charmcraft.yaml +++ b/charmcraft.yaml @@ -48,6 +48,8 @@ parts: charm-libs: - lib: haproxy.haproxy_route version: "1" + - lib: traefik_k8s.ingress + version: "2" config: options: @@ -101,3 +103,11 @@ requires: interface: haproxy-route limit: 1 optional: true + ingress_daisy: + interface: ingress + limit: 1 + optional: true + ingress_errors: + interface: ingress + limit: 1 + optional: true