feat(container): pasta apps join the Quadlet migration — units over scopes
The pasta exclusion paired with the transient-scope machinery: daemon-started pasta/conmon inherit the starter's cgroup, so scopes kept app networking alive across daemon restarts. A quadlet unit provides the same independence (user.slice parentage) with supervision on top — Restart=always and RestartSec=10, which also spaces restarts past pasta's port teardown (the race observed live on strfry, 2026-08-10). Renderer needed nothing: Network=pasta, ports and RestartSec were already in place. Lifted in all four places: migrate_to_quadlet_if_needed, install_fresh, and the missing-container unit-start path; the scoped start/restart helpers are now unit-aware — quadlet-managed pasta routes through systemctl --user (bare podman would fight systemd over an --rm container), legacy pasta keeps its scope, so mixed fleets stay coherent during rollout. Container suite 221/221. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
a0ffc5ca3d
commit
cd795c98ea
@@ -1039,7 +1039,9 @@ async fn repair_manifest_host_ports_after_stability(
|
||||
container = %name,
|
||||
"host listener disappeared after startup; restarting container"
|
||||
);
|
||||
if uses_pasta_network(manifest) {
|
||||
if uses_pasta_network(manifest) && !quadlet::unit_exists(name).await {
|
||||
// Legacy (pre-quadlet) pasta app: no unit owns it, so a transient
|
||||
// scope keeps its networking's cgroup independent of the daemon.
|
||||
podman_user_scope(&["restart", name])
|
||||
.await
|
||||
.with_context(|| format!("podman restart {name}"))?;
|
||||
@@ -1085,9 +1087,16 @@ async fn start_container_scoped_if_pasta(
|
||||
name: &str,
|
||||
) -> Result<()> {
|
||||
if uses_pasta_network(manifest) {
|
||||
// Rootless pasta/conmon inherit the cgroup of the process that starts
|
||||
// them. Starting through archipelago.service lets backend restarts kill
|
||||
// app networking; a transient user scope keeps app daemons independent.
|
||||
// Quadlet-managed pasta app: the unit owns the cgroup and the
|
||||
// container is rendered --rm — bare `podman start` would fight
|
||||
// systemd over it. Restart-through-the-unit starts a stopped one.
|
||||
if quadlet::unit_exists(name).await {
|
||||
return quadlet::restart_service(&format!("{name}.service")).await;
|
||||
}
|
||||
// Legacy pasta app: rootless pasta/conmon inherit the cgroup of the
|
||||
// process that starts them. Starting through archipelago.service lets
|
||||
// backend restarts kill app networking; a transient user scope keeps
|
||||
// app daemons independent.
|
||||
podman_user_scope(&["start", name]).await
|
||||
} else {
|
||||
runtime.start_container(name).await
|
||||
@@ -1100,6 +1109,9 @@ async fn restart_container_scoped_if_pasta(
|
||||
name: &str,
|
||||
) -> Result<()> {
|
||||
if uses_pasta_network(manifest) {
|
||||
if quadlet::unit_exists(name).await {
|
||||
return quadlet::restart_service(&format!("{name}.service")).await;
|
||||
}
|
||||
podman_user_scope(&["restart", name]).await
|
||||
} else {
|
||||
let _ = runtime.stop_container(name).await;
|
||||
@@ -2267,7 +2279,15 @@ impl ProdContainerOrchestrator {
|
||||
// after proving the container exists. Boot reconciliation must
|
||||
// not create every catalog app just because a Quadlet unit is
|
||||
// absent.
|
||||
if self.use_quadlet_backends && !uses_pasta_network(&resolved_manifest) {
|
||||
//
|
||||
// Pasta apps included since 2026-08-10: the old exclusion
|
||||
// paired with the transient-scope machinery (daemon-started
|
||||
// pasta died with the daemon's cgroup). A quadlet unit gives
|
||||
// pasta the same independence with systemd supervision on top
|
||||
// — Restart=always + RestartSec=10, which also spaces restarts
|
||||
// past pasta's port teardown. The scoped start/restart helpers
|
||||
// now defer to the unit whenever one exists.
|
||||
if self.use_quadlet_backends {
|
||||
if let Some(action) = self.migrate_to_quadlet_if_needed(lm, &name).await? {
|
||||
return Ok(action);
|
||||
}
|
||||
@@ -2539,10 +2559,7 @@ impl ProdContainerOrchestrator {
|
||||
// lost the container record after a crash/reboot. Sync the unit
|
||||
// bytes first (clears stale Notify=healthy/nc probes), then ask
|
||||
// user systemd to start the generated service.
|
||||
if self.use_quadlet_backends
|
||||
&& !uses_pasta_network(&resolved_manifest)
|
||||
&& self.quadlet_unit_exists(&name).await?
|
||||
{
|
||||
if self.use_quadlet_backends && self.quadlet_unit_exists(&name).await? {
|
||||
self.prepare_for_start(&resolved_manifest).await?;
|
||||
self.sync_quadlet_unit(lm, &name).await?;
|
||||
self.ensure_resolved_source_available(lm).await?;
|
||||
@@ -2725,11 +2742,13 @@ impl ProdContainerOrchestrator {
|
||||
self.prepare_for_start(&resolved_manifest).await?;
|
||||
self.ensure_container_network(&resolved_manifest).await?;
|
||||
|
||||
if self.use_quadlet_backends && !uses_pasta_network(&resolved_manifest) {
|
||||
if self.use_quadlet_backends {
|
||||
// Phase 3.2 path: declarative .container unit + systemctl.
|
||||
// Containers parented under user.slice instead of
|
||||
// archipelago.service's cgroup → no FM3 cascade SIGKILL on
|
||||
// archipelago restart.
|
||||
// archipelago restart. Pasta apps included since 2026-08-10 —
|
||||
// the unit gives them the same cgroup independence the transient
|
||||
// scopes provided, plus Restart=always supervision.
|
||||
self.install_via_quadlet(&resolved_manifest, &name).await?;
|
||||
} else {
|
||||
self.remove_quadlet_unit_if_present(&name).await?;
|
||||
|
||||
@@ -661,6 +661,19 @@ fn parse_memory_mib(raw: &str) -> Option<u32> {
|
||||
num_part.trim().parse::<u32>().ok()?.checked_mul(mul)
|
||||
}
|
||||
|
||||
/// Does a quadlet `.container` unit exist for this container name?
|
||||
/// Errors count as "unknown" and return false — callers use this to decide
|
||||
/// whether systemd owns the container, and claiming ownership on an
|
||||
/// unreadable answer would route lifecycle ops around a live unit.
|
||||
pub async fn unit_exists(name: &str) -> bool {
|
||||
let Ok(dir) = unit_dir().await else {
|
||||
return false;
|
||||
};
|
||||
tokio::fs::try_exists(dir.join(format!("{name}.container")))
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Resolve the per-user quadlet dir under $HOME. Created if missing.
|
||||
pub async fn unit_dir() -> Result<PathBuf> {
|
||||
let home = std::env::var_os("HOME")
|
||||
|
||||
Reference in New Issue
Block a user