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,
|
container = %name,
|
||||||
"host listener disappeared after startup; restarting container"
|
"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])
|
podman_user_scope(&["restart", name])
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("podman restart {name}"))?;
|
.with_context(|| format!("podman restart {name}"))?;
|
||||||
@@ -1085,9 +1087,16 @@ async fn start_container_scoped_if_pasta(
|
|||||||
name: &str,
|
name: &str,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
if uses_pasta_network(manifest) {
|
if uses_pasta_network(manifest) {
|
||||||
// Rootless pasta/conmon inherit the cgroup of the process that starts
|
// Quadlet-managed pasta app: the unit owns the cgroup and the
|
||||||
// them. Starting through archipelago.service lets backend restarts kill
|
// container is rendered --rm — bare `podman start` would fight
|
||||||
// app networking; a transient user scope keeps app daemons independent.
|
// 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
|
podman_user_scope(&["start", name]).await
|
||||||
} else {
|
} else {
|
||||||
runtime.start_container(name).await
|
runtime.start_container(name).await
|
||||||
@@ -1100,6 +1109,9 @@ async fn restart_container_scoped_if_pasta(
|
|||||||
name: &str,
|
name: &str,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
if uses_pasta_network(manifest) {
|
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
|
podman_user_scope(&["restart", name]).await
|
||||||
} else {
|
} else {
|
||||||
let _ = runtime.stop_container(name).await;
|
let _ = runtime.stop_container(name).await;
|
||||||
@@ -2267,7 +2279,15 @@ impl ProdContainerOrchestrator {
|
|||||||
// after proving the container exists. Boot reconciliation must
|
// after proving the container exists. Boot reconciliation must
|
||||||
// not create every catalog app just because a Quadlet unit is
|
// not create every catalog app just because a Quadlet unit is
|
||||||
// absent.
|
// 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? {
|
if let Some(action) = self.migrate_to_quadlet_if_needed(lm, &name).await? {
|
||||||
return Ok(action);
|
return Ok(action);
|
||||||
}
|
}
|
||||||
@@ -2539,10 +2559,7 @@ impl ProdContainerOrchestrator {
|
|||||||
// lost the container record after a crash/reboot. Sync the unit
|
// lost the container record after a crash/reboot. Sync the unit
|
||||||
// bytes first (clears stale Notify=healthy/nc probes), then ask
|
// bytes first (clears stale Notify=healthy/nc probes), then ask
|
||||||
// user systemd to start the generated service.
|
// user systemd to start the generated service.
|
||||||
if self.use_quadlet_backends
|
if self.use_quadlet_backends && self.quadlet_unit_exists(&name).await? {
|
||||||
&& !uses_pasta_network(&resolved_manifest)
|
|
||||||
&& self.quadlet_unit_exists(&name).await?
|
|
||||||
{
|
|
||||||
self.prepare_for_start(&resolved_manifest).await?;
|
self.prepare_for_start(&resolved_manifest).await?;
|
||||||
self.sync_quadlet_unit(lm, &name).await?;
|
self.sync_quadlet_unit(lm, &name).await?;
|
||||||
self.ensure_resolved_source_available(lm).await?;
|
self.ensure_resolved_source_available(lm).await?;
|
||||||
@@ -2725,11 +2742,13 @@ impl ProdContainerOrchestrator {
|
|||||||
self.prepare_for_start(&resolved_manifest).await?;
|
self.prepare_for_start(&resolved_manifest).await?;
|
||||||
self.ensure_container_network(&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.
|
// Phase 3.2 path: declarative .container unit + systemctl.
|
||||||
// Containers parented under user.slice instead of
|
// Containers parented under user.slice instead of
|
||||||
// archipelago.service's cgroup → no FM3 cascade SIGKILL on
|
// 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?;
|
self.install_via_quadlet(&resolved_manifest, &name).await?;
|
||||||
} else {
|
} else {
|
||||||
self.remove_quadlet_unit_if_present(&name).await?;
|
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)
|
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.
|
/// Resolve the per-user quadlet dir under $HOME. Created if missing.
|
||||||
pub async fn unit_dir() -> Result<PathBuf> {
|
pub async fn unit_dir() -> Result<PathBuf> {
|
||||||
let home = std::env::var_os("HOME")
|
let home = std::env::var_os("HOME")
|
||||||
|
|||||||
Reference in New Issue
Block a user