feat(orchestrator): Phase 3.2 — wire Quadlet path behind feature flag
prod_orchestrator::install_fresh now branches on the new Config::use_quadlet_backends flag (default false): * off (today's production behavior) — unchanged: runtime.create_container + start_container, container parented under archipelago.service's cgroup, FM3 cascade SIGKILL on every archipelago restart. * on — install_via_quadlet renders the manifest as a Quadlet unit via QuadletUnit::from_manifest, writes it atomically into ~/.config/containers/systemd/, calls daemon-reload, and starts the generated <name>.service. Container ends up under user.slice — no more cgroup parented under archipelago, so archipelago restarts don't touch the container's lifetime. Default off so this commit is structurally safe to ship: nothing changes at runtime until an operator opts in. Flip the default once tests/lifecycle/run-20x.sh has gone green against the new path on .228 + .198 (the v1.7.52 release gate). Plumbing: * config.rs — `use_quadlet_backends: bool` w/ Default false * prod_orchestrator.rs — flag stored on the struct, threaded through new(), with set_use_quadlet_backends(bool) test setter * prod_orchestrator.rs — install_via_quadlet helper * dropped the Phase-3.1 #[allow(dead_code)] markers on from_manifest / parse_memory_mib / RestartPolicy::OnFailure now that the call path exists; if a future revert removes the wiring, the warnings come back. Tests: 624 passing, cargo check clean (0 warnings). Existing companion behavior unaffected — render_skips_backend_directives_when_default still passes byte-equal to before quadlet.rs grew the new fields. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9becafafd3
commit
5b2e02bd43
@ -62,6 +62,14 @@ pub struct Config {
|
|||||||
/// Tor SOCKS5 proxy (e.g. 127.0.0.1:9050). When set, ALL Nostr traffic routes through Tor.
|
/// Tor SOCKS5 proxy (e.g. 127.0.0.1:9050). When set, ALL Nostr traffic routes through Tor.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub nostr_tor_proxy: Option<String>,
|
pub nostr_tor_proxy: Option<String>,
|
||||||
|
/// Phase 3.2 of v1.7.52: route orchestrator-managed backend installs
|
||||||
|
/// through Quadlet (`.container` units in ~/.config/containers/systemd
|
||||||
|
/// + systemctl --user start) instead of `podman create + start`. Default
|
||||||
|
/// off so the legacy path stays the production path until the harness
|
||||||
|
/// at tests/lifecycle/run-20x.sh has gone green against the new path
|
||||||
|
/// on .228 + .198. See `project_v1_7_52_phase3_quadlet_design`.
|
||||||
|
#[serde(default)]
|
||||||
|
pub use_quadlet_backends: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
@ -221,6 +229,7 @@ impl Default for Config {
|
|||||||
"wss://relay.nostr.info".into(),
|
"wss://relay.nostr.info".into(),
|
||||||
],
|
],
|
||||||
nostr_tor_proxy: Some("127.0.0.1:9050".into()),
|
nostr_tor_proxy: Some("127.0.0.1:9050".into()),
|
||||||
|
use_quadlet_backends: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,6 +38,7 @@ use tokio::sync::{Mutex, RwLock};
|
|||||||
use crate::config::{Config, ContainerRuntime as ConfigContainerRuntime};
|
use crate::config::{Config, ContainerRuntime as ConfigContainerRuntime};
|
||||||
use crate::container::bitcoin_ui;
|
use crate::container::bitcoin_ui;
|
||||||
use crate::container::filebrowser;
|
use crate::container::filebrowser;
|
||||||
|
use crate::container::quadlet;
|
||||||
use crate::container::traits::ContainerOrchestrator;
|
use crate::container::traits::ContainerOrchestrator;
|
||||||
use crate::update::host_sudo;
|
use crate::update::host_sudo;
|
||||||
|
|
||||||
@ -140,6 +141,12 @@ pub struct ProdContainerOrchestrator {
|
|||||||
/// Root directory for secret files referenced by
|
/// Root directory for secret files referenced by
|
||||||
/// `container.secret_env[*].secret_file`.
|
/// `container.secret_env[*].secret_file`.
|
||||||
secrets_dir: PathBuf,
|
secrets_dir: PathBuf,
|
||||||
|
/// Phase 3.2 feature flag: when true, `install_fresh` writes a
|
||||||
|
/// Quadlet `.container` unit and starts it via systemctl --user
|
||||||
|
/// instead of shelling out to `podman create + start`. Default
|
||||||
|
/// false so the legacy path remains the production path until the
|
||||||
|
/// 20× lifecycle harness goes green against the new path.
|
||||||
|
use_quadlet_backends: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct FileSecretsProvider {
|
struct FileSecretsProvider {
|
||||||
@ -184,6 +191,7 @@ impl ProdContainerOrchestrator {
|
|||||||
bitcoin_ui_paths: bitcoin_ui::RenderPaths::default(),
|
bitcoin_ui_paths: bitcoin_ui::RenderPaths::default(),
|
||||||
filebrowser_paths: filebrowser::EnsurePaths::default(),
|
filebrowser_paths: filebrowser::EnsurePaths::default(),
|
||||||
secrets_dir: PathBuf::from("/var/lib/archipelago/secrets"),
|
secrets_dir: PathBuf::from("/var/lib/archipelago/secrets"),
|
||||||
|
use_quadlet_backends: config.use_quadlet_backends,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,9 +206,18 @@ impl ProdContainerOrchestrator {
|
|||||||
bitcoin_ui_paths: bitcoin_ui::RenderPaths::default(),
|
bitcoin_ui_paths: bitcoin_ui::RenderPaths::default(),
|
||||||
filebrowser_paths: filebrowser::EnsurePaths::default(),
|
filebrowser_paths: filebrowser::EnsurePaths::default(),
|
||||||
secrets_dir: PathBuf::from("/var/lib/archipelago/secrets"),
|
secrets_dir: PathBuf::from("/var/lib/archipelago/secrets"),
|
||||||
|
use_quadlet_backends: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Test-only setter for the Phase 3.2 feature flag, so unit tests
|
||||||
|
/// can exercise the Quadlet-backend install path without going
|
||||||
|
/// through the full Config plumbing.
|
||||||
|
#[cfg(test)]
|
||||||
|
pub fn set_use_quadlet_backends(&mut self, on: bool) {
|
||||||
|
self.use_quadlet_backends = on;
|
||||||
|
}
|
||||||
|
|
||||||
/// Override the bitcoin-ui render paths (secret + output). Only used
|
/// Override the bitcoin-ui render paths (secret + output). Only used
|
||||||
/// by tests that exercise the bitcoin-ui pre-start hook — the
|
/// by tests that exercise the bitcoin-ui pre-start hook — the
|
||||||
/// default `/var/lib/archipelago/...` paths are correct for prod.
|
/// default `/var/lib/archipelago/...` paths are correct for prod.
|
||||||
@ -466,15 +483,50 @@ impl ProdContainerOrchestrator {
|
|||||||
self.run_pre_start_hooks(&lm.manifest.app.id).await?;
|
self.run_pre_start_hooks(&lm.manifest.app.id).await?;
|
||||||
self.apply_data_uid(&resolved_manifest).await?;
|
self.apply_data_uid(&resolved_manifest).await?;
|
||||||
self.ensure_container_network(&resolved_manifest).await?;
|
self.ensure_container_network(&resolved_manifest).await?;
|
||||||
// Production orchestrator: no port offset.
|
|
||||||
self.runtime
|
if self.use_quadlet_backends {
|
||||||
.create_container(&resolved_manifest, &name, 0)
|
// 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.
|
||||||
|
self.install_via_quadlet(&resolved_manifest, &name).await?;
|
||||||
|
} else {
|
||||||
|
// Legacy path. Production until tests/lifecycle/run-20x.sh
|
||||||
|
// goes green against the Quadlet path.
|
||||||
|
self.runtime
|
||||||
|
.create_container(&resolved_manifest, &name, 0)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("create_container {name}"))?;
|
||||||
|
self.runtime
|
||||||
|
.start_container(&name)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("start_container {name}"))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Phase 3.2 install path. Renders the manifest as a Quadlet unit,
|
||||||
|
/// writes it atomically into ~/.config/containers/systemd/, asks
|
||||||
|
/// systemd to reload, and starts the generated service. Errors at
|
||||||
|
/// any step propagate as install_fresh failures — no half-state.
|
||||||
|
async fn install_via_quadlet(
|
||||||
|
&self,
|
||||||
|
resolved_manifest: &AppManifest,
|
||||||
|
name: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
let unit = quadlet::QuadletUnit::from_manifest(resolved_manifest, name);
|
||||||
|
let dir = quadlet::unit_dir()
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("create_container {name}"))?;
|
.context("locate user quadlet unit dir")?;
|
||||||
self.runtime
|
quadlet::write_if_changed(&unit, &dir)
|
||||||
.start_container(&name)
|
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("start_container {name}"))?;
|
.with_context(|| format!("write quadlet unit for {name}"))?;
|
||||||
|
quadlet::daemon_reload_user()
|
||||||
|
.await
|
||||||
|
.context("systemctl --user daemon-reload after writing quadlet unit")?;
|
||||||
|
quadlet::enable_now(&unit.service_name())
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("systemctl --user start {}", unit.service_name()))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -66,9 +66,8 @@ pub enum NetworkMode {
|
|||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum RestartPolicy {
|
pub enum RestartPolicy {
|
||||||
Always,
|
Always,
|
||||||
/// Used by `from_manifest` for backend manifests. Wired into the
|
/// Used by `from_manifest` for backend manifests. Wired through
|
||||||
/// orchestrator in Phase 3.2 (see `project_v1_7_52_phase3_quadlet_design`).
|
/// `install_via_quadlet` (gated by `Config::use_quadlet_backends`).
|
||||||
#[allow(dead_code)]
|
|
||||||
OnFailure,
|
OnFailure,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -253,10 +252,9 @@ fn shell_join(parts: &[String]) -> String {
|
|||||||
|
|
||||||
impl QuadletUnit {
|
impl QuadletUnit {
|
||||||
/// Build a backend-flavour QuadletUnit from a parsed AppManifest.
|
/// Build a backend-flavour QuadletUnit from a parsed AppManifest.
|
||||||
/// Wired into the orchestrator in Phase 3.2 (see
|
/// Wired through `prod_orchestrator::install_via_quadlet`, gated by
|
||||||
/// `project_v1_7_52_phase3_quadlet_design`); marked allow(dead_code)
|
/// `Config::use_quadlet_backends`.
|
||||||
/// here so the warning resurfaces if 3.2 doesn't actually call this.
|
///
|
||||||
#[allow(dead_code)]
|
|
||||||
/// `name` is the on-disk container name (typically the manifest's
|
/// `name` is the on-disk container name (typically the manifest's
|
||||||
/// `app.id`, but the orchestrator may rename — see
|
/// `app.id`, but the orchestrator may rename — see
|
||||||
/// `compute_container_name`). The returned unit is NOT yet written;
|
/// `compute_container_name`). The returned unit is NOT yet written;
|
||||||
@ -329,7 +327,6 @@ impl QuadletUnit {
|
|||||||
/// Parse the manifest's memory_limit string into MiB. Recognises the
|
/// Parse the manifest's memory_limit string into MiB. Recognises the
|
||||||
/// forms our manifests actually use: "<n>", "<n>m"/"<n>M", "<n>g"/"<n>G".
|
/// forms our manifests actually use: "<n>", "<n>m"/"<n>M", "<n>g"/"<n>G".
|
||||||
/// Returns None for anything else; the caller treats None as unlimited.
|
/// Returns None for anything else; the caller treats None as unlimited.
|
||||||
#[allow(dead_code)] // called only from from_manifest (also dead until Phase 3.2)
|
|
||||||
fn parse_memory_mib(raw: &str) -> Option<u32> {
|
fn parse_memory_mib(raw: &str) -> Option<u32> {
|
||||||
let trimmed = raw.trim();
|
let trimmed = raw.trim();
|
||||||
if trimmed.is_empty() {
|
if trimmed.is_empty() {
|
||||||
|
|||||||
@ -96,7 +96,7 @@ Goal: minimum-viable container subsystem.
|
|||||||
| `core/container/src/bitcoin_simulator.rs` | 219 | 0 | -219 | ○ couples with dev_orchestrator |
|
| `core/container/src/bitcoin_simulator.rs` | 219 | 0 | -219 | ○ couples with dev_orchestrator |
|
||||||
| `core/container/src/port_manager.rs` | 175 | 0 | -175 | ○ couples with dev_orchestrator |
|
| `core/container/src/port_manager.rs` | 175 | 0 | -175 | ○ couples with dev_orchestrator |
|
||||||
| `core/archipelago/src/api/rpc/package/install.rs::install_bitcoincoin_rpc_repair` | ~150 | 0 | -150 | ◐ pending fold into orchestrator pre-start |
|
| `core/archipelago/src/api/rpc/package/install.rs::install_bitcoincoin_rpc_repair` | ~150 | 0 | -150 | ◐ pending fold into orchestrator pre-start |
|
||||||
| imperative `install_fresh` in prod_orchestrator | ~120 | 0 | -120 | ○ pending Phase 3.2 Quadlet renderer |
|
| imperative `install_fresh` in prod_orchestrator | ~120 | 0 | -120 | ◐ Phase 3.2 wired behind `use_quadlet_backends` flag (default off); flip default after 20× green |
|
||||||
|
|
||||||
**Today: -270 LoC committed. Outstanding deletes possible: ~1,616 LoC** (if Phase 3 ships fully + dev_mode resolved).
|
**Today: -270 LoC committed. Outstanding deletes possible: ~1,616 LoC** (if Phase 3 ships fully + dev_mode resolved).
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user