From 28ce898cc722d2a26a927e0eac959c80cc03f347 Mon Sep 17 00:00:00 2001 From: Joe Doss Date: Wed, 8 Apr 2026 00:40:09 -0500 Subject: [PATCH] Fix serve quadlet: SELinux label and ready-signal healthcheck MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two regressions in generate_container_serve_quadlet caught when the generator produced its first real deployment on the test server. First: the container crashed on startup with 'Config file not found: /etc/psi/config.yaml', because the generated quadlet did not set SecurityLabelType. Without container_runtime_t, the container runs under the default container_t SELinux type, which cannot read /etc/psi (labeled etc_t) via a :ro bind mount — :Z would relabel the host dir, which we never want on shared config. Setting SecurityLabelType=container_runtime_t is the standard workaround and matches what generate_container_provider_setup_quadlet already does. Second: quadlet emits Type=notify for .container units by default and expects podman to send sd_notify(READY=1). psi serve does not call sd_notify itself, so the unit used to sit in 'activating' until systemd's TimeoutStartSec killed it. Notify=healthy plus a HealthCmd that curls the /healthz endpoint through the unix socket makes podman fire the ready signal once the first healthcheck passes. HealthStartPeriod=60s gives HSM login + cache decrypt enough headroom before the first probe. --- psi/unitgen.py | 7 +++++++ tests/test_unitgen.py | 21 +++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/psi/unitgen.py b/psi/unitgen.py index 61bce02..896ca45 100644 --- a/psi/unitgen.py +++ b/psi/unitgen.py @@ -354,6 +354,13 @@ def generate_container_serve_quadlet(image: str, settings: PsiSettings) -> str: f"Image={image}", "Exec=serve", "Network=host", + "SecurityLabelType=container_runtime_t", + "Notify=healthy", + f"HealthCmd=curl -sf --unix-socket {sock} http://localhost/healthz", + "HealthInterval=30s", + "HealthRetries=10", + "HealthStartPeriod=60s", + "HealthTimeout=5s", f"Volume={config_dir}:{config_dir}:ro", f"Volume={state}:{state}:Z", f"Volume={runtime_dir}:{runtime_dir}:Z", diff --git a/tests/test_unitgen.py b/tests/test_unitgen.py index 6f3587f..e1df13f 100644 --- a/tests/test_unitgen.py +++ b/tests/test_unitgen.py @@ -419,3 +419,24 @@ def test_tls_renew_quadlet_has_container_name(self, tmp_path: Path) -> None: settings = _mock_settings(tmp_path) content = generate_container_tls_renew_quadlet("psi:latest", settings) assert "ContainerName=psi-tls-renew" in content + + def test_serve_quadlet_has_security_label_type(self, tmp_path: Path) -> None: + """Without SecurityLabelType=container_runtime_t the container cannot + read /etc/psi/config.yaml from the host without a :Z relabel, which we + do not want on shared config directories. + """ + settings = _mock_settings(tmp_path) + content = generate_container_serve_quadlet("psi:latest", settings) + assert "SecurityLabelType=container_runtime_t" in content + + def test_serve_quadlet_has_notify_healthy(self, tmp_path: Path) -> None: + """Quadlet emits Type=notify by default and expects an sd_notify ready + signal. Notify=healthy makes podman send it once the healthcheck first + passes. Without this the unit hangs in 'activating' until TimeoutStartSec. + """ + settings = _mock_settings(tmp_path) + content = generate_container_serve_quadlet("psi:latest", settings) + assert "Notify=healthy" in content + assert "HealthCmd=curl -sf --unix-socket " in content + assert "http://localhost/healthz" in content + assert "HealthStartPeriod=60s" in content